Closed Bug 2013619 (CVE-2026-6758) Opened 9 months ago Closed 8 months ago

Assertion failure: this->flags() == 0, at /js/src/gc/Cell.h:800 with Debugger and wasm

Categories

(Core :: JavaScript: WebAssembly, defect, P1)

x86_64
Linux
defect

Tracking

()

VERIFIED FIXED
150 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr140 --- unaffected
firefox148 --- wontfix
firefox149 --- wontfix
firefox150 + fixed

People

(Reporter: decoder, Assigned: rhunt)

Details

(8 keywords, Whiteboard: [bugmon:update,bisected,confirmed][adv-main150+])

Attachments

(4 files, 2 external files, 2 obsolete files)

The following testcase crashes on mozilla-central revision 20260130-20b1a1e6c530 (fuzzing-debug build, run with --fuzzing-safe --no-threads):

See attachment.

Backtrace:

==1750025==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000000 (pc 0x5887bf3e04ab bp 0x7ffc277df4f0 sp 0x7ffc277df4f0 T1750025)
==1750025==The signal is caused by a WRITE memory access.
==1750025==Hint: address points to the zero page.
    #0 0x5887bf3e04ab in MOZ_CrashSequence /builds/worker/workspace/obj-build/dist/include/mozilla/Assertions.h:237:3
    #1 0x5887bf3e04ab in headerPtr /js/src/gc/Cell.h:800:5
    #2 0x5887bf3e04ab in shape /js/src/vm/JSObject.h:94:37
    #3 0x5887bf3e04ab in JSObject::getClass() const /js/src/vm/JSObject.h:115:44
    #4 0x5887c0b7b604 in is<WasmValueBox> /js/src/vm/JSObject.h:512:12
    #5 0x5887c0b7b604 in js::wasm::AnyRef::toJSValue() const /js/src/wasm/WasmAnyRef.cpp:121:13
    #6 0x5887c0d0d95a in js::wasm::DebugFrame::getLocal(unsigned int, JS::MutableHandle<JS::Value>) /js/src/wasm/WasmDebugFrame.cpp:120:34
    #7 0x5887c0a50c48 in (anonymous namespace)::DebugEnvironmentProxyHandler::handleUnaliasedAccess(JSContext*, JS::Handle<js::DebugEnvironmentProxy*>, JS::Handle<js::EnvironmentObject*>, JS::Handle<JS::PropertyKey>, (anonymous namespace)::DebugEnvironmentProxyHandler::Action, JS::MutableHandle<JS::Value>, (anonymous namespace)::DebugEnvironmentProxyHandler::AccessResult*) const /js/src/vm/EnvironmentObject.cpp:1943:27
    #8 0x5887c0a2af0c in getMaybeSentinelValue /js/src/vm/EnvironmentObject.cpp:2380:10
    #9 0x5887c0a2af0c in js::DebugEnvironmentProxy::getMaybeSentinelValue(JSContext*, JS::Handle<js::DebugEnvironmentProxy*>, JS::Handle<JS::PropertyKey>, JS::MutableHandle<JS::Value>) /js/src/vm/EnvironmentObject.cpp:2655:50
    #10 0x5887bf7ac999 in js::DebuggerEnvironment::getVariable(JSContext*, JS::Handle<js::DebuggerEnvironment*>, JS::Handle<JS::PropertyKey>, JS::MutableHandle<JS::Value>) /js/src/debugger/Environment.cpp:602:12
    #11 0x5887bf7ac6d4 in js::DebuggerEnvironment::CallData::getVariableMethod() /js/src/debugger/Environment.cpp:324:10
    #12 0x5887bf7ae781 in bool js::DebuggerEnvironment::CallData::ToNative<&js::DebuggerEnvironment::CallData::getVariableMethod()>(JSContext*, unsigned int, JS::Value*) /js/src/debugger/Environment.cpp:140:10
    #13 0x5887c07940c4 in CallJSNative(JSContext*, bool (*)(JSContext*, unsigned int, JS::Value*), js::CallReason, JS::CallArgs const&) /js/src/vm/Interpreter.cpp:490:13
    #14 0x5887c079396f in js::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason) /js/src/vm/Interpreter.cpp:586:12
    #15 0x5887c07a4652 in CallFromStack /js/src/vm/Interpreter.cpp:658:10
    #16 0x5887c07a4652 in js::Interpret(JSContext*, js::RunState&) /js/src/vm/Interpreter.cpp:3272:16
    #17 0x5887c0792fea in js::RunScript(JSContext*, js::RunState&) /js/src/vm/Interpreter.cpp:460:13
    #18 0x5887c0793995 in js::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason) /js/src/vm/Interpreter.cpp:618:13
    #19 0x5887c0794d6c in js::Call(JSContext*, JS::Handle<JS::Value>, JS::Handle<JS::Value>, js::AnyInvokeArgs const&, JS::MutableHandle<JS::Value>, js::CallReason) /js/src/vm/Interpreter.cpp:685:8
    #20 0x5887bf5caf66 in js::Call(JSContext*, JS::Handle<JS::Value>, JSObject*, JS::Handle<JS::Value>, JS::Handle<JS::Value>, JS::MutableHandle<JS::Value>) /js/src/vm/Interpreter.h:145:10
    #21 0x5887bf7774ac in js::Debugger::fireExceptionUnwind(JSContext*, JS::Handle<JS::Value>, js::ResumeMode&, JS::MutableHandle<JS::Value>) /js/src/debugger/Debugger.cpp:2299:13
    #22 0x5887bf772669 in operator() /js/src/debugger/Debugger.cpp:1391:21
    #23 0x5887bf772669 in operator() /js/src/debugger/Debugger.cpp:887:32
    #24 0x5887bf772669 in operator() /js/src/debugger/Debugger.cpp:855:61
    #25 0x5887bf772669 in enterDebuggerHook<(lambda at /js/src/debugger/Debugger.cpp:855:38)> /js/src/debugger/Debugger.h:1066:10
    #26 0x5887bf772669 in dispatchHook<(lambda at /js/src/debugger/Debugger.cpp:886:23)> /js/src/debugger/Debugger.cpp:855:16
    #27 0x5887bf772669 in dispatchResumptionHook<(lambda at /js/src/debugger/Debugger.cpp:1389:7)> /js/src/debugger/Debugger.cpp:885:10
    #28 0x5887bf772669 in js::DebugAPI::slowPathOnExceptionUnwind(JSContext*, js::AbstractFramePtr) /js/src/debugger/Debugger.cpp:1387:30
    #29 0x5887c0ccc217 in onExceptionUnwind /js/src/debugger/DebugAPI-inl.h:165:12
    #30 0x5887c0ccc217 in js::wasm::HandleExceptionWasm(JSContext*, js::JitFrameIter&, js::jit::ResumeFromException*) /js/src/wasm/WasmBuiltins.cpp:883:12
    #31 0x5887c05a20af in js::jit::HandleException(js::jit::ResumeFromException*) /js/src/jit/JitFrames.cpp:758:7
    #32 0x5887c0cd08a7 in WasmHandleThrow(js::jit::ResumeFromException*) /js/src/wasm/WasmBuiltins.cpp:929:3
    #33 0x2855d6715544  ([anon:js-executable-memory]+0x544)

