CVE-2026-117971
Overview
Background
- `CanonicalBrowsingContext::PrintWithNoContentAnalysis`
- Parent-process method (
docshell/base/CanonicalBrowsingContext.cpp) that initiates printing for a browsing context, returning aRefPtr<PrintPromise>; it constructs a remote print job and wires up a listener. - `RemotePrintJobParent`
- The parent-side IPDL actor for a remote (content-process) print job, constructed with
PrintSettingsand bound to the protocol viaSendPRemotePrintJobConstructor. - `SendPRemotePrintJobConstructor(actor)`
- The IPDL “constructor send” that binds the parent actor to the protocol and returns the actor pointer on success, or a null/failure indication if the message cannot be sent (e.g. the content process/manager is gone). On failure the actor is not successfully managed.
- `MakeRefPtr` / reference-counted actors
MakeRefPtr<T>(...)creates an object already owned by aRefPtr. The patch switches the actor from a rawnewtoMakeRefPtr, giving the local code a strong reference independent of IPDL’s management.- `RegisterListener`
- Called on the
remotePrintJobafter construction to attach the print listener; dereferences the actor.
Root Cause Analysis
Pre-fix, PrintWithNoContentAnalysis created the actor with a raw new:
layout::RemotePrintJobParent* remotePrintJob = new layout::RemotePrintJobParent(printSettings);
printData.remotePrintJob() = browserParent->Manager()->SendPRemotePrintJobConstructor(remotePrintJob);
remotePrintJob->RegisterListener(listener);
The method then unconditionally dereferenced remotePrintJob->RegisterListener(listener) regardless of whether the constructor send succeeded. When SendPRemotePrintJobConstructor fails — which can happen if the managing content process/actor (the browserParent->Manager()) has gone away — the IPDL machinery tears down and frees the just-passed actor, leaving remotePrintJob dangling. The subsequent RegisterListener call then operates on freed memory: a use-after-free of the RemotePrintJobParent in the parent process. The patch fixes this two ways. First, it constructs the actor with auto remotePrintJob = MakeRefPtr<layout::RemotePrintJobParent>(printSettings);, so local code holds a strong reference and the object is not freed out from under the subsequent dereference. Second, it checks the constructor result and bails out before using the actor: if (NS_WARN_IF(!printData.remotePrintJob())) { promise->Reject(NS_ERROR_FAILURE, __func__); return promise; }. Together these ensure RegisterListener is only reached when the actor is live and strongly held. The advisory describes a “sandbox escape due to use-after-free in Print Preview”; the diff substantiates the use-after-free (raw actor used after a possibly-failed constructor send, now RefPtr + failure check). The sandbox-escape framing is the escalation (a content process that can force the constructor to fail provokes a UAF in the parent), which is plausible but not directly shown by the diff — inferred.
PrintWithNoContentAnalysis dereferenced a raw RemotePrintJobParent with RegisterListener even when SendPRemotePrintJobConstructor failed and freed the actor, a parent-process use-after-free; the fix holds the actor in a RefPtr via MakeRefPtr and returns early when the constructor send fails.Attack Path
- Trigger remote printing
A content process initiates a print that reaches
CanonicalBrowsingContext::PrintWithNoContentAnalysisin the parent (print preview / printing path; web-triggerability of printing is inferred). - Cause the constructor send to fail
Arrange for the managing content-process actor to be gone or the IPDL send to fail at the moment
SendPRemotePrintJobConstructoris invoked (e.g. racing process teardown), so the just-passed actor is freed. - Dangling actor
Pre-fix,
remotePrintJobnow points at freed memory while the code continues. - Use freed actor
remotePrintJob->RegisterListener(listener)dereferences and mutates the freedRemotePrintJobParent. - Groom the freed allocation With control over parent-process heap timing, the freed slot could be reclaimed with attacker-influenced data (inferred).
- Primitive achieved Use-after-free of a parent-process IPDL actor, the basis for a sandbox escape (escalation inferred, not shown by the diff).
Impact Assessment
RemotePrintJobParent actor, driven from the printing path. Because the parent is the sandbox boundary, a content process that can force the constructor send to fail and then win the heap-grooming race would corrupt parent-process memory — hence Mozilla’s “sandbox escape due to use-after-free” label. The diff proves the UAF (raw actor dereferenced after a failed/freed constructor send) and the lifetime fix; it does not demonstrate the full escape, so the escalation to code execution/sandbox escape is inferred. Mozilla rates it moderate.Files Changed
docshell/base/CanonicalBrowsingContext.cpp
Audit Directions
- Raw actors after constructor sendsAudit other
SendP*Constructorcall sites that store the actor in a raw pointer and dereference it without checking the send result; these are the same UAF pattern. - Printing/IPDL lifetimesReview
RemotePrintJobParentand related print IPDL actors for other spots assuming successful construction before use. - MakeRefPtr migrationLook for remaining
new SomeActor(...)immediately followed by aSendP...Constructorin the parent where failure leaves a dangling raw pointer; preferMakeRefPtrplus a result check.
Patch
diff --git a/docshell/base/CanonicalBrowsingContext.cpp b/docshell/base/CanonicalBrowsingContext.cpp
index aa25126b9a9..a2fef661169 100644
--- a/docshell/base/CanonicalBrowsingContext.cpp
+++ b/docshell/base/CanonicalBrowsingContext.cpp
@@ -1119,10 +1119,13 @@ RefPtr<PrintPromise> CanonicalBrowsingContext::PrintWithNoContentAnalysis(
return promise;
}
- layout::RemotePrintJobParent* remotePrintJob =
- new layout::RemotePrintJobParent(printSettings);
+ auto remotePrintJob = MakeRefPtr<layout::RemotePrintJobParent>(printSettings);
printData.remotePrintJob() =
browserParent->Manager()->SendPRemotePrintJobConstructor(remotePrintJob);
+ if (NS_WARN_IF(!printData.remotePrintJob())) {
+ promise->Reject(NS_ERROR_FAILURE, __func__);
+ return promise;
+ }
remotePrintJob->RegisterListener(listener);