MOZ_CRASH(Invalid object. Dead wrapper?) in JSObject::maybeUnwrapAs<SavedFrame> via shell PrintUnhandledRejection after resolution-site wrapper is nuked
Categories
(Core :: JavaScript Engine, defect, P3)
Tracking
()
People
(Reporter: bugmon, Unassigned)
References
(Blocks 1 open bug)
Details
(Keywords: crash, testcase)
Attachments
(3 files)
|
973 bytes,
patch
|
Details | Diff | Splinter Review | |
|
3.25 KB,
text/plain
|
Details | |
|
186 bytes,
text/plain
|
Details |
PrintUnhandledRejection in js/src/shell/js.cpp reads promise->resolutionSite() into a rooted site before calling JS_ValueToSource on the rejection reason, and uses site in JS::BuildStackString afterwards without re-checking it. PromiseObject::resolutionSite() filters dead wrappers only at the time of that read. JS_ValueToSource invokes a script-defined toSource, which can nuke the cross-compartment wrapper held in site; BuildStackString then passes the resulting dead proxy to js::UnwrapSavedFrame, and JSObject::maybeUnwrapAs<SavedFrame>() reaches MOZ_CRASH("Invalid object. Dead wrapper?").
A script author running code in the SpiderMonkey shell can abort the shell process with a release-mode MOZ_CRASH by leaving an unhandled rejection whose stack was captured in another compartment and whose reason's toSource calls nukeAllCCWs(). The demonstrated effect is a controlled abort at shell exit, not memory corruption. The faulty caller is JS shell code and the trigger relies on the shell-only nukeAllCCWs() testing function; no path from web content was demonstrated, and the Gecko callers of JS::BuildStackString were not examined.
Build Info
- Branch: main
- Revision: b4b9efb6ee130740a94caf46ffff0625af1d51db
- Timestamp: 2026-10-04T19:31:16+00:00
Affected Code
File: js/src/shell/js.cpp, line 12428-12455
[[nodiscard]] static bool PrintUnhandledRejection(
JSContext* cx, Handle<PromiseObject*> promise) {
RootedValue reason(cx, promise->reason());
RootedObject site(cx, promise->resolutionSite());
RootedString str(cx, JS_ValueToSource(cx, reason));
...
if (!site) {
fputs("(no stack trace available)\n", stderr);
return true;
}
JSPrincipals* principals = cx->realm()->principals();
RootedString stackStr(cx);
if (!BuildStackString(cx, principals, site, &stackStr, 2)) {
return false;
}
File: js/src/builtin/Promise.cpp, line 656-666
JSObject* PromiseObject::resolutionSite() {
auto debugInfo = PromiseDebugInfo::FromPromise(this);
if (debugInfo) {
JSObject* site = debugInfo->resolutionSite();
if (site && !JS_IsDeadWrapper(site)) {
MOZ_ASSERT(UncheckedUnwrap(site)->is<SavedFrame>());
return site;
}
}
return nullptr;
}
File: js/src/builtin/Promise.cpp, line 601-616
RootedObject stack(cx, unwrappedRejectionStack);
if (stack) {
// The exception stack is always unwrapped so it might be in
// a different compartment.
if (!cx->compartment()->wrap(cx, &stack)) {
cx->clearPendingException();
return;
}
} else {
...
}
debugInfo->setFixedSlot(Slot_ResolutionSite, ObjectOrNullValue(stack));
File: js/src/vm/SavedStacks.cpp, line 753-767
js::SavedFrame* js::UnwrapSavedFrame(JSContext* cx, JSPrincipals* principals,
HandleObject obj,
JS::SavedFrameSelfHosted selfHosted,
bool& skippedAsync) {
if (!obj) {
return nullptr;
}
Rooted<SavedFrame*> frame(cx, obj->maybeUnwrapAs<SavedFrame>());
if (!frame) {
return nullptr;
}
return GetFirstSubsumedFrame(cx, principals, frame, selfHosted, skippedAsync);
}
File: js/src/vm/JSObject.h, line 681-700
template <class T>
inline T* JSObject::maybeUnwrapAs() {
...
if (is<T>()) {
return &as<T>();
}
JSObject* unwrapped = js::CheckedUnwrapStatic(this);
if (!unwrapped) {
return nullptr;
}
if (MOZ_LIKELY(unwrapped->is<T>())) {
return &unwrapped->as<T>();
}
MOZ_CRASH("Invalid object. Dead wrapper?");
}
File: js/src/shell/js.cpp, line 7688-7701
static bool NukeAllCCWs(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
...
NukeCrossCompartmentWrappers(cx, AllCompartments(), cx->realm(),
NukeWindowReferences, NukeAllReferences);
args.rval().setUndefined();
return true;
}
The shell reporter fetches the resolution site (where dead wrappers are filtered) before a call that runs script, then uses it after. The site is a cross-compartment wrapper because setResolutionInfo wraps the unwrapped exception stack into the promise's compartment. maybeUnwrapAs<T> has no non-crashing path for an object that unwraps to something other than T.
Exploit Chain
- Create a second global in a separate compartment (newGlobal({newCompartment: true})) and define a function there that throws its argument, so the exception stack (a SavedFrame) is captured in that compartment.
- Call that function from an async function in the main compartment with an object
oas the thrown value; the async function's promise is rejected and PromiseDebugInfo::setResolutionInfo stores a cross-compartment wrapper to the foreign SavedFrame as the resolution site (js/src/builtin/Promise.cpp:601-616). - Leave the rejection unhandled so the shell calls ReportUnhandledRejections -> PrintUnhandledRejection at exit.
- PrintUnhandledRejection reads promise->resolutionSite() (live wrapper at this point), then calls JS_ValueToSource(reason), which invokes o.toSource().
- o.toSource() calls the shell function nukeAllCCWs(), turning the rooted
sitewrapper into a dead proxy. - PrintUnhandledRejection passes
siteto JS::BuildStackString -> js::UnwrapSavedFrame -> JSObject::maybeUnwrapAs<SavedFrame>(), which hits MOZ_CRASH("Invalid object. Dead wrapper?") at js/src/vm/JSObject.h:699 and the shell aborts.
Steps to Reproduce
- Start from an unmodified tree (git -C /firefox diff is empty); no patches, prefs, or extra shell flags.
- Build the JS shell with build_firefox using /app/mozconfigs/mozconfig.js.linux.debug (build_dir /firefox/obj-js-debug).
- Run testcase.js with js_shell_evaluator and js_binary=/firefox/obj-js-debug/dist/bin/js (the evaluator adds --fuzzing-safe):
var g = newGlobal({newCompartment: true});
g.evaluate("function thrower(o) { throw o; }");
var o = {toSource() { nukeAllCCWs(); return "o"; }};
async function f() { g.thrower(o); }
f();
4. Observe crashed=true with 'Hit MOZ_CRASH(Invalid object. Dead wrapper?) at /firefox/js/src/vm/JSObject.h:699'.
5. Controls run by the verifier, both crashed=false (exit code 3, ordinary unhandled-rejection report): (a) same testcase with nukeAllCCWs() removed from toSource; (b) nukeAllCCWs() called at top level after f() instead of inside toSource, where resolutionSite() filters the dead wrapper.
Security Impact
- Severity: Low
- Attacker capability: Abort the SpiderMonkey JS shell with a deterministic MOZ_CRASH at shell exit. No out-of-bounds access, use-after-free, or type confusion was observed; the crash is an intentional abort on a dead wrapper. The faulty ordering is in js/src/shell/js.cpp, which is shell code, and the trigger uses the shell-only nukeAllCCWs() function, so no impact on Firefox was demonstrated.
- Preconditions: Ability to run script in the JS shell with access to the shell testing functions newGlobal and nukeAllCCWs (both available under --fuzzing-safe). Not shown to be reachable from web content.
ASAN Report
Unhandled rejection: o
[2221] Hit MOZ_CRASH(Invalid object. Dead wrapper?) at /firefox/js/src/vm/JSObject.h:699
UndefinedBehaviorSanitizer:DEADLYSIGNAL
==2221==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000000 (pc 0x55ee9cf78956 bp 0x7ec7e9500a70 sp 0x7ec7e9500a60 T2221)
==2221==The signal is caused by a READ memory access.
==2221==Hint: address points to the zero page.
#0 0x55ee9cf78956 in MOZ_CrashSequence(void*, long) /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3
#1 0x55ee9cf78956 in js::SavedFrame* JSObject::maybeUnwrapAs<js::SavedFrame>() /firefox/js/src/vm/JSObject.h:699:3
#2 0x55ee9d0439a5 in js::UnwrapSavedFrame(JSContext*, JSPrincipals*, JS::Handle<JSObject*>, JS::SavedFrameSelfHosted, bool&) /firefox/obj-js-debug/js/src/vm/./../../../../js/src/vm/SavedStacks.cpp:761:38
#3 0x55ee9d045b07 in JS::BuildStackString(JSContext*, JSPrincipals*, JS::Handle<JSObject*>, JS::MutableHandle<JSString*>, unsigned long, js::StackFormat) /firefox/obj-js-debug/js/src/vm/./../../../../js/src/vm/SavedStacks.cpp:1066:16
#4 0x55ee9ba23f37 in PrintUnhandledRejection(JSContext*, JS::Handle<js::PromiseObject*>) /firefox/obj-js-debug/js/src/shell/./../../../../js/src/shell/js.cpp:12453:8
#5 0x55ee9ba23f37 in ReportUnhandledRejections(JSContext*) /firefox/obj-js-debug/js/src/shell/./../../../../js/src/shell/js.cpp:12519:10
#6 0x55ee9b9ddfe2 in Shell(JSContext*, js::cli::OptionParser*) /firefox/obj-js-debug/js/src/shell/./../../../../js/src/shell/js.cpp:12653:19
#7 0x55ee9b9d63d6 in main /firefox/obj-js-debug/js/src/shell/./../../../../js/src/shell/js.cpp:13056:12
#8 0x7ee9fe96f1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: a4a7992a8e66555c8141ab2a08a8465ff6e0ea65)
#9 0x7ee9fe96f28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: a4a7992a8e66555c8141ab2a08a8465ff6e0ea65)
#10 0x55ee9b99bb98 in _start (/firefox/obj-js-debug/dist/bin/js+0x2766b98) (BuildId: 538a5e1d223f99074750b183c7f52e6d)
UndefinedBehaviorSanitizer can not provide additional info.
SUMMARY: UndefinedBehaviorSanitizer: SEGV /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3 in MOZ_CrashSequence(void*, long)
==2221==ABORTING
| Reporter | ||
Comment 1•1 month ago
|
||
| Reporter | ||
Comment 2•1 month ago
|
||
| Reporter | ||
Comment 3•1 month ago
|
||
| Reporter | ||
Comment 4•1 month ago
|
||
This bug was filed automatically by an AI-powered security testing tool. The analysis, testcase, and any suggested patches were generated by an AI model and have NOT been verified by a human.
Please review all details carefully before triaging. The issue was confirmed by an independent verifier, but for non-ASAN issues, the verifier has been made less strict. Additionally, the root cause analysis and suggested fix may contain errors.
Updated•1 month ago
|
Updated•1 month ago
|
Comment 5•1 month ago
|
||
This is a bug in a shell testing function. (The proposed patch only touches js/src/shell/js.cpp.) This isn't a sec bug.
Description
•