==1750025==Register values:
rax = 0x0000000000000000  rbx = 0x000012485d201cd0  rcx = 0x0000000000000320  rdx = 0x00007ea0e3204563  
rdi = 0x00007ea0e3205700  rsi = 0x0000000000000000  rbp = 0x00007ffc277df4f0  rsp = 0x00007ffc277df4f0  
 r8 = 0x0000000000000000   r9 = 0x0000000000000003  r10 = 0x0000000000000002  r11 = 0x0000000000000293  
r12 = 0x0000000000000013  r13 = 0x0000000000000000  r14 = 0x0000000000000001  r15 = 0x0000000000000030  
UndefinedBehaviorSanitizer can not provide additional info.
SUMMARY: UndefinedBehaviorSanitizer: SEGV /builds/worker/workspace/obj-build/dist/include/mozilla/Assertions.h:237:3 in MOZ_CrashSequence
==1750025==ABORTING

This bug is part of a series of bugs reported by Anthropic, thank you for your contributions to Firefox!

Attached file Testcase —

Verified bug as reproducible on mozilla-central 20260130211306-fda5656fd125.
Unable to bisect testcase (Testcase reproduces on start build!):

Start: d87cd04072b041911d218cec1baf59de7f2a5ede (20251103213617)
End: 20b1a1e6c530f3506d5378bf5f4da504b6f03661 (20260130092147)
BuildFlags: BuildFlags(asan=True, tsan=False, debug=False, fuzzing=True, coverage=False, valgrind=False, no_opt=False, fuzzilli=False, nyx=False, searchfox=False, afl=False)

Whiteboard: [bugmon:update,bisect] → [bugmon:update,bisected,confirmed]
Severity: -- → S3
Priority: -- → P1
Assignee: nobody → jseward
Attached file testcase2013619c2.js (obsolete) —

