RequestServer: Store a couple of data hashes in disk cache entries

We currently have a FIXME to validate cached data with a crc32. But this
is sort of a non-starter, because we never actually have the cached data
in-memory - we transfer it to the WebContent process via system calls,
and it never reaches userspace in RequestServer.

Chrome makes a bit of an educated gamble here. They assume cosmic bit
blips are extremely unlikely, thus the cached data does not get verified
with a hash. Instead, they store non-cryptographic hashes of some select
fields, and they validate just those hashes.

Here, we store a hash of the cache key in the cache header, and a hash
of the cache header in the cache footer. With these validations, along
with other validations already in-place, we can be reasonably sure we
are not sending corrupt data to the WebContent process.
This commit is contained in:
Timothy Flynn 2025-11-20 15:12:05 -05:00 committed by Jelle Raaijmakers
parent 3663e12585
commit 4470f94129
Notes: github-actions[bot] 2025-11-21 07:49:53 +00:00
3 changed files with 33 additions and 8 deletions

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/HashFunctions.h>
#include <AK/ScopeGuard.h>
#include <LibCore/Notifier.h>
#include <LibCore/System.h>
@ -20,6 +21,7 @@ ErrorOr<CacheHeader> CacheHeader::read_from_stream(Stream& stream)
CacheHeader header;
header.magic = TRY(stream.read_value<u32>());
header.version = TRY(stream.read_value<u32>());
header.key_hash = TRY(stream.read_value<u32>());
header.url_size = TRY(stream.read_value<u32>());
header.url_hash = TRY(stream.read_value<u32>());
header.status_code = TRY(stream.read_value<u32>());
@ -32,6 +34,7 @@ ErrorOr<void> CacheHeader::write_to_stream(Stream& stream) const
{
TRY(stream.write_value(magic));
TRY(stream.write_value(version));
TRY(stream.write_value(key_hash));
TRY(stream.write_value(url_size));
TRY(stream.write_value(url_hash));
TRY(stream.write_value(status_code));
@ -40,10 +43,24 @@ ErrorOr<void> CacheHeader::write_to_stream(Stream& stream) const
return {};
}
u32 CacheHeader::hash() const
{
u32 hash = 0;
hash = pair_int_hash(hash, magic);
hash = pair_int_hash(hash, version);
hash = pair_int_hash(hash, key_hash);
hash = pair_int_hash(hash, url_size);
hash = pair_int_hash(hash, url_hash);
hash = pair_int_hash(hash, status_code);
hash = pair_int_hash(hash, reason_phrase_size);
hash = pair_int_hash(hash, reason_phrase_hash);
return hash;
}
ErrorOr<void> CacheFooter::write_to_stream(Stream& stream) const
{
TRY(stream.write_value(data_size));
TRY(stream.write_value(crc32));
TRY(stream.write_value(header_hash));
return {};
}
@ -51,7 +68,7 @@ ErrorOr<CacheFooter> CacheFooter::read_from_stream(Stream& stream)
{
CacheFooter footer;
footer.data_size = TRY(stream.read_value<u64>());
footer.crc32 = TRY(stream.read_value<u32>());
footer.header_hash = TRY(stream.read_value<u32>());
return footer;
}
@ -84,6 +101,7 @@ ErrorOr<NonnullOwnPtr<CacheEntryWriter>> CacheEntryWriter::create(DiskCache& dis
auto file = TRY(Core::OutputBufferedFile::create(move(unbuffered_file)));
CacheHeader cache_header;
cache_header.key_hash = u64_hash(cache_key);
cache_header.url_size = url.byte_count();
cache_header.url_hash = url.hash();
@ -162,8 +180,6 @@ ErrorOr<void> CacheEntryWriter::write_data(ReadonlyBytes data)
}
m_cache_footer.data_size += data.size();
// FIXME: Update the crc.
return {};
}
@ -174,6 +190,8 @@ ErrorOr<void> CacheEntryWriter::flush(HTTP::HeaderMap response_headers)
if (m_marked_for_deletion)
return Error::from_string_literal("Cache entry has been deleted");
m_cache_footer.header_hash = m_cache_header.hash();
if (auto result = m_file->write_value(m_cache_footer); result.is_error()) {
dbgln("\033[31;1mUnable to flush cache entry for\033[0m {}: {}", m_url, result.error());
remove();
@ -209,6 +227,9 @@ ErrorOr<NonnullOwnPtr<CacheEntryReader>> CacheEntryReader::create(DiskCache& dis
if (cache_header.version != CACHE_VERSION)
return Error::from_string_literal("Version mismatch");
if (cache_header.key_hash != u64_hash(cache_key))
return Error::from_string_literal("Key hash mismatch");
url = TRY(String::from_stream(*file, cache_header.url_size));
if (url.hash() != cache_header.url_hash)
return Error::from_string_literal("URL hash mismatch");
@ -350,8 +371,8 @@ ErrorOr<void> CacheEntryReader::read_and_validate_footer()
if (m_cache_footer.data_size != m_data_size)
return Error::from_string_literal("Invalid data size in footer");
// FIXME: Validate the crc.
if (m_cache_footer.header_hash != m_cache_header.hash())
return Error::from_string_literal("Invalid header hash in footer");
return {};
}

View file

@ -22,11 +22,15 @@ struct CacheHeader {
static ErrorOr<CacheHeader> read_from_stream(Stream&);
ErrorOr<void> write_to_stream(Stream&) const;
u32 hash() const;
static constexpr auto CACHE_MAGIC = 0xcafef00du;
u32 magic { CACHE_MAGIC };
u32 version { CACHE_VERSION };
u32 key_hash { 0 };
u32 url_size { 0 };
u32 url_hash { 0 };
@ -40,7 +44,7 @@ struct CacheFooter {
ErrorOr<void> write_to_stream(Stream&) const;
u64 data_size { 0 };
u32 crc32 { 0 };
u32 header_hash { 0 };
};
// A cache entry is an amalgamation of all information needed to reconstruct HTTP responses. It is created once we have

View file

@ -11,6 +11,6 @@
namespace RequestServer {
// Increment this version when a breaking change is made to the cache index or cache entry formats.
static constexpr inline u32 CACHE_VERSION = 3u;
static constexpr inline u32 CACHE_VERSION = 4u;
}