Skip to content

deps: V8: backport 0b94a9fd23ba - #65753

Open
ruangustavo wants to merge 1 commit into
nodejs:v24.x-stagingfrom
ruangustavo:backport-v8-0b94a9fd23ba
Open

deps: V8: backport 0b94a9fd23ba#65753
ruangustavo wants to merge 1 commit into
nodejs:v24.x-stagingfrom
ruangustavo:backport-v8-0b94a9fd23ba

Conversation

@ruangustavo

Copy link
Copy Markdown

Backport of v8/v8@0b94a9f ("[leaptiering] Fix BaselineOutOfLinePrologue builtin") to the V8 13.6 in v24.x. Applies cleanly to deps/v8; v8_embedder_string bumped to -node.54.

Fixes: #62393

What crashes

A register that nothing guarantees, pushed on the stack as if it were a tagged pointer, then read by the GC:

sequenceDiagram
    participant M as JS caller (Sparkplug code)
    participant CL as CompileLazy (TurboFan/CSA-generated)
    participant P as BaselineOutOfLinePrologue (hand-written asm)
    participant GC as mark-compact
    M->>CL: first call of f()
    Note over CL: x4 is NOT part of the JS linkage here<br/>(V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE undefined)<br/>register allocator uses x4 as scratch → x4 = 0x7
    CL->>P: tail call into freshly compiled baseline code
    Note over P: stack guard slow path:<br/>Push(x4, new_target)  // "dispatch handles always look like Smis"
    P->>GC: Runtime_StackGuardWithGap → CollectGarbage
    Note over GC: InternalFrame::Iterate → ClearStaleLeftTrimmedPointerVisitor<br/>IsHeapObject(0x7) is true (low bit set)<br/>reads map_word at 0x7 - 1
    GC--xGC: SIGSEGV, KERN_INVALID_ADDRESS at 0x6
Loading

Node.js builds V8 with leaptiering on and the sandbox off. In that configuration V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE is not defined (src/common/globals.h), so kJavaScriptCallDispatchHandleRegister (x4 on arm64, r15 on x64) is not carried by JS calls. Two hand-written builtins still gated the push on V8_ENABLE_LEAPTIERING_BOOL. The upstream commit message says it directly: "This issue triggered only on non-sandbox configuration with enabled leaptiering."

Why only v24: ClearStaleLeftTrimmedPointerVisitor started visiting stack roots in V8 12.6 (v8/v8@22c404b8bbbb, 2024-05). Node 22 ships V8 12.4, so the same stale slot was never dereferenced there. The fix landed in V8 main after the 13.6 branch cut and is present in Node 25/26.

Evidence

Instrumented v24.20.0 (macOS arm64) logging every non-Smi, non-pointer value in INTERNAL frame slots right before the GC visitor ran. Three independent crashes, identical frame:

builtin = BaselineOutOfLinePrologue   code_offset = 116   INTERNAL frame, 6 slots from sp:

[0] 0x7000000000    SmiTag(frame_size)              ok
[1] 0x0             padreg (PushArgument)           ok
[2] 0x10b697280011  new_target                      ok, valid HeapObject
[3] 0x7             kJavaScriptCallDispatchHandleRegister   <-- stale; callee's real dispatch_handle was 0x143c900
[4] 0x0             padreg (EnterFrame)             ok
[5] 0x2e            INTERNAL frame marker (23 << 1) ok

Matching crash report for the same worker: EXC_BAD_ACCESS / KERN_INVALID_ADDRESS at 0x0000000000000006 in ClearStaleLeftTrimmedPointerVisitor::VisitRootPointersInternalFrame::Iterate.

Stack walk at that point (callee = first call of a small module-level helper, i.e. the CompileLazy path):

 0  EXIT      CEntry_Return1_ArgvOnStack_NoBuiltinExit
 1  INTERNAL  BaselineOutOfLinePrologue                          <-- bad slot here
 2  BASELINE  forEach            (axios.cjs)   dispatch_handle 0x143c900
 3  BASELINE  <module wrapper>   (axios.cjs)   dispatch_handle 0x143c400
 4  TURBOFAN  _execModule        (jest-runtime)