A reduced test case. Note that changing the type of (wasm) function
testTraps argument from externref to i32 causes the failure to
disappear, and also for valgrind to no longer complain that we're reading
from a poisoned Nursery.

Attachment #9544254 - Attachment mime type: application/octet-stream → text/javascript
Attached file testcase2013619c5.js —

Here's a slightly revised testcase and a summary of what happened.
The essence is that we allocate a GC object (any kind will do) by calling mkS
(from JS-land):

  (type $S (struct (field i32)))
  (func (export "mkS") (result structref)
    struct.new_default $S
  )

Then, in another call from JS-land it is passed up a chain of functions,
beginning at three:

  (func $zero (export "zero") (param structref)
    (unreachable)
  )
  (func $one (export "one") (param structref)
    (local.get 0) (call $zero)
  )
  (func $two (export "two") (param structref)
    (local.get 0) (call $one)
  )
  (func $three (export "three") (param structref)
    (local.get 0) (call $two)
  )

The chain of function calls exists only to make the stack traces clearer.

We arrive at zero, which traps. zeros frame is overwritten (by who?) with
a frame from code generated by GenerateThrowStub (written (GenerateThrowStub)
below).

This calls onwards, per the dbg.onExceptionUnwind setting, and eventually
does a moving GC. This appears to cause a call to Instance::traceFrame (and
Instance::updateFrameForMovingGC) for the frame (GenerateThrowStub) but with
a nextPC for the zero frame, that has by now been overwritten. This causes
no stackmap to be found and I suspect is closely related to the missing update
of the stack local.

Subsequently env.getvariable(n) causes DebugFrame::getLocal to read this
non-updated ref, which points into a poisoned Nursery, so we crash soon after.

This is difficult to debug because I don't know what was supposed to have
happened. In particular, is it intended that a frame that traps gets replaced
with a frame from (GenerateThrowStub) ?

Comment 6 contains supporting details and further questions at the end.

Attachment #9541416 - Attachment is obsolete: true
Attachment #9544254 - Attachment is obsolete: true

More details and some questions, in support of the summary in comment 5.
Note these are from an x86_32-linux build, to avoid any complexities
resulting from the amd64-ELF ABI red zone.

The stack at the time GC happens, as shown by GDB:

(further in)
#20 0x59cf9be7  js::DebugAPI::onExceptionUnwind   js/src/debugger/DebugAPI-inl.h:165
#21 0x5a665146  js::wasm::HandleExceptionWasm     js/src/wasm/WasmBuiltins.cpp:887
#22 0x59cd9c70  js::jit::HandleException (rfe=0xffffa0d8)  js/src/jit/JitFrames.cpp:758
#23 0x5a66ae26  WasmHandleThrow (rfe=0xffffa0d8)  js/src/wasm/WasmBuiltins.cpp:933
#24 0x437063e2  ??? ()
#25 0x43726370  ??? ()
#26 0x43726470  ??? ()
#27 0x43726570  ??? ()
#28 0x437268e9  ??? ()
#29 0x5a70c9ac  js::wasm::Instance::callExport  js/src/wasm/WasmInstance.cpp:3979
#30 0x5a70bf85  WasmCall                        js/src/wasm/WasmInstance.cpp:3765
#31 0x59f01af9  CallJSNative                    js/src/vm/Interpreter.cpp:490
(further out)

and as shown by Valgrind, including JITted function names. We can see that GDB
and V agree on the stack, modulo page numbers:

(further in)
     0x77A4BE6  js::DebugAPI::onExceptionUnwind js/src/debugger/DebugAPI-inl.h:165
     0x8110145  js::wasm::HandleExceptionWasm   js/src/wasm/WasmBuiltins.cpp:887
     0x7784C6F  js::jit::HandleException        js/src/jit/JitFrames.cpp:758
     0x8115E25  WasmHandleThrow                 js/src/wasm/WasmBuiltins.cpp:933
    0x4CE2B3E1  wasmSharedStubs:Throw@..4ce2b3c0 (in ???)
    0x4CE6B36F  wasmBL:fI=2:one (in ???)
    0x4CE6B46F  wasmBL:fI=3:two (in ???)
    0x4CE6B56F  wasmBL:fI=4:three (in ???)
    0x4CE6B8E8  wasmBL:fI=4:InterpEntry@..4ce6b8a0 (in ???)
     0x81B79AB  js::wasm::Instance::callExport  js/src/wasm/WasmInstance.cpp:3979
     0x81B6F84  WasmCall                        js/src/wasm/WasmInstance.cpp:3765
     0x79ACAF8  CallJSNative                    js/src/vm/Interpreter.cpp:490
