| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/code_cache/generated_code_cache.h" |
| |
| #include <iostream> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/feature_list.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "components/services/storage/public/cpp/big_io_buffer.h" |
| #include "content/public/common/url_constants.h" |
| #include "crypto/sha2.h" |
| #include "net/base/completion_once_callback.h" |
| #include "net/base/features.h" |
| #include "net/base/network_isolation_key.h" |
| #include "net/base/url_util.h" |
| #include "url/gurl.h" |
| |
| using storage::BigIOBuffer; |
| |
| namespace content { |
| |
| namespace { |
| |
| constexpr char kPrefix[] = "_key"; |
| constexpr char kSeparator[] = " \n"; |
| |
| // We always expect to receive valid URLs that can be used as keys to the code |
| // cache. The relevant checks (for ex: resource_url is valid, origin_lock is |
| // not opque etc.,) must be done prior to requesting the code cache. |
| // |
| // This function doesn't enforce anything in the production code. It is here |
| // to make the assumptions explicit and to catch any errors when DCHECKs are |
| // enabled. |
| void CheckValidKeys(const GURL& resource_url, |
| const GURL& origin_lock, |
| GeneratedCodeCache::CodeCacheType cache_type) { |
| // If the resource url is invalid don't cache the code. |
| DCHECK(resource_url.is_valid()); |
| bool resource_url_is_chrome_or_chrome_untrusted = |
| resource_url.SchemeIs(content::kChromeUIScheme) || |
| resource_url.SchemeIs(content::kChromeUIUntrustedScheme); |
| DCHECK(resource_url.SchemeIsHTTPOrHTTPS() || |
| resource_url_is_chrome_or_chrome_untrusted); |
| |
| // |origin_lock| should be either empty or should have |
| // Http/Https/chrome/chrome-untrusted schemes and it should not be a URL with |
| // opaque origin. Empty origin_locks are allowed when the renderer is not |
| // locked to an origin. |
| bool origin_lock_is_chrome_or_chrome_untrusted = |
| origin_lock.SchemeIs(content::kChromeUIScheme) || |
| origin_lock.SchemeIs(content::kChromeUIUntrustedScheme); |
| DCHECK(origin_lock.is_empty() || |
| ((origin_lock.SchemeIsHTTPOrHTTPS() || |
| origin_lock_is_chrome_or_chrome_untrusted) && |
| !url::Origin::Create(origin_lock).opaque())); |
| |
| // The chrome and chrome-untrusted schemes are only used with the WebUI |
| // code cache type. |
| DCHECK_EQ(origin_lock_is_chrome_or_chrome_untrusted, |
| cache_type == GeneratedCodeCache::kWebUIJavaScript); |
| DCHECK_EQ(resource_url_is_chrome_or_chrome_untrusted, |
| cache_type == GeneratedCodeCache::kWebUIJavaScript); |
| } |
| |
| // Generates the cache key for the given |resource_url|, |origin_lock| and |
| // |nik|. |
| // |resource_url| is the url corresponding to the requested resource. |
| // |origin_lock| is the origin that the renderer which requested this |
| // resource is locked to. |
| // |nik| is the network isolation key that consists of top-level-site that |
| // initiated the request. |
| // For example, if SitePerProcess is enabled and http://script.com/script1.js is |
| // requested by http://example.com, then http://script.com/script.js is the |
| // resource_url and http://example.com is the origin_lock. |
| // |
| // This returns the key by concatenating the serialized url, origin lock and nik |
| // with a separator in between. |origin_lock| could be empty when renderer is |
| // not locked to an origin (ex: SitePerProcess is disabled) and it is safe to |
| // use only |resource_url| as the key in such cases. |
| // TODO(wjmaclean): Either convert this to use a SiteInfo object, or convert it |
| // to something not based on URLs. |
| std::string GetCacheKey(const GURL& resource_url, |
| const GURL& origin_lock, |
| const net::NetworkIsolationKey& nik, |
| GeneratedCodeCache::CodeCacheType cache_type) { |
| CheckValidKeys(resource_url, origin_lock, cache_type); |
| |
| // Add a prefix _ so it can't be parsed as a valid URL. |
| std::string key(kPrefix); |
| // Remove reference, username and password sections of the URL. |
| key.append(net::SimplifyUrlForRequest(resource_url).spec()); |
| // Add a separator between URL and origin to avoid any possibility of |
| // attacks by crafting the URL. URLs do not contain any control ASCII |
| // characters, and also space is encoded. So use ' \n' as a seperator. |
| key.append(kSeparator); |
| |
| if (origin_lock.is_valid()) |
| key.append(net::SimplifyUrlForRequest(origin_lock).spec()); |
| |
| if (base::FeatureList::IsEnabled( |
| net::features::kSplitCacheByNetworkIsolationKey)) { |
| key.append(kSeparator); |
| key.append(nik.ToString()); |
| } |
| return key; |
| } |
| |
| constexpr size_t kResponseTimeSizeInBytes = sizeof(int64_t); |
| constexpr size_t kDataSizeInBytes = sizeof(uint32_t); |
| constexpr size_t kHeaderSizeInBytes = |
| kResponseTimeSizeInBytes + kDataSizeInBytes; |
| // The SHA-256 checksum is used as the key for the de-duplicated code data. We |
| // must convert the checksum to a string key in a way that is guaranteed not to |
| // match a key generated by |GetCacheKey|. A simple way to do this is to convert |
| // it to a hex number string, which is twice as long as the checksum. |
| constexpr size_t kSHAKeySizeInBytes = 2 * crypto::kSHA256Length; |
| |
| // This is the threshold for storing the header and cached code in stream 0, |
| // which is read into memory on opening an entry. JavaScript code caching stores |
| // time stamps with no data, or timestamps with just a tag, and we observe many |
| // 8 and 16 byte reads and writes. Make the threshold larger to speed up small |
| // code entries too. |
| constexpr size_t kInlineDataLimit = 4096; |
| // This is the maximum size for code that will be stored under the key generated |
| // by |GetCacheKey|. Each origin will get its own copy of the generated code for |
| // a given resource. Code that is larger than this limit will be stored under a |
| // key derived from the code checksum, and each origin using a given resource |
| // gets its own small entry under the key generated by |GetCacheKey| that holds |
| // the hash, enabling a two stage lookup. This limit was determined empirically |
| // by a Finch experiment. |
| constexpr size_t kDedicatedDataLimit = 16384; |
| |
| void WriteCommonDataHeader(scoped_refptr<net::IOBufferWithSize> buffer, |
| const base::Time& response_time, |
| uint32_t data_size) { |
| DCHECK_LE(static_cast<int>(kHeaderSizeInBytes), buffer->size()); |
| int64_t serialized_time = |
| response_time.ToDeltaSinceWindowsEpoch().InMicroseconds(); |
| memcpy(buffer->data(), &serialized_time, kResponseTimeSizeInBytes); |
| // Copy size to small data buffer. |
| memcpy(buffer->data() + kResponseTimeSizeInBytes, &data_size, |
| kDataSizeInBytes); |
| } |
| |
| void ReadCommonDataHeader(scoped_refptr<net::IOBufferWithSize> buffer, |
| base::Time* response_time, |
| uint32_t* data_size) { |
| DCHECK_LE(static_cast<int>(kHeaderSizeInBytes), buffer->size()); |
| int64_t raw_response_time; |
| memcpy(&raw_response_time, buffer->data(), kResponseTimeSizeInBytes); |
| *response_time = base::Time::FromDeltaSinceWindowsEpoch( |
| base::Microseconds(raw_response_time)); |
| memcpy(data_size, buffer->data() + kResponseTimeSizeInBytes, |
| kDataSizeInBytes); |
| } |
| |
| static_assert(mojo_base::BigBuffer::kMaxInlineBytes <= |
| std::numeric_limits<int>::max(), |
| "Buffer size calculations may overflow int"); |
| |
| net::CacheType CodeCacheTypeToNetCacheType( |
| GeneratedCodeCache::CodeCacheType type) { |
| switch (type) { |
| case GeneratedCodeCache::CodeCacheType::kJavaScript: |
| return net::GENERATED_BYTE_CODE_CACHE; |
| case GeneratedCodeCache::CodeCacheType::kWebAssembly: |
| return net::GENERATED_NATIVE_CODE_CACHE; |
| case GeneratedCodeCache::CodeCacheType::kWebUIJavaScript: |
| return net::GENERATED_WEBUI_BYTE_CODE_CACHE; |
| } |
| NOTREACHED(); |
| } |
| |
| } // namespace |
| |
| bool GeneratedCodeCache::IsValidHeader( |
| scoped_refptr<net::IOBufferWithSize> small_buffer) const { |
| size_t buffer_size = small_buffer->size(); |
| if (buffer_size < kHeaderSizeInBytes) |
| return false; |
| uint32_t data_size; |
| memcpy(&data_size, small_buffer->data() + kResponseTimeSizeInBytes, |
| kDataSizeInBytes); |
| if (data_size <= kInlineDataLimit) |
| return buffer_size == kHeaderSizeInBytes + data_size; |
| if (!ShouldDeduplicateEntry(data_size)) |
| return buffer_size == kHeaderSizeInBytes; |
| return buffer_size == kHeaderSizeInBytes + kSHAKeySizeInBytes; |
| } |
| |
| std::string GeneratedCodeCache::GetResourceURLFromKey(const std::string& key) { |
| constexpr size_t kPrefixStringLen = base::size(kPrefix) - 1; |
| // |key| may not have a prefix and separator (e.g. for deduplicated entries). |
| // In that case, return an empty string. |
| const size_t separator_index = key.find(kSeparator); |
| if (key.length() < kPrefixStringLen || separator_index == std::string::npos) { |
| return std::string(); |
| } |
| |
| std::string resource_url = |
| key.substr(kPrefixStringLen, separator_index - kPrefixStringLen); |
| return resource_url; |
| } |
| |
| void GeneratedCodeCache::CollectStatistics( |
| GeneratedCodeCache::CacheEntryStatus status) { |
| switch (cache_type_) { |
| case GeneratedCodeCache::CodeCacheType::kJavaScript: |
| case GeneratedCodeCache::CodeCacheType::kWebUIJavaScript: |
| UMA_HISTOGRAM_ENUMERATION("SiteIsolatedCodeCache.JS.Behaviour", status); |
| break; |
| case GeneratedCodeCache::CodeCacheType::kWebAssembly: |
| UMA_HISTOGRAM_ENUMERATION("SiteIsolatedCodeCache.WASM.Behaviour", status); |
| break; |
| } |
| } |
| |
| // Stores the information about a pending request while disk backend is |
| // being initialized or another request for the same key is live. |
| class GeneratedCodeCache::PendingOperation { |
| public: |
| PendingOperation(Operation op, |
| const std::string& key, |
| scoped_refptr<net::IOBufferWithSize> small_buffer, |
| scoped_refptr<BigIOBuffer> large_buffer) |
| : op_(op), |
| key_(key), |
| small_buffer_(small_buffer), |
| large_buffer_(large_buffer) { |
| DCHECK(Operation::kWrite == op_ || Operation::kWriteWithSHAKey == op_); |
| } |
| |
| PendingOperation(Operation op, |
| const std::string& key, |
| ReadDataCallback read_callback) |
| : op_(op), key_(key), read_callback_(std::move(read_callback)) { |
| DCHECK_EQ(Operation::kFetch, op_); |
| } |
| |
| PendingOperation(Operation op, |
| const std::string& key, |
| const base::Time& response_time, |
| scoped_refptr<net::IOBufferWithSize> small_buffer, |
| scoped_refptr<BigIOBuffer> large_buffer, |
| ReadDataCallback read_callback) |
| : op_(op), |
| key_(key), |
| response_time_(response_time), |
| small_buffer_(small_buffer), |
| large_buffer_(large_buffer), |
| read_callback_(std::move(read_callback)) { |
| DCHECK_EQ(Operation::kFetchWithSHAKey, op_); |
| } |
| |
| PendingOperation(Operation op, const std::string& key) : op_(op), key_(key) { |
| DCHECK_EQ(Operation::kDelete, op_); |
| } |
| |
| PendingOperation(Operation op, GetBackendCallback backend_callback) |
| : op_(op), backend_callback_(std::move(backend_callback)) { |
| DCHECK_EQ(Operation::kGetBackend, op_); |
| } |
| |
| ~PendingOperation(); |
| |
| Operation operation() const { return op_; } |
| const std::string& key() const { return key_; } |
| scoped_refptr<net::IOBufferWithSize> small_buffer() { return small_buffer_; } |
| scoped_refptr<BigIOBuffer> large_buffer() { return large_buffer_; } |
| ReadDataCallback TakeReadCallback() { return std::move(read_callback_); } |
| GetBackendCallback TakeBackendCallback() { |
| return std::move(backend_callback_); |
| } |
| |
| // These are called by Fetch operations to hold the buffers we create once the |
| // entry is opened. |
| void set_small_buffer(scoped_refptr<net::IOBufferWithSize> small_buffer) { |
| DCHECK_EQ(Operation::kFetch, op_); |
| small_buffer_ = small_buffer; |
| } |
| void set_large_buffer(scoped_refptr<BigIOBuffer> large_buffer) { |
| DCHECK_EQ(Operation::kFetch, op_); |
| large_buffer_ = large_buffer; |
| } |
| |
| // This returns the site-specific response time for merged code entries. |
| const base::Time& response_time() const { |
| DCHECK_EQ(Operation::kFetchWithSHAKey, op_); |
| return response_time_; |
| } |
| |
| // These are called by write and fetch operations to track buffer completions |
| // and signal when the operation has finished, and whether it was successful. |
| bool succeeded() const { return succeeded_; } |
| |
| bool AddBufferCompletion(bool succeeded) { |
| DCHECK(op_ == Operation::kWrite || op_ == Operation::kWriteWithSHAKey || |
| op_ == Operation::kFetch || op_ == Operation::kFetchWithSHAKey); |
| if (!succeeded) |
| succeeded_ = false; |
| DCHECK_GT(2, completions_); |
| completions_++; |
| return completions_ == 2; |
| } |
| |
| private: |
| const Operation op_; |
| const std::string key_; |
| const base::Time response_time_; |
| scoped_refptr<net::IOBufferWithSize> small_buffer_; |
| scoped_refptr<BigIOBuffer> large_buffer_; |
| ReadDataCallback read_callback_; |
| GetBackendCallback backend_callback_; |
| int completions_ = 0; |
| bool succeeded_ = true; |
| }; |
| |
| GeneratedCodeCache::PendingOperation::~PendingOperation() = default; |
| |
| GeneratedCodeCache::GeneratedCodeCache(const base::FilePath& path, |
| int max_size_bytes, |
| CodeCacheType cache_type) |
| : backend_state_(kInitializing), |
| path_(path), |
| max_size_bytes_(max_size_bytes), |
| cache_type_(cache_type) { |
| CreateBackend(); |
| } |
| |
| GeneratedCodeCache::~GeneratedCodeCache() = default; |
| |
| void GeneratedCodeCache::GetBackend(GetBackendCallback callback) { |
| switch (backend_state_) { |
| case kFailed: |
| std::move(callback).Run(nullptr); |
| return; |
| case kInitialized: |
| std::move(callback).Run(backend_.get()); |
| return; |
| case kInitializing: |
| pending_ops_.emplace(std::make_unique<PendingOperation>( |
| Operation::kGetBackend, std::move(callback))); |
| return; |
| } |
| } |
| |
| void GeneratedCodeCache::WriteEntry(const GURL& url, |
| const GURL& origin_lock, |
| const net::NetworkIsolationKey& nik, |
| const base::Time& response_time, |
| mojo_base::BigBuffer data) { |
| if (backend_state_ == kFailed) { |
| // Silently fail the request. |
| CollectStatistics(CacheEntryStatus::kError); |
| return; |
| } |
| |
| // Reject buffers that are large enough to cause overflow problems. |
| if (data.size() >= std::numeric_limits<int32_t>::max()) |
| return; |
| |
| scoped_refptr<net::IOBufferWithSize> small_buffer; |
| scoped_refptr<BigIOBuffer> large_buffer; |
| uint32_t data_size = static_cast<uint32_t>(data.size()); |
| // We have three different cache entry layouts, depending on data size. |
| if (data_size <= kInlineDataLimit) { |
| // 1. Inline |
| // [stream0] response time, size, data |
| // [stream1] <empty> |
| small_buffer = base::MakeRefCounted<net::IOBufferWithSize>( |
| kHeaderSizeInBytes + data.size()); |
| // Copy |data| into the small buffer. |
| memcpy(small_buffer->data() + kHeaderSizeInBytes, data.data(), data.size()); |
| // Write 0 bytes and truncate stream 1 to clear any stale data. |
| large_buffer = base::MakeRefCounted<BigIOBuffer>(mojo_base::BigBuffer()); |
| } else if (!ShouldDeduplicateEntry(data_size)) { |
| // 2. Dedicated |
| // [stream0] response time, size |
| // [stream1] data |
| small_buffer = |
| base::MakeRefCounted<net::IOBufferWithSize>(kHeaderSizeInBytes); |
| large_buffer = base::MakeRefCounted<BigIOBuffer>(std::move(data)); |
| } else { |
| // 3. Indirect |
| // [stream0] response time, size, checksum |
| // [stream1] <empty> |
| // [stream0 (checksum key entry)] <empty> |
| // [stream1 (checksum key entry)] data |
| |
| // Make a copy of the data before hashing. A compromised renderer could |
| // change shared memory before we can compute the hash and write the data. |
| // TODO(1135729) Eliminate this copy when the shared memory can't be written |
| // by the sender. |
| mojo_base::BigBuffer copy({data.data(), data.size()}); |
| if (copy.size() != data.size()) |
| return; |
| data = mojo_base::BigBuffer(); // Release the old buffer. |
| uint8_t result[crypto::kSHA256Length]; |
| crypto::SHA256HashString( |
| base::StringPiece(reinterpret_cast<char*>(copy.data()), copy.size()), |
| result, base::size(result)); |
| std::string checksum_key = base::HexEncode(result, base::size(result)); |
| small_buffer = base::MakeRefCounted<net::IOBufferWithSize>( |
| kHeaderSizeInBytes + kSHAKeySizeInBytes); |
| // Copy |checksum_key| into the small buffer. |
| DCHECK_EQ(kSHAKeySizeInBytes, checksum_key.length()); |
| memcpy(small_buffer->data() + kHeaderSizeInBytes, checksum_key.data(), |
| kSHAKeySizeInBytes); |
| // Write 0 bytes and truncate stream 1 to clear any stale data. |
| large_buffer = base::MakeRefCounted<BigIOBuffer>(mojo_base::BigBuffer()); |
| |
| // Issue another write operation for the code, with the checksum as the key |
| // and nothing in the header. |
| auto small_buffer2 = base::MakeRefCounted<net::IOBufferWithSize>(0); |
| auto large_buffer2 = base::MakeRefCounted<BigIOBuffer>(std::move(copy)); |
| auto op2 = std::make_unique<PendingOperation>(Operation::kWriteWithSHAKey, |
| checksum_key, small_buffer2, |
| large_buffer2); |
| EnqueueOperation(std::move(op2)); |
| } |
| WriteCommonDataHeader(small_buffer, response_time, data_size); |
| |
| // Create the write operation. |
| std::string key = GetCacheKey(url, origin_lock, nik, cache_type_); |
| auto op = std::make_unique<PendingOperation>(Operation::kWrite, key, |
| small_buffer, large_buffer); |
| EnqueueOperation(std::move(op)); |
| } |
| |
| void GeneratedCodeCache::FetchEntry(const GURL& url, |
| const GURL& origin_lock, |
| const net::NetworkIsolationKey& nik, |
| ReadDataCallback read_data_callback) { |
| if (backend_state_ == kFailed) { |
| CollectStatistics(CacheEntryStatus::kError); |
| // Fail the request. |
| std::move(read_data_callback).Run(base::Time(), mojo_base::BigBuffer()); |
| return; |
| } |
| |
| std::string key = GetCacheKey(url, origin_lock, nik, cache_type_); |
| auto op = std::make_unique<PendingOperation>(Operation::kFetch, key, |
| std::move(read_data_callback)); |
| EnqueueOperation(std::move(op)); |
| } |
| |
| void GeneratedCodeCache::DeleteEntry(const GURL& url, |
| const GURL& origin_lock, |
| const net::NetworkIsolationKey& nik) { |
| if (backend_state_ == kFailed) { |
| // Silently fail. |
| CollectStatistics(CacheEntryStatus::kError); |
| return; |
| } |
| |
| std::string key = GetCacheKey(url, origin_lock, nik, cache_type_); |
| auto op = std::make_unique<PendingOperation>(Operation::kDelete, key); |
| EnqueueOperation(std::move(op)); |
| } |
| |
| void GeneratedCodeCache::CreateBackend() { |
| // Create a new Backend pointer that cleans itself if the GeneratedCodeCache |
| // instance is not live when the CreateCacheBackend finishes. |
| scoped_refptr<base::RefCountedData<ScopedBackendPtr>> shared_backend_ptr = |
| new base::RefCountedData<ScopedBackendPtr>(); |
| |
| net::CompletionOnceCallback create_backend_complete = |
| base::BindOnce(&GeneratedCodeCache::DidCreateBackend, |
| weak_ptr_factory_.GetWeakPtr(), shared_backend_ptr); |
| |
| // If the initialization of the existing cache fails, this call would delete |
| // all the contents and recreates a new one. |
| int rv = disk_cache::CreateCacheBackend( |
| CodeCacheTypeToNetCacheType(cache_type_), net::CACHE_BACKEND_SIMPLE, |
| path_, max_size_bytes_, disk_cache::ResetHandling::kResetOnError, nullptr, |
| &shared_backend_ptr->data, std::move(create_backend_complete)); |
| if (rv != net::ERR_IO_PENDING) { |
| DidCreateBackend(shared_backend_ptr, rv); |
| } |
| } |
| |
| void GeneratedCodeCache::DidCreateBackend( |
| scoped_refptr<base::RefCountedData<ScopedBackendPtr>> backend_ptr, |
| int rv) { |
| if (rv != net::OK) { |
| backend_state_ = kFailed; |
| } else { |
| backend_ = std::move(backend_ptr->data); |
| backend_state_ = kInitialized; |
| } |
| IssuePendingOperations(); |
| } |
| |
| void GeneratedCodeCache::EnqueueOperation( |
| std::unique_ptr<PendingOperation> op) { |
| if (backend_state_ != kInitialized) { |
| // Insert it into the list of pending operations while the backend is |
| // still being opened. |
| pending_ops_.emplace(std::move(op)); |
| return; |
| } |
| |
| EnqueueOperationAndIssueIfNext(std::move(op)); |
| } |
| |
| void GeneratedCodeCache::IssuePendingOperations() { |
| // Issue any operations that were received while creating the backend. |
| while (!pending_ops_.empty()) { |
| // Take ownership of the next PendingOperation here. |op| will either be |
| // moved onto a queue in active_entries_map_ or issued and completed in |
| // |DoPendingGetBackend|. |
| std::unique_ptr<PendingOperation> op = std::move(pending_ops_.front()); |
| pending_ops_.pop(); |
| // Properly enqueue/dequeue ops for Write, Fetch, and Delete. |
| if (op->operation() != Operation::kGetBackend) { |
| EnqueueOperationAndIssueIfNext(std::move(op)); |
| } else { |
| // There is no queue for get backend operations. Issue them immediately. |
| IssueOperation(op.get()); |
| } |
| } |
| } |
| |
| void GeneratedCodeCache::IssueOperation(PendingOperation* op) { |
| switch (op->operation()) { |
| case kFetch: |
| case kFetchWithSHAKey: |
| FetchEntryImpl(op); |
| break; |
| case kWrite: |
| case kWriteWithSHAKey: |
| WriteEntryImpl(op); |
| break; |
| case kDelete: |
| DeleteEntryImpl(op); |
| break; |
| case kGetBackend: |
| DoPendingGetBackend(op); |
| break; |
| } |
| } |
| |
| void GeneratedCodeCache::WriteEntryImpl(PendingOperation* op) { |
| DCHECK(Operation::kWrite == op->operation() || |
| Operation::kWriteWithSHAKey == op->operation()); |
| if (backend_state_ != kInitialized) { |
| // Silently fail the request. |
| CloseOperationAndIssueNext(op); |
| return; |
| } |
| |
| disk_cache::EntryResult result = backend_->OpenOrCreateEntry( |
| op->key(), net::LOW, |
| base::BindOnce(&GeneratedCodeCache::OpenCompleteForWrite, |
| weak_ptr_factory_.GetWeakPtr(), op)); |
| |
| if (result.net_error() != net::ERR_IO_PENDING) { |
| OpenCompleteForWrite(op, std::move(result)); |
| } |
| } |
| |
| void GeneratedCodeCache::OpenCompleteForWrite( |
| PendingOperation* op, |
| disk_cache::EntryResult entry_result) { |
| DCHECK(Operation::kWrite == op->operation() || |
| Operation::kWriteWithSHAKey == op->operation()); |
| if (entry_result.net_error() != net::OK) { |
| CollectStatistics(CacheEntryStatus::kError); |
| CloseOperationAndIssueNext(op); |
| return; |
| } |
| |
| if (entry_result.opened()) { |
| CollectStatistics(CacheEntryStatus::kUpdate); |
| } else { |
| CollectStatistics(CacheEntryStatus::kCreate); |
| } |
| |
| disk_cache::ScopedEntryPtr entry(entry_result.ReleaseEntry()); |
| // There should be a valid entry if the open was successful. |
| DCHECK(entry); |
| |
| // For merged entries, don't write if the entry already exists. |
| if (op->operation() == Operation::kWriteWithSHAKey) { |
| int small_size = entry->GetDataSize(kSmallDataStream); |
| int large_size = entry->GetDataSize(kLargeDataStream); |
| if (small_size == 0 && large_size == op->large_buffer()->size()) { |
| // Skip overwriting with identical data. |
| CloseOperationAndIssueNext(op); |
| return; |
| } |
| // Otherwise, there shouldn't be any data for this entry yet. |
| DCHECK_EQ(0, small_size); |
| DCHECK_EQ(0, large_size); |
| } |
| |
| // Write the small data first, truncating. |
| auto small_buffer = op->small_buffer(); |
| int result = entry->WriteData( |
| kSmallDataStream, 0, small_buffer.get(), small_buffer->size(), |
| base::BindOnce(&GeneratedCodeCache::WriteSmallBufferComplete, |
| weak_ptr_factory_.GetWeakPtr(), op), |
| true); |
| |
| if (result != net::ERR_IO_PENDING) { |
| WriteSmallBufferComplete(op, result); |
| } |
| |
| // Write the large data, truncating. |
| auto large_buffer = op->large_buffer(); |
| result = entry->WriteData( |
| kLargeDataStream, 0, large_buffer.get(), large_buffer->size(), |
| base::BindOnce(&GeneratedCodeCache::WriteLargeBufferComplete, |
| weak_ptr_factory_.GetWeakPtr(), op), |
| true); |
| |
| if (result != net::ERR_IO_PENDING) { |
| WriteLargeBufferComplete(op, result); |
| } |
| } |
| |
| void GeneratedCodeCache::WriteSmallBufferComplete(PendingOperation* op, |
| int rv) { |
| DCHECK(Operation::kWrite == op->operation() || |
| Operation::kWriteWithSHAKey == op->operation()); |
| if (op->AddBufferCompletion(rv == op->small_buffer()->size())) { |
| WriteComplete(op); |
| } |
| } |
| |
| void GeneratedCodeCache::WriteLargeBufferComplete(PendingOperation* op, |
| int rv) { |
| DCHECK(Operation::kWrite == op->operation() || |
| Operation::kWriteWithSHAKey == op->operation()); |
| if (op->AddBufferCompletion(rv == op->large_buffer()->size())) { |
| WriteComplete(op); |
| } |
| } |
| |
| void GeneratedCodeCache::WriteComplete(PendingOperation* op) { |
| DCHECK(Operation::kWrite == op->operation() || |
| Operation::kWriteWithSHAKey == op->operation()); |
| if (!op->succeeded()) { |
| // The write failed; record the failure and doom the entry here. |
| CollectStatistics(CacheEntryStatus::kWriteFailed); |
| DoomEntry(op); |
| } |
| CloseOperationAndIssueNext(op); |
| } |
| |
| void GeneratedCodeCache::FetchEntryImpl(PendingOperation* op) { |
| DCHECK(Operation::kFetch == op->operation() || |
| Operation::kFetchWithSHAKey == op->operation()); |
| if (backend_state_ != kInitialized) { |
| op->TakeReadCallback().Run(base::Time(), mojo_base::BigBuffer()); |
| CloseOperationAndIssueNext(op); |
| return; |
| } |
| |
| // This is a part of loading cycle and hence should run with a high priority. |
| disk_cache::EntryResult result = backend_->OpenEntry( |
| op->key(), net::HIGHEST, |
| base::BindOnce(&GeneratedCodeCache::OpenCompleteForRead, |
| weak_ptr_factory_.GetWeakPtr(), op)); |
| if (result.net_error() != net::ERR_IO_PENDING) { |
| OpenCompleteForRead(op, std::move(result)); |
| } |
| } |
| |
| void GeneratedCodeCache::OpenCompleteForRead( |
| PendingOperation* op, |
| disk_cache::EntryResult entry_result) { |
| DCHECK(Operation::kFetch == op->operation() || |
| Operation::kFetchWithSHAKey == op->operation()); |
| if (entry_result.net_error() != net::OK) { |
| CollectStatistics(CacheEntryStatus::kMiss); |
| op->TakeReadCallback().Run(base::Time(), mojo_base::BigBuffer()); |
| CloseOperationAndIssueNext(op); |
| return; |
| } |
| |
| disk_cache::ScopedEntryPtr entry(entry_result.ReleaseEntry()); |
| // There should be a valid entry if the open was successful. |
| DCHECK(entry); |
| |
| int small_size = entry->GetDataSize(kSmallDataStream); |
| int large_size = entry->GetDataSize(kLargeDataStream); |
| scoped_refptr<net::IOBufferWithSize> small_buffer; |
| scoped_refptr<BigIOBuffer> large_buffer; |
| if (op->operation() == Operation::kFetch) { |
| small_buffer = base::MakeRefCounted<net::IOBufferWithSize>(small_size); |
| op->set_small_buffer(small_buffer); |
| large_buffer = base::MakeRefCounted<BigIOBuffer>(large_size); |
| op->set_large_buffer(large_buffer); |
| } else { |
| small_buffer = op->small_buffer(); |
| large_buffer = op->large_buffer(); |
| DCHECK_EQ(small_size, small_buffer->size()); |
| DCHECK_EQ(large_size, large_buffer->size()); |
| } |
| |
| // Read the small data first. |
| int result = entry->ReadData( |
| kSmallDataStream, 0, small_buffer.get(), small_buffer->size(), |
| base::BindOnce(&GeneratedCodeCache::ReadSmallBufferComplete, |
| weak_ptr_factory_.GetWeakPtr(), op)); |
| |
| if (result != net::ERR_IO_PENDING) { |
| ReadSmallBufferComplete(op, result); |
| } |
| |
| // Skip the large read if data is in the small read. |
| if (large_size == 0) |
| return; |
| |
| // Read the large data. |
| result = entry->ReadData( |
| kLargeDataStream, 0, large_buffer.get(), large_buffer->size(), |
| base::BindOnce(&GeneratedCodeCache::ReadLargeBufferComplete, |
| weak_ptr_factory_.GetWeakPtr(), op)); |
| if (result != net::ERR_IO_PENDING) { |
| ReadLargeBufferComplete(op, result); |
| } |
| } |
| |
| void GeneratedCodeCache::ReadSmallBufferComplete(PendingOperation* op, int rv) { |
| DCHECK(Operation::kFetch == op->operation() || |
| Operation::kFetchWithSHAKey == op->operation()); |
| bool no_header = op->operation() == Operation::kFetchWithSHAKey; |
| bool succeeded = (rv == op->small_buffer()->size() && |
| (no_header || IsValidHeader(op->small_buffer()))); |
| CollectStatistics(succeeded ? CacheEntryStatus::kHit |
| : CacheEntryStatus::kMiss); |
| |
| if (op->AddBufferCompletion(succeeded)) |
| ReadComplete(op); |
| |
| // Small reads must finish now since no large read is pending. |
| if (op->large_buffer()->size() == 0) |
| ReadLargeBufferComplete(op, 0); |
| } |
| |
| void GeneratedCodeCache::ReadLargeBufferComplete(PendingOperation* op, int rv) { |
| DCHECK(Operation::kFetch == op->operation() || |
| Operation::kFetchWithSHAKey == op->operation()); |
| if (op->AddBufferCompletion(rv == op->large_buffer()->size())) |
| ReadComplete(op); |
| } |
| |
| void GeneratedCodeCache::ReadComplete(PendingOperation* op) { |
| DCHECK(Operation::kFetch == op->operation() || |
| Operation::kFetchWithSHAKey == op->operation()); |
| if (!op->succeeded()) { |
| op->TakeReadCallback().Run(base::Time(), mojo_base::BigBuffer()); |
| // Doom this entry since it is inaccessible. |
| DoomEntry(op); |
| } else { |
| if (op->operation() != Operation::kFetchWithSHAKey) { |
| base::Time response_time; |
| uint32_t data_size = 0; |
| ReadCommonDataHeader(op->small_buffer(), &response_time, &data_size); |
| if (data_size <= kInlineDataLimit) { |
| // Small data. Copy the data from the small buffer. |
| DCHECK_EQ(0, op->large_buffer()->size()); |
| mojo_base::BigBuffer data(data_size); |
| memcpy(data.data(), op->small_buffer()->data() + kHeaderSizeInBytes, |
| data_size); |
| op->TakeReadCallback().Run(response_time, std::move(data)); |
| } else if (!ShouldDeduplicateEntry(data_size)) { |
| // Large data below the merging threshold, or deduplication is disabled. |
| // Return the large buffer. |
| op->TakeReadCallback().Run(response_time, |
| op->large_buffer()->TakeBuffer()); |
| } else { |
| // Very large data. Create the second fetch using the checksum as key. |
| DCHECK_EQ(static_cast<int>(kHeaderSizeInBytes + kSHAKeySizeInBytes), |
| op->small_buffer()->size()); |
| std::string checksum_key( |
| op->small_buffer()->data() + kHeaderSizeInBytes, |
| kSHAKeySizeInBytes); |
| auto small_buffer = base::MakeRefCounted<net::IOBufferWithSize>(0); |
| auto large_buffer = base::MakeRefCounted<BigIOBuffer>(data_size); |
| auto op2 = std::make_unique<PendingOperation>( |
| Operation::kFetchWithSHAKey, checksum_key, response_time, |
| small_buffer, large_buffer, op->TakeReadCallback()); |
| EnqueueOperation(std::move(op2)); |
| } |
| } else { |
| // Large merged code data with no header. |op| holds the response time. |
| op->TakeReadCallback().Run(op->response_time(), |
| op->large_buffer()->TakeBuffer()); |
| } |
| } |
| CloseOperationAndIssueNext(op); |
| } |
| |
| void GeneratedCodeCache::DeleteEntryImpl(PendingOperation* op) { |
| DCHECK_EQ(Operation::kDelete, op->operation()); |
| DoomEntry(op); |
| CloseOperationAndIssueNext(op); |
| } |
| |
| void GeneratedCodeCache::DoomEntry(PendingOperation* op) { |
| // Write, Fetch, and Delete may all doom an entry. |
| DCHECK_NE(Operation::kGetBackend, op->operation()); |
| // Entries shouldn't be doomed if the backend hasn't been initialized. |
| DCHECK_EQ(kInitialized, backend_state_); |
| CollectStatistics(CacheEntryStatus::kClear); |
| backend_->DoomEntry(op->key(), net::LOWEST, net::CompletionOnceCallback()); |
| } |
| |
| void GeneratedCodeCache::IssueNextOperation(const std::string& key) { |
| auto it = active_entries_map_.find(key); |
| if (it == active_entries_map_.end()) |
| return; |
| |
| DCHECK(!it->second.empty()); |
| IssueOperation(it->second.front().get()); |
| } |
| |
| void GeneratedCodeCache::CloseOperationAndIssueNext(PendingOperation* op) { |
| // Dequeue op, keeping it alive long enough to issue another op. |
| std::unique_ptr<PendingOperation> keep_alive = DequeueOperation(op); |
| IssueNextOperation(op->key()); |
| } |
| |
| void GeneratedCodeCache::EnqueueOperationAndIssueIfNext( |
| std::unique_ptr<PendingOperation> op) { |
| // GetBackend ops have no key and shouldn't be enqueued here. |
| DCHECK_NE(Operation::kGetBackend, op->operation()); |
| auto it = active_entries_map_.find(op->key()); |
| bool can_issue = false; |
| if (it == active_entries_map_.end()) { |
| it = active_entries_map_.emplace(op->key(), PendingOperationQueue()).first; |
| can_issue = true; |
| } |
| const std::string& key = op->key(); |
| it->second.emplace(std::move(op)); |
| if (can_issue) |
| IssueNextOperation(key); |
| } |
| |
| std::unique_ptr<GeneratedCodeCache::PendingOperation> |
| GeneratedCodeCache::DequeueOperation(PendingOperation* op) { |
| auto it = active_entries_map_.find(op->key()); |
| DCHECK(it != active_entries_map_.end()); |
| DCHECK(!it->second.empty()); |
| std::unique_ptr<PendingOperation> result = std::move(it->second.front()); |
| // |op| should be at the front. |
| DCHECK_EQ(op, result.get()); |
| it->second.pop(); |
| // Delete the queue if it becomes empty. |
| if (it->second.empty()) { |
| active_entries_map_.erase(it); |
| } |
| return result; |
| } |
| |
| void GeneratedCodeCache::DoPendingGetBackend(PendingOperation* op) { |
| // |op| is kept alive in |IssuePendingOperations| for the duration of this |
| // call. We shouldn't access |op| after returning from this function. |
| DCHECK_EQ(kGetBackend, op->operation()); |
| if (backend_state_ == kInitialized) { |
| op->TakeBackendCallback().Run(backend_.get()); |
| } else { |
| DCHECK_EQ(backend_state_, kFailed); |
| op->TakeBackendCallback().Run(nullptr); |
| } |
| } |
| |
| bool GeneratedCodeCache::IsDeduplicationEnabled() const { |
| // Deduplication is disabled in the WebUI code cache, as an additional defense |
| // against privilege escalation in case there is a bug in the deduplication |
| // logic. |
| return cache_type_ != kWebUIJavaScript; |
| } |
| |
| bool GeneratedCodeCache::ShouldDeduplicateEntry(uint32_t data_size) const { |
| return data_size > kDedicatedDataLimit && IsDeduplicationEnabled(); |
| } |
| |
| void GeneratedCodeCache::SetLastUsedTimeForTest( |
| const GURL& resource_url, |
| const GURL& origin_lock, |
| const net::NetworkIsolationKey& nik, |
| base::Time time, |
| base::OnceClosure user_callback) { |
| // This is used only for tests. So reasonable to assume that backend is |
| // initialized here. All other operations handle the case when backend was not |
| // yet opened. |
| DCHECK_EQ(backend_state_, kInitialized); |
| auto split = base::SplitOnceCallback(std::move(user_callback)); |
| |
| disk_cache::EntryResultCallback callback = base::BindOnce( |
| &GeneratedCodeCache::OpenCompleteForSetLastUsedForTest, |
| weak_ptr_factory_.GetWeakPtr(), time, std::move(split.first)); |
| |
| std::string key = GetCacheKey(resource_url, origin_lock, nik, cache_type_); |
| disk_cache::EntryResult result = |
| backend_->OpenEntry(key, net::LOWEST, std::move(callback)); |
| if (result.net_error() != net::ERR_IO_PENDING) { |
| OpenCompleteForSetLastUsedForTest(time, std::move(split.second), |
| std::move(result)); |
| } |
| } |
| |
| void GeneratedCodeCache::OpenCompleteForSetLastUsedForTest( |
| base::Time time, |
| base::OnceClosure callback, |
| disk_cache::EntryResult result) { |
| DCHECK_EQ(result.net_error(), net::OK); |
| { |
| disk_cache::ScopedEntryPtr disk_entry(result.ReleaseEntry()); |
| DCHECK(disk_entry); |
| disk_entry->SetLastUsedTimeForTest(time); |
| } |
| std::move(callback).Run(); |
| } |
| |
| } // namespace content |