CVE-2024-8389
Overview
Background
- Sandbox
- an isolated JavaScript global (a
JSObjectcreated byxpc::CreateSandboxObject) used by privileged XPConnect code to run script with a controlled set of capabilities. - Sandbox metadata
- arbitrary caller-supplied data attached to a sandbox global via
xpc::SetSandboxMetadataand later retrieved withxpc::GetSandboxMetadata. - `nsresult`
- Mozilla’s status code type; a value satisfying
NS_FAILEDsignals that an operation did not complete successfully. - `[[nodiscard]]`
- a C++ attribute that makes the compiler warn when a function’s return value is ignored, used to enforce that error codes are checked.
Root Cause Analysis
In xpc::CreateSandboxObject, the call to xpc::SetSandboxMetadata returned an nsresult that was silently discarded, so the function proceeded to JS_FireOnNewGlobalObject and returned a fully-constructed sandbox even when metadata initialization had failed. Setting metadata can fail — for example when a pending JS exception or an out-of-memory / conversion error leaves the JSContext in an error state — and continuing past that point violates the invariant that a sandbox is only handed back to callers once all of its initialization steps succeeded and the context is clean. Firing the new-global-object hook and running further script against a half-initialized global, while an exception may already be pending on cx, is what leads to the memory-unsafe state exercised by the crashtest.
The fix captures the nsresult from xpc::SetSandboxMetadata, and on NS_FAILED returns early before the global is finalized and exposed, so no code operates on an inconsistently initialized sandbox. Marking xpc::SetSandboxMetadata [[nodiscard]] guarantees the compiler will flag any future caller that repeats the mistake of ignoring the result.
xpc::SetSandboxMetadata, allowing sandbox construction to continue after a failed, exception-leaving step; the fix propagates that nsresult with an early return and enforces the check permanently via [[nodiscard]].Attack Path
- Trigger sandbox creation
Cause privileged code to enter
xpc::CreateSandboxObject, which reaches the metadata-initialization step. - Force metadata failure
Arrange for
xpc::SetSandboxMetadatato fail (e.g. by driving theJSContextinto an error/exception or OOM state during the metadata conversion), leaving a pending exception oncx. - Continue past the error
Because the returned
nsresultwas discarded, execution falls through toJSAutoRealmandJS_FireOnNewGlobalObjecton the incompletely initialized global. - Run script on the broken global
The event and DOM manipulation in the crashtest (
dispatchEventondocument.doctype, thendocument.write("<audio>")) drives further work against the inconsistent state, producing memory corruption. - Escalate With controlled heap/state manipulation, the resulting memory corruption is potentially shaped into arbitrary code execution.
Impact Assessment
xpc::SetSandboxMetadata to fail at the right moment and then interacting with the partially-constructed global, as demonstrated by the 1907230.html crashtest. The impact is rated high given the privileged process context and the code-execution potential.Files Changed
js/xpconnect/crashtests/1907230.htmljs/xpconnect/crashtests/crashtests.listjs/xpconnect/src/Sandbox.cppjs/xpconnect/src/xpcprivate.h
Audit Directions
- Discarded `nsresult` at initialization boundariesLook for other object/global construction paths that call fallible helpers without checking the returned
nsresultbefore firing hooks or returning the object. - Missing `[[nodiscard]]` on fallible APIsAudit XPConnect and SpiderMonkey helper declarations that return
nsresultorboolstatus but lack[[nodiscard]], since these invite silent error dropping. - Continuation with a pending exceptionSearch for code that proceeds to run script (
JS_FireOnNewGlobalObject, callbacks, DOM dispatch) after a step that can leave a pending exception on theJSContextwithout clearing or propagating it.
Patch
diff --git a/js/xpconnect/src/Sandbox.cpp b/js/xpconnect/src/Sandbox.cpp
index 0af438d7ce4..3dc7866929e 100644
--- a/js/xpconnect/src/Sandbox.cpp
+++ b/js/xpconnect/src/Sandbox.cpp
@@ -1500,7 +1500,10 @@ nsresult xpc::CreateSandboxObject(JSContext* cx, MutableHandleValue vp,
// about:memory may use that information
xpc::SetLocationForGlobal(sandbox, options.sandboxName);
- xpc::SetSandboxMetadata(cx, sandbox, options.metadata);
+ nsresult rv = xpc::SetSandboxMetadata(cx, sandbox, options.metadata);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
JSAutoRealm ar(cx, sandbox);
JS_FireOnNewGlobalObject(cx, sandbox);
diff --git a/js/xpconnect/src/xpcprivate.h b/js/xpconnect/src/xpcprivate.h
index 38d7516c361..ce7b60b3a72 100644
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -2442,8 +2442,9 @@ nsresult EvalInSandbox(JSContext* cx, JS::HandleObject sandbox,
nsresult GetSandboxMetadata(JSContext* cx, JS::HandleObject sandboxArg,
JS::MutableHandleValue rval);
-nsresult SetSandboxMetadata(JSContext* cx, JS::HandleObject sandboxArg,
- JS::HandleValue metadata);
+[[nodiscard]] nsresult SetSandboxMetadata(JSContext* cx,
+ JS::HandleObject sandboxArg,
+ JS::HandleValue metadata);
bool CreateObjectIn(JSContext* cx, JS::HandleValue vobj,
CreateObjectInOptions& options,
diff --git a/js/xpconnect/crashtests/1907230.html b/js/xpconnect/crashtests/1907230.html
new file mode 100644
index 00000000000..9a72f688826
--- /dev/null
+++ b/js/xpconnect/crashtests/1907230.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<script>
+window.addEventListener("load", () => {
+ let a = new UIEvent("load", {"bubbles": true})
+ try { document.doctype.dispatchEvent(a) } catch (e) {}
+ document.write("<audio>")
+})
+</script>
diff --git a/js/xpconnect/crashtests/crashtests.list b/js/xpconnect/crashtests/crashtests.list
index 129c34db277..6cf1bf4e435 100644
--- a/js/xpconnect/crashtests/crashtests.list
+++ b/js/xpconnect/crashtests/crashtests.list
@@ -53,3 +53,4 @@ pref(dom.use_xbl_scopes_for_remote_xul,true) load 898939.html
pref(security.fileuri.strict_origin_policy,false) load 938297.html
load 977538.html
load 1577573.html
+load 1907230.html
Regression Test / PoC
diff --git a/js/xpconnect/crashtests/1907230.html b/js/xpconnect/crashtests/1907230.html
new file mode 100644
index 00000000000..9a72f688826
--- /dev/null
+++ b/js/xpconnect/crashtests/1907230.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<script>
+window.addEventListener("load", () => {
+ let a = new UIEvent("load", {"bubbles": true})
+ try { document.doctype.dispatchEvent(a) } catch (e) {}
+ document.write("<audio>")
+})
+</script>
diff --git a/js/xpconnect/crashtests/crashtests.list b/js/xpconnect/crashtests/crashtests.list
index 129c34db277..6cf1bf4e435 100644
--- a/js/xpconnect/crashtests/crashtests.list
+++ b/js/xpconnect/crashtests/crashtests.list
@@ -53,3 +53,4 @@ pref(dom.use_xbl_scopes_for_remote_xul,true) load 898939.html
pref(security.fileuri.strict_origin_policy,false) load 938297.html
load 977538.html
load 1577573.html
+load 1907230.html