(further out)

Everything after his point pertains to the Valgrind stack.

Code ranges of the relevant JITted functions are as follows. Note this isn't
a stack.

C::aCB == Code::addCodeBlock
C::aCB [c:0x4ce2b3c0-c:0x4ce2b3e7) 39 wasmSharedStubs:Throw@..4ce2b3c0
                                      == (GenerateThrowStub)
C::aCB [c:0x4ce6b040-c:0x4ce6b1f7) 439 wasmBL:fI=0:wasm-function[0]
C::aCB [c:0x4ce6b200-c:0x4ce6b2c0) 192 wasmBL:fI=1:zero
C::aCB [c:0x4ce6b2c0-c:0x4ce6b3b9) 249 wasmBL:fI=2:one
C::aCB [c:0x4ce6b3c0-c:0x4ce6b4b9) 249 wasmBL:fI=3:two
C::aCB [c:0x4ce6b4c0-c:0x4ce6b5b9) 249 wasmBL:fI=4:three
C::aCB [c:0x4ce6b5d0-c:0x4ce6b647) 119 wasmBL:fI=0:InterpEntry@..4ce6b5d0

Here's an analysis of code addresses passed to Instance::traceFrame. The first
entry is the important one.

(further in)
Instance::traceFrame  frameBase=s:0xfe87f198  nextPC=c:0x4ce6b29b  map=d:(nil)
  # function = (from GenerateThrowStub)
  # but nextPC (0x4ce6b29b) isn't from this function
  # it is inside "zero"
  # So the unwinder is in the frame for (GenerateThrowStub) but we are presented
  #   with a PC from "zero" instead

Instance::traceFrame  frameBase=s:0xfe87f1f8  nextPC=c:0x4ce6b370  map=d:0xaf89520
  frame=s:0xfe87f1b0-s:0xfe87f210
  # function = "one", nextPC valid

Instance::traceFrame  frameBase=s:0xfe87f258  nextPC=c:0x4ce6b470  map=d:0xaf895c0
  frame=s:0xfe87f210-s:0xfe87f270
  # function = "two", nextPC valid

Instance::traceFrame  frameBase=s:0xfe87f2b8  nextPC=c:0x4ce6b570  map=d:0xaf89660
  frame=s:0xfe87f270-s:0xfe87f2d0
  # function = "three", nextPC valid
(further out)

Here's a reconstruction of the frame boundaries:

DebugFrame::getLocal: reading ref at 0xfe87f1a8
  # This is the local ref that apparently doesn't get updated across GC

# Frame boundaries at time of GC are
  (further out)
  frame=s:0xfe87f270-s:0xfe87f2d0 // three
  frame=s:0xfe87f210-s:0xfe87f270 // two
  frame=s:0xfe87f1b0-s:0xfe87f210 // one

  frameBase=s:0xfe87f198 // (GenerateThrowStub)
  .. ASSUMING.. frame=s:0xfe87f190-s:0xfe87f1b0
  (further in)

# So the not-updated local is in the frame of (GenerateThrowStub)

Finally, some questions resulting from all this:

(0) Is it indeed correct, that a wasm frame that traps gets overwritten by a
frame from (GenerateThrowStub) ?

(1) Where did DebugFrame::getLocal get the local address 0xfe87f1a8
from? Did it really mean to query that, or is it the address of
the local in the (now-overwritten) function zero frame?

(2) When unwinding, why are we given a nextPC for (GenerateThrowStub) which
is again from the overwritten zero frame?

(3) Even if (2) were fixed so that nextPC was actually inside
(GenerateThrowStub), does it have a stackmap? Even if it does, the
jit::ResumeFromException structure that is really the only thing in
(GenerateThrowStub)'s frame contains two JS::Values; everything else is
scalar. Can the wasm stackmap mechanism even handle a JS::Value rather
than a standard GC-thing pointer? I don't think so.

Attachment #9541416 - Attachment is obsolete: false

After some discussion with Jan, it appears that the reduced test case from
comment 5 fails because we don't create a stackmap for the wasm unreachable
opcode. And the original test case from decoder (which I have now
un-obsoleted) also fails because there's no stackmap for the integer
divide-by-zero trap.

The following ultra-kludge makes both test cases run successfully. As a
general fix it seems to me we'd need to create a stackmap for every trap insn
we create in debug mode. Maybe we can make BaseCompiler::trap do that,
since it can easily enough consult compilerEnv_.debugEnabled().