A temporary check at the builtin entry (x4 == closure.dispatch_handle, abort otherwise) fires even for:

node --always-sparkplug -e 'function f(a,b){return a+b}; f(1,2)'

so this is not memory corruption: in this configuration the register simply never holds the handle.

The fix (same as upstream, all ports)

 deps/v8/src/builtins/arm64/builtins-arm64.cc          Generate_BaselineOutOfLinePrologue
 deps/v8/src/codegen/arm64/macro-assembler-arm64.cc    GenerateTailCallToReturnedCode
-    Register maybe_dispatch_handle = V8_ENABLE_LEAPTIERING_BOOL
+    Register maybe_dispatch_handle = V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE_BOOL
                                          ? kJavaScriptCallDispatchHandleRegister
                                          : padreg;
     static_assert(kJSDispatchHandleShift > 0);
+    __ AssertSmi(maybe_dispatch_handle);
     __ Push(maybe_dispatch_handle, new_target);

With the linkage macro undefined the slot now receives padreg (xzr, i.e. Smi 0) instead of whatever was in x4. Same change on x64 (#ifdef V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE around the push/pop), loong64, mips64 and riscv.

Verification

Same jest suite (322 suites / 5184 tests), 4 workers, --no-maglev --always-sparkplug --max-old-space-size=512, macOS arm64:

build runs runs with a SIGSEGV worker invalid slots logged by instrumentation
v24.20.0 official, 12 workers, under load 5 5 n/a
v24.20.0 + instrumentation, no fix 4 1 3
v24.20.0 + instrumentation + this patch 6 0 0

The last column matters more than the crash count: the instrumentation logged any non-pointer tagged value in an INTERNAL frame slot whether or not a GC happened to hit it.

I have not run V8 CI / make test-v8; please trigger it.

Refs: v8/v8@0b94a9f

Original commit message:

    [leaptiering] Fix BaselineOutOfLinePrologue builtin

    ... which tried to preserve kJavaScriptCallDispatchHandleRegister even
    on configurations where it's not used which resulted in a random value
    on the stack discoverable by GC.
    This issue triggered only on non-sandbox configuration with enabled
    leaptiering.

    Drive-by: fix MacroAssembler::GenerateTailCallToReturnedCode() on riscv
    port which wasn't preserving dispatch handle as all the other ports do.

    Bug: 42204201
    Fixed: 413769394

    Change-Id: If146b0b7a6cf972ed5a881142f40980774f19cba
    Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6587010
    Commit-Queue: Igor Sheludko <ishell@chromium.org>
    Reviewed-by: Olivier Flückiger <olivf@chromium.org>
    Cr-Commit-Position: refs/heads/main@{#100512}

Node.js 24 builds V8 with leaptiering enabled and the sandbox disabled,
so V8_JS_LINKAGE_INCLUDES_DISPATCH_HANDLE is not defined and the JS
calling convention does not carry the dispatch handle register (x4 on
arm64). BaselineOutOfLinePrologue and GenerateTailCallToReturnedCode
still pushed that register as a tagged slot of an INTERNAL frame, so
whatever value the caller left there is dereferenced by
ClearStaleLeftTrimmedPointerVisitor during mark-compact root scanning
and crashes the process with SIGSEGV (seen as jest workers dying).

Refs: v8/v8@0b94a9f
Fixes: nodejs#62393
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/gyp
  • @nodejs/security-wg
  • @nodejs/v8-update

@nodejs-github-bot nodejs-github-bot added build Issues and PRs related to Node.js builds or CI infrastructure. needs-ci PRs that need a full CI run. v24.x Issues that can be reproduced on v24.x or PRs targeting the v24.x-staging branch. v8 engine Issues and PRs related to the V8 dependency. labels Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Issues and PRs related to Node.js builds or CI infrastructure. needs-ci PRs that need a full CI run. v8 engine Issues and PRs related to the V8 dependency. v24.x Issues that can be reproduced on v24.x or PRs targeting the v24.x-staging branch.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants