From cba5248898277117bbfa100f6a136128f575dd3b Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:47:31 +0900 Subject: [PATCH] docs: document preload code cache threat model (#54654) Explain why SetFromRenderer stores the renderer-produced blob unverified and that the (BrowserContext, ProcessLock, id) scope is the security boundary. Claude-Session: https://claude.ai/code/session_018A4Mt59Nr4h4WSFjyNNkxb Co-authored-by: Claude --- shell/browser/preload_code_cache.cc | 15 +++++++++++++++ shell/browser/preload_code_cache.h | 4 +++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/shell/browser/preload_code_cache.cc b/shell/browser/preload_code_cache.cc index 518c2bcc88..27e7bc7096 100644 --- a/shell/browser/preload_code_cache.cc +++ b/shell/browser/preload_code_cache.cc @@ -254,6 +254,21 @@ void SetFromRenderer(content::RenderFrameHost* rfh, // first launch) just writes identical bytes twice; harmless and rare, and // SetFromRenderer() is UI-thread serialized so it's not a data race. Entry entry; + // Security note: |blob| is renderer-produced and stored without + // verification. This is intentional and matches Chromium's + // GeneratedCodeCache (content/browser/renderer_host/code_cache_host_impl.cc). + // The served-preload and source hash checks above only decide whether this + // write is accepted and which source hash it is filed under. They do not, + // and cannot, prove |blob| was compiled from that source (V8's CachedData + // source check is length-only). Containment comes from the key instead: + // entries are scoped to (BrowserContext, ProcessLock, preload id), with the + // lock taken from the browser-side RenderProcessHost (see ScopeForFrame()), + // so a write is only ever served back to frames in the same BrowserContext + // whose process has an equal ProcessLock. A compromised renderer can + // therefore only replace bytecode for its own principal's sandboxed preload, + // whose capabilities it already holds. Persistence across restarts is the + // accepted cost, as in Chromium. Serving an entry across a different + // BrowserContext or ProcessLock WOULD be a security bug. entry.source_hash = hash_it->second; entry.blob.assign(blob.begin(), blob.end()); diff --git a/shell/browser/preload_code_cache.h b/shell/browser/preload_code_cache.h index 32036d6738..fe6f492338 100644 --- a/shell/browser/preload_code_cache.h +++ b/shell/browser/preload_code_cache.h @@ -35,7 +35,9 @@ std::string IdForWebPreferencesPreload(const base::FilePath& path); // origin keying, sandboxing, guest and cross-origin-isolation status, // storage partition). Entries are only ever served back within an equal // scope, so a blob produced by one principal's renderer is never handed to -// another. |dir| is empty for off-the-record contexts (memory tier only). +// another. Scope is the security boundary for cached blobs; see the note in +// SetFromRenderer(). |dir| is empty for off-the-record contexts (memory tier +// only). struct Scope { Scope(); Scope(const Scope&);