diff --git a/js/src/wasm/WasmBaselineCompile.cpp b/js/src/wasm/WasmBaselineCompile.cpp
index dc5950441a..efdfcc0c32 100644
--- a/js/src/wasm/WasmBaselineCompile.cpp
+++ b/js/src/wasm/WasmBaselineCompile.cpp
@@ -294,6 +294,8 @@
   Label nonZero;
   masm.branchTest32(Assembler::NonZero, rhs, rhs, &nonZero);
   trap(Trap::IntegerDivideByZero);
+  (void/*FIXME*/)createStackMap("BC::checkDivideByZero(RegI32)",
+                                HasDebugFrameWithLiveRefs::Maybe);
   masm.bind(&nonZero);
 }
 
@@ -10573,6 +10588,10 @@
         CHECK(iter_.readUnreachable());
         if (!deadCode_) {
           trap(Trap::Unreachable);
+          if (!createStackMap("unreachable",
+                              HasDebugFrameWithLiveRefs::Maybe)) {
+            return false;
+          }
           deadCode_ = true;
         }
         NEXT();

Attached file (secure) (obsolete) —

In the wasm baseline compiler, generate stackmaps when JS debugging is enabled,
for some cases that were previously missing:

  • traps resulting from integer division (or remainder) by zero

  • traps resulting from integer division (or remainder) overflow

  • the wasm unreachable opcode

Actual changes are:

  • BaseCompiler::trap is renamed to BaseCompiler::trapWithoutStackMap; no change
    of behaviour (it just generates a trap insn)

  • new method BaseCompiler::trapWithDebugStackMap, which also generates a trap
    instruction, and, if JS debugging is enabled, a stackmap

  • change abovementioned cases (integer div/rem, unreachable) to use
    ::trapWithDebugStackMap

  • propagate resulting bool-return-OOM changes in the usual way

  • change all other uses of ::trap to track its renaming; no functional change.

Attached file (secure) —
Attached file (secure) —
Group: javascript-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 8 months ago
Resolution: --- → FIXED
Target Milestone: --- → 150 Branch

Verified bug as fixed on rev mozilla-central 20260303043313-961340a3993b.
Removing bugmon keyword as no further action possible. Please review the bug and re-add the keyword for further analysis.

Status: RESOLVED → VERIFIED
Keywords: bugmon

The patch landed in nightly and beta is affected.
:jseward, is this bug important enough to require an uplift?

For more information, please visit BugBot documentation.

Flags: needinfo?(jseward)
Assignee: jseward → rhunt

I don't think this is worth uplifting because it requires the debugger to perform specific actions to lead to the UaF. I don't think web content could exploit this.

Flags: needinfo?(jseward)
QA Whiteboard: [sec] [qa-triage-done-c151/b150]
Whiteboard: [bugmon:update,bisected,confirmed] → [bugmon:update,bisected,confirmed][adv-main150+]
Alias: CVE-2026-6758
[task 2026-08-12T15:40:49.615+00:00] TEST-PASS | js/src/jit-test/tests/debug/bug2008675.js | Success (code 0, args "--blinterp-eager") [0.0 s]
[task 2026-08-12T15:40:49.615+00:00] @evaluate:22:13 ReferenceError: WebAssembly is not defined
[task 2026-08-12T15:40:49.615+00:00] Stack:
[task 2026-08-12T15:40:49.615+00:00]   @@evaluate:22:13
[task 2026-08-12T15:40:49.615+00:00]   @/builds/worker/checkouts/gecko/js/src/jit-test/tests/debug/bug2013619.js:12:3
[task 2026-08-12T15:40:49.615+00:00] Exit code: 3
[task 2026-08-12T15:40:49.615+00:00] FAIL - debug/bug2013619.js
[task 2026-08-12T15:40:49.615+00:00] TEST-UNEXPECTED-FAIL | js/src/jit-test/tests/debug/bug2013619.js | @evaluate:22:13 ReferenceError: WebAssembly is not defined (code 3, args "--ion-eager --ion-offthread-compile=off --ion-check-range-analysis --ion-extra-checks --no-sse3 --no-threads") [0.0 s]
[task 2026-08-12T15:40:49.615+00:00] INFO exit-status     : 3
Flags: needinfo?(rhunt)
Attachment #9546082 - Attachment is obsolete: true
Flags: needinfo?(rhunt)
Group: core-security-release
Keywords: keep-hidden
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: