[6] LoadImageForDecoding MESSAGE_CHECK tripped on legitimate embedder calls
Low — the failure mode is a spurious process kill, not a policy bypass; the cookie validator was unconditional for every value that reached it. What keeps it above "not a bug" is that ordinary embedder API usage triggered it, and the repair relocates where cookie authority is derived.
WebKit's MESSAGE_CHECK family encodes a specific claim: the checked field is an invariant the sending process is responsible for maintaining, so a violation means the sender is compromised and should be killed. That claim only holds when the sender actually authors the value. The _loadAndDecodeImage: SPI carries an app-supplied NSURLRequest from the UI process, through the WebContent process, into the Networking process, with the application's own mainDocumentURL still attached — and that field arrives at a fail-stop validator in a process that never saw who set it.
The angle: for defensive value — an app calling _loadAndDecodeImage: before any navigation, or with its own mainDocumentURL, had its WebContent process terminated; the audit lesson is which fields belong behind a MESSAGE_CHECK at all.
The commit message enumerates the two surviving failure modes from an earlier incomplete fix:
There are two cases where the
MESSAGE_CHECKinNetworkConnectionToWebProcess::loadImageForDecodingcan still fail after the fix in rdar://176310702: a client set an incorrect or emptymainDocumentURL; a client called_loadAndDecodeImage:on aWKWebViewthat has not loaded any content. SetfirstPartyForCookiesin the web process frompage->mainFrameURL(), which is the authoritative value, and drop theWKWebView-side fallback added in the previous fix. WhenmainFrameURLis empty (no navigation has happened yet, so there is no cookie context), disable cookies for the request instead of failing theMESSAGE_CHECK.
Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
Source/WebKit/WebProcess/WebPage/WebPage.cpp
Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm
Tools/TestWebKitAPI/Tests/WebKit/WKWebView/LoadAndDecodeImage.mm
Patch Details
The change spans three layers — it moves cookie-context derivation into the web process, relaxes the network-process validator for the unset case, and deletes the now-redundant UI-process fallback.
In WebPage.cpp, WebPage::loadAndDecodeImage now resolves corePage() first (bailing out via decodeError(url) if there is no page) and unconditionally overwrites the incoming request's cookie context with request.setFirstPartyForCookies(page->mainFrameURL()) before forwarding the IPC — so the value reaching the network process is derived in-process from the page's own main-frame URL rather than from whatever mainDocumentURL the API client attached.
In NetworkConnectionToWebProcess.cpp, the previously unconditional allowsFirstPartyForCookies MESSAGE_CHECK_COMPLETION is now gated on request.firstPartyForCookies().isValid(); when the first-party URL is not valid the validator is skipped and request.setAllowCookies(false) is applied instead of terminating the sender. The URL-validity/HTTP-family check is untouched.
In WKWebView.mm, -_loadAndDecodeImage:... drops the UI-process fallback added by the previous fix (the NSMutableURLRequest copy that set mainDocumentURL to self.URL). Both new tests assert the image decodes and, critically, EXPECT_FALSE(processTerminated).
Applying a fail-stop compromise-detection validator to a field that originates outside the validated trust boundary and has a legitimate unset state.
Background
Process split. WKWebKit runs the embedding app and UI in one process, page content in a sandboxed WebContent process, and all networking in a separate Networking process. The Networking process treats WebContent as untrusted and validates its messages.
firstPartyForCookies.
A field on WebCore::ResourceRequest holding the URL of the top-level document a subresource load belongs to; the network stack uses it as the cookie/partitioning context for the request. On Cocoa it maps to NSURLRequest.mainDocumentURL.
MESSAGE_CHECK_COMPLETION.
WebKit's IPC validation macro. When the asserted condition is false it runs the supplied completion handler and then treats the incoming message as invalid, which terminates the connection to the sending process. In NetworkConnectionToWebProcess.cpp it is bound to this->connection(). Its design intent is compromise detection, not input sanitation — which is what makes the choice of what to put behind it a design decision rather than a formality.
NetworkProcess::allowsFirstPartyForCookies(processIdentifier, url).
The network process's registry check answering "has this web process been told it may load with this first-party cookie context?", returning a NetworkProcess::AllowCookieAccess value (Allow being the passing one). The UI process registers permitted first parties per web process as navigations happen.
Page::mainFrameURL() and URL::isValid().
mainFrameURL() is the WebCore-side URL of the page's main frame — the top-level document currently loaded in that WebPage; it is empty when no navigation has occurred. URL::isValid() is true when the URL parsed into a well-formed URL; an empty or unparseable string yields an invalid URL.
ResourceRequest::setAllowCookies(false).
Marks a request so the platform network layer does not attach or store cookies for it (on Cocoa, HTTPShouldHandleCookies off).
-[WKWebView _loadAndDecodeImage:...].
A private WKWebView API for embedding applications that fetches an image URL through the web page's network context and returns a decoded platform image. It runs UI process -> WebPage::loadAndDecodeImage (WebContent) -> NetworkConnectionToWebProcess::loadImageForDecoding (Networking), so the request object crosses two process boundaries with the app's fields still attached.
Analysis
This is an IPC validator misclassification — a policy error causing spurious process termination, not memory corruption.
Pre-fix Post-fix
App: NSURLRequest.mainDocumentURL App: mainDocumentURL (ignored)
│ (may be nil / unrelated origin) │
▼ UI process ▼ UI process
WKWebView: backfill from self.URL WKWebView: pass through
│ (nil if never navigated) │
▼ WebContent ▼ WebContent
forward request verbatim setFirstPartyForCookies(mainFrameURL)
│ │
▼ Networking ▼ Networking
MESSAGE_CHECK(allowsFirstParty…) isValid() ? MESSAGE_CHECK(…)
└─► FAIL ► kill WebContent : setAllowCookies(false)
WebPage::loadAndDecodeImage forwarded the ResourceRequest essentially verbatim, so firstPartyForCookies was whatever the embedding application had put in NSURLRequest.mainDocumentURL (with a UI-process fallback to self.URL from the earlier fix). On the receiving side, loadImageForDecoding treated that field as an attested value and ran it through a validator whose failure semantics are "the sender is malicious, kill it."
The violated invariant is the one every MESSAGE_CHECK implicitly asserts: the checked field is something the sending process is responsible for maintaining, so a violation is evidence of compromise. Here the field was not sender-controlled in that sense — it flowed in from outside the WebContent trust boundary, and it had a legitimate "no value yet" state. The commit message enumerates the two surviving failure modes exactly, and both became tests. In the first case, allowsFirstPartyForCookies returns something other than Allow because http://unrelated-domain.example/ was never registered for that web process identifier. In the second, there is no valid URL to check at all — the LoadAndDecodeImageBeforeAnyNavigation test's EXPECT_NULL([webView URL]) pins the state where the UI-process fallback self.URL is nil and firstPartyForCookies ends up empty. (The crash-report signature in the bug title and the attribution to rdar://176310702 come from the commit message; neither the prior commit nor the crash record is part of the supplied context.)
The fix splits the two concerns. The authoritative cookie context is now computed where the authority actually lives — page->mainFrameURL() inside the web process — so the network-process validator is once again checking a value the sender is responsible for. The "no cookie context exists yet" case is handled by fail-closed degradation rather than by the fail-stop validator, which is the correct disposition for a legitimate state rather than a compromise signal.
This change relaxes an IPC validator, so the surface question is worth stating explicitly. NetworkConnectionToWebProcess::loadImageForDecoding now has a path that reaches networkSession() and issues a real network load without having consulted allowsFirstPartyForCookies at all — previously every reaching request had passed that check. Any WebContent process, including a compromised one, can select this path simply by sending a firstPartyForCookies that fails URL::isValid(). What is assumed rather than enforced is that setAllowCookies(false) is honored end-to-end for this load, and that every other consumer of firstPartyForCookies downstream behaves safely when the field is empty: the field keys more than cookie attachment in WebKit — it feeds cache partitioning, same-site determination and tracking-prevention decisions — and the new branch only neutralizes the cookie dimension. If those assumptions break, the class of bug that would become possible is partition confusion rather than memory corruption: a load attributed to an empty first-party key could share a cache or storage partition with unrelated first parties. The commit does not show the downstream handling of setAllowCookies(false) for the image-decode path, so this is a surface to audit rather than an identified defect.
On exploitability, there is no attacker gain to extract. Reachability from web content is absent: WebPage::loadAndDecodeImage is driven by the _loadAndDecodeImage: SPI, not by any JavaScript, HTML, or CSS-reachable binding, so page script cannot originate the LoadImageForDecoding message through supported paths — layout tests reach it only through the IPC_TESTING_API harness, which is not compiled into shipping builds. The realistic pre-fix trigger is an embedder calling the SPI before any navigation, or with mainDocumentURL set to a host the UI process never registered, in both cases tripping the validator and tearing down the WebContent process. The one actor who can reach the vulnerable branch deliberately is an already-compromised WebContent process, and the only outcome available to it is its own termination, which is not an attacker transition.
The trust boundary at stake is WebContent -> Networking IPC, and the security model assumption is that MESSAGE_CHECK failures mean the sender violated an invariant it was responsible for. Before the fix that assumption was false for firstPartyForCookies on this message, so ordinary application behavior was misread as compromise and terminated the WebContent process. The concrete pre-fix consequence is availability loss attributable to a legitimate embedder API call, not cross-origin data access: the cookie-isolation policy itself was never bypassed, because the validator was unconditional for every value that reached it. The residual model change worth tracking is that the fix's fail-closed branch moves cookie safety for invalid first-party values from validated to enforced-by-flag, a different and weaker form of assurance than the validator provided.
Insight
The durable lesson is about what belongs behind a MESSAGE_CHECK at all. These macros encode "a violation here means the peer is compromised," so the checked value must be one the peer is responsible for producing correctly. This message failed that test in two ways at once: the value originated with the embedding application, outside the WebContent trust boundary entirely, and it had a legitimate unset state. The fix is the textbook repair — move authority to where it exists, keep the fail-stop validator for the case where a compromised sender lies about a value it owns, and give the legitimate-unset case a fail-closed data path instead of a fail-stop one. Note also that this is the second attempt: the earlier fix patched the symptom at the UI-process layer by backfilling mainDocumentURL from self.URL, which cannot cover a WKWebView that has never navigated. Layering a default at the wrong process boundary narrows a validator's false-positive window without eliminating it; this commit deletes that fallback and relocates the derivation.
Audit directions
-
Fail-stop IPC validators on fields that entered the sending process from outside the validated boundary. The invariant is a
MESSAGE_CHECKmay only assert properties the sender itself is responsible for producing; pass-through data from the embedder or from another process is input to sanitize, not evidence of compromise. Narrow: grepSource/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cppand the*MessageReceiver.cppfiles forMESSAGE_CHECK*whose asserted expression reads aResourceRequestfield (firstPartyForCookies,httpReferrer,httpOrigin,isSameSite,siteForCookies) — match tell: trace the field back to its origin and if it is set from anNSURLRequest/API::URLRequesthanded in by an app SPI rather than computed in WebCore, it is a candidate. Wider: the same class appears wherever a validator sits on a value routed through an intermediate process — UI-process-supplied policy fields arriving viaWebPageProxyand re-sent byWebPage, injected-bundle-supplied request mutations, andWKURLSchemeHandlerresponses re-entering the loader; match tell in code-search results is any check on a struct field whose setter is reachable from public/private API rather than only from internal navigation code. Widest: the principle is "authentication of a value must be performed at the boundary where the value is authored, not at a downstream boundary that merely relayed it" — the same shape as validating a JWT claim a proxy copied from a client header, or Chromium Mojo'sReportBadMessagebeing called on data the renderer forwarded from an extension; the audit question to carry anywhere is "if this check fails, is the process I am about to kill actually the one that authored the value?" -
Validators with no legitimate representation of "not yet initialized." A valid uninitialized state becomes indistinguishable from an attack. The invariant is any check on a value with a lifecycle must define behavior for the pre-initialization window, and that behavior must be fail-closed data handling rather than fail-stop termination. Narrow: audit the other
NetworkConnectionToWebProcessandNetworkResourceLoaderentry points reachable before a first navigation completes — verify each either tolerates an emptyfirstPartyForCookies/mainFrameURLor is unreachable in that window; match tell is aMESSAGE_CHECKon a URL, origin, or identifier that a freshly createdWebPagecannot yet have. Wider: the same shape recurs for any state established by a lifecycle event —WebPageProxyhandlers that assume a committed provisional load,WebProcessProxyregistration tables consulted beforedidCommitLoadForFrame, service-worker and shared-worker connection setup racing page creation; match tell is a lookup into a per-process or per-page registry whose population happens in a load-commit callback. Widest: this is the general "empty state conflated with invalid state" class, applicable to any system that validates against a registry filled asynchronously — OAuth scope tables checked before consent completes, RBAC checks against a not-yet-synced role cache, capability tables in microkernels; the invariant to carry is "absent is not the same as forbidden, and only one of the two justifies terminating the caller." -
Relaxing a validator and compensating with a single neutralizing flag. The flag may cover fewer downstream consumers than the check did. The invariant is when a check is relaxed and compensated by a mitigation flag, the flag must dominate every consumer the check previously protected. Narrow: trace
ResourceRequest::setAllowCookies(false)from the newelsebranch inloadImageForDecodingthroughNetworkSessionCocoa::loadImageForDecodingand confirm every downstream reader offirstPartyForCookieson this path — cookie attachment, cache-partition key derivation, same-site computation, tracking-prevention classification — behaves correctly with an empty first party; match tell is any downstream call site that readsfirstPartyForCookieswithout first consultingallowCookies(). Wider: apply the same audit to other "skip the check, set a safety flag instead" relaxations across WebKit's loading code —setAllowCookies, credential-suppression flags, and sandbox-flag-driven early-outs; match tell in code-search results is a conditional validator (if (x.isValid()) MESSAGE_CHECK(...)orif (!foo) { /* mitigate */ }) where the mitigation and the check protect nominally different resources. Widest: the reusable question for any codebase that softens a hard reject into a degraded-mode allow is "enumerate every consumer the reject used to protect, then prove the degraded mode neutralizes all of them, not just the one that motivated the change" — this applies equally to feature-flagged auth bypasses, HTTP downgrade-to-anonymous fallbacks, and TLS verification relaxations paired with a "do not send credentials" flag.