[wasm] Fix stack shrinking during tail calls on growable stacks [v8/v8 : main]

0 views
Skip to first unread message

Francis McCabe (Gerrit)

unread,
Aug 12, 2026, 6:21:45 PM (3 days ago) Aug 12
to Ryan Diaz, Thibaud Michaud, Code Review Nudger, android-bu...@system.gserviceaccount.com, v8-s...@luci-project-accounts.iam.gserviceaccount.com, v8-ppc...@googlegroups.com, v8-mip...@googlegroups.com, v8-risc...@chromium.org, dmercadi...@chromium.org, v8-re...@googlegroups.com, was...@google.com
Attention needed from Ryan Diaz and Thibaud Michaud

Francis McCabe added 17 comments

Patchset-level comments
File-level comment, Patchset 43 (Latest):
Francis McCabe . resolved

Made a few improvements and implemented on missing architectures.

Commit Message
Line 12, Patchset 17:call. This deallocates the secondary segment and ensures stack
Thibaud Michaud . resolved

This doesn't actually deallocate it, we keep it around so that we can reuse it in case we grow again. Shrinking just switches back to the previous segment.

Francis McCabe

Acknowledged

Line 11, Patchset 17:grew the stack), we must shrink the stack before executing the tail
call. This deallocates the secondary segment and ensures stack
Thibaud Michaud . resolved

Can you explain why? I.e. because otherwise, the tail callee overwrites the frame type slot containing WASM_SEGMENT_START with a new WASM frame type. So when the function returns, it sees the WASM frame type and does not correctly switch back to the previous segment (doesn't update the stack limit, doesn't copy the stack returns, etc.). I think it can also confuse the stack walker.

Francis McCabe

Acknowledged

File src/compiler/backend/arm64/code-generator-arm64.cc
Line 1022, Patchset 17: if (v8_flags.wasm_growable_stacks) {
Thibaud Michaud . resolved

Please add a high-level comment to explain why we need to shrink before the tail call.

Francis McCabe

Acknowledged

Line 1028, Patchset 17: // Check if we need to shrink stack (marker at sp - 24)
__ Ldr(scratch, MemOperand(sp, -24));
Thibaud Michaud . resolved

Please consider using `StandardFrameConstants::kContextOrFrameTypeOffset` (fp-relative) to avoid using a magic number.

Francis McCabe

Acknowledged

Line 1045, Patchset 17: __ PushCPURegList(gp_regs);
Thibaud Michaud . resolved

There is a DCHECK inside this call:
https://source.chromium.org/chromium/chromium/src/+/main:v8/src/codegen/arm64/macro-assembler-arm64.cc;drc=0dfa33ccbd59150e54c81dee86e42a4df6232a83;l=65

Which I'm hitting when I try to run regress-520827581.js on arm64.

I think we need to use a padding reg here to ensure that we push an even number of them.

After adding this padding reg, I played around with regress-520827581.js, and I noticed that it crashes if I add `--no-wasm-inlining`.

Can you confirm these two issues locally?

Francis McCabe

Acknowledged

Line 1051, Patchset 17: // Call wasm_shrink_stack
Thibaud Michaud . resolved

Self-explanatory, please remove.

Francis McCabe

Acknowledged

Line 1056, Patchset 17: // Temporarily restore sp to Segment 1 stack to pop registers
Thibaud Michaud . resolved

Full stop at the end of comments (here and in several other places).

Francis McCabe

Acknowledged

Line 1065, Patchset 17: // Copy return address (saved lr in x20) to Segment 0 return address
// slot? Wait, on arm64 we don't need to write lr to stack, but we must
// restore it to lr register!
Thibaud Michaud . resolved

This comment sounds like an AI talking to itself, can we rephrase it?

Francis McCabe

Acknowledged

Line 1068, Patchset 17: __ Mov(lr, x20);
Thibaud Michaud . resolved

`x20` is uninitialized here. Is there supposed to be a `__ Mov(x20, lr)` before the C call?

Francis McCabe

Acknowledged

File src/compiler/backend/x64/code-generator-x64.cc
Line 1760, Patchset 17: __ movq(r15, Operand(rsp, 0));
__ movq(r12, rsp);
Thibaud Michaud . resolved

What if `r12` and `r15` hold arguments or something? Shouldn't we preserve them and restore them from the stack, like `x19` and `x20` on arm64?

Francis McCabe

Acknowledged

Line 1830, Patchset 17: __ jmp(&done_prepare);
Thibaud Michaud . unresolved

Why not just fall through?

Francis McCabe

At this point, if the stack has shrunk, both rbp and rsp have been calculated based on shrinking.

File src/wasm/baseline/ia32/liftoff-assembler-ia32-inl.h
Line 251, Patchset 17: if (v8_flags.wasm_growable_stacks) {
Thibaud Michaud . resolved

Same for Liftoff: let's add a comment to explain why we need to shrink here.

Francis McCabe

Acknowledged

File test/mjsunit/regress/wasm/regress-520400061.js
Line 5, Patchset 17:// Flags: --experimental-wasm-wasmfx
// Flags: --allow-natives-syntax --fuzzing --future --wasm-staging
// Flags: --wasm-stack-switching-stack-size=291 --experimental-wasm-growable-stacks
// Flags: --stress-wasm-stack-switching --experimental-wasm-wide-arithmetic
Thibaud Michaud . resolved

FYI: we are dropping the `--experimental` prefix from wasm feature flags. We have aliases at the moment so users can migrate smoothly. But we should use `--wasm-<feature>` from now on.

Francis McCabe

Acknowledged

File test/mjsunit/regress/wasm/regress-520827581.js
Line 5, Patchset 17:// Flags: --allow-natives-syntax --experimental-wasm-growable-stacks --expose-gc --trace-wasm-stack-switching
Thibaud Michaud . resolved

The tracing flag is not needed for the test and will just slow it down.

Francis McCabe

Acknowledged

Line 5, Patchset 17:// Flags: --allow-natives-syntax --experimental-wasm-growable-stacks --expose-gc --trace-wasm-stack-switching
Thibaud Michaud . resolved

same here: `--wasm-growable-stacks`

Francis McCabe

Acknowledged

Line 18, Patchset 17: .addLocals(kWasmI32, 10000)
Thibaud Michaud . resolved

We need something more reliable for Turboshaft, the locals will be eliminated if they are unused.

Francis McCabe

We have several tests now.

Open in Gerrit

Related details

Attention is currently required from:
  • Ryan Diaz
  • Thibaud Michaud
Submit Requirements:
  • requirement is not satisfiedCode-Owners
  • requirement is not satisfiedCode-Review
  • requirement is not satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: v8/v8
Gerrit-Branch: main
Gerrit-Change-Id: Idad7ac0dab8190a1caf4ece677e8477b75ec02b2
Gerrit-Change-Number: 7926084
Gerrit-PatchSet: 43
Gerrit-Owner: Ryan Diaz <ryan...@chromium.org>
Gerrit-Reviewer: Francis McCabe <f...@chromium.org>
Gerrit-Reviewer: Ryan Diaz <ryan...@chromium.org>
Gerrit-Reviewer: Thibaud Michaud <thib...@chromium.org>
Gerrit-CC: Code Review Nudger <android-build...@prod.google.com>
Gerrit-Attention: Thibaud Michaud <thib...@chromium.org>
Gerrit-Attention: Ryan Diaz <ryan...@chromium.org>
Gerrit-Comment-Date: Wed, 12 Aug 2026 22:21:39 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
Comment-In-Reply-To: Thibaud Michaud <thib...@chromium.org>
unsatisfied_requirement
open
diffy

Thibaud Michaud (Gerrit)

unread,
Aug 13, 2026, 11:30:00 AM (2 days ago) Aug 13
to Ryan Diaz, Francis McCabe, Code Review Nudger, android-bu...@system.gserviceaccount.com, v8-s...@luci-project-accounts.iam.gserviceaccount.com, v8-ppc...@googlegroups.com, v8-mip...@googlegroups.com, v8-risc...@chromium.org, dmercadi...@chromium.org, v8-re...@googlegroups.com, was...@google.com
Attention needed from Francis McCabe and Ryan Diaz

Thibaud Michaud added 16 comments

Patchset-level comments
Thibaud Michaud . resolved

I didn't finish the review. I mostly reviewed the x64 implementation so far, but I'm sending you my first round of comments so you can work on it asynchronously.

Commit Message
Line 17, Patchset 43 (Latest):Bug: 520400061
Thibaud Michaud . unresolved

nit: let's use the canonical bug ID, 450597581.

File src/compiler/backend/arm/code-generator-arm.cc
Line 587, Patchset 43 (Latest):#if V8_ENABLE_WEBASSEMBLY
if (v8_flags.wasm_growable_stacks &&
linkage()->GetIncomingDescriptor()->IsAnyWasmFunctionCall()) {
return;
}
#endif // V8_ENABLE_WEBASSEMBLY
Thibaud Michaud . unresolved

Why do we skip this? Please add a comment.

File src/compiler/backend/arm64/code-generator-arm64.cc
Line 857, Patchset 43 (Latest): int depth = 2;
#if V8_ENABLE_WEBASSEMBLY
ArchOpcode arch_opcode = ArchOpcodeField::decode(instr->opcode());
if (arch_opcode == kArchTailCallWasm ||
arch_opcode == kArchTailCallWasmIndirect) {
depth = 4;
}
#endif
Thibaud Michaud . unresolved

This looks error prone, it uses magic numbers and needs to be kept in sync with the instruction selector. See also comments below, the whole input encoding looks pretty fragile. Not really this change's fault, to be clear, but maybe we can limit the damage.

We could push the tail call inputs first in instruction-selector.cc so that they don't affect the existing (reverse) indices. This would just move the complexity of computing the correct indices to the tail-call inputs, but at least this reduces the risk of breaking existing code.

I don't have anything much better to suggest. Maybe this needs a larger refactoring that is out of scope for this change. But if you can think of any DCHECK or anything to improve the situation while you are here, please don't hesitate.

Line 1022, Patchset 17: if (v8_flags.wasm_growable_stacks) {
Thibaud Michaud . unresolved

Please add a high-level comment to explain why we need to shrink before the tail call.

Francis McCabe

Acknowledged

Thibaud Michaud

The comment just says that we need to shrink, it still doesn't explain *why*.

File src/compiler/backend/instruction.h
Line 1131, Patchset 43 (Latest): return InputCount() - 3;
Thibaud Michaud . unresolved

Doesn't this also need to be updated? This is pushed before the new optional tail call inputs in instruction-selector.cc, so its index is also shifted.

Line 1115, Patchset 43 (Latest): return InputCount() - 5;
Thibaud Michaud . unresolved

In instruction-selector.cc, the new tail call inputs are only pushed if the callee is a wasm function. Don't we need something similar here to keep the indices in sync?

File src/compiler/backend/x64/code-generator-x64.cc
Line 1771, Patchset 43 (Latest): // Save return address of w1's caller (from rsp + 16 because we pushed 2
// words) and Segment 1 rsp.
__ movq(r15, Operand(rsp, 2 * kSystemPointerSize));
Thibaud Michaud . unresolved

I don't understand: at [rsp+16], we will find whatever was at the top of the current frame before pushing r12 and r15, not the return address?
The return address should be at an FP-relative offset.

Line 1776, Patchset 43 (Latest): // Save GP caller-saved registers.
__ pushq(rax);
__ pushq(rcx);
__ pushq(rdx);
__ pushq(rsi);
__ pushq(rdi);
__ pushq(r8);
__ pushq(r9);
__ pushq(r10);
__ pushq(r11);

// Save FP registers.
__ subq(rsp, Immediate(128));
for (int idx = 0; idx < 8; ++idx) {
__ movdqu(Operand(rsp, idx * 16), XMMRegister::from_code(idx + 1));
}
Thibaud Michaud . unresolved

There's a `PushCallerSaved`/`PopCallerSaved` helper in the x64 macro assembler, can you use it here?

Line 1830, Patchset 17: __ jmp(&done_prepare);
Thibaud Michaud . resolved

Why not just fall through?

Francis McCabe

At this point, if the stack has shrunk, both rbp and rsp have been calculated based on shrinking.

Thibaud Michaud

Yeah, it makes sense after your latest changes.
In the old version, the label was bound immediately after the jump so it didn't actually skip anything.

File src/wasm/baseline/ia32/liftoff-assembler-ia32-inl.h
Line 251, Patchset 17: if (v8_flags.wasm_growable_stacks) {
Thibaud Michaud . unresolved

Same for Liftoff: let's add a comment to explain why we need to shrink here.

Francis McCabe

Acknowledged

Thibaud Michaud

Same here, the comment still doesn't explain the *why*.
I insist on it because it's not obvious at all.

File src/wasm/wasm-external-refs.cc
Line 1490, Patchset 43 (Latest): // stack segments. Checking isolate->IsOnCentralStack() instead of
// active_stack->jmpbuf()->parent == nullptr ensures that growable stacks work
// correctly even when invoked without stack switching.
Thibaud Michaud . unresolved

This comment is a bit confusing:

  • It refers to old code (`...->parent == nullptr`) that won't be visible to future readers,
  • "ensures that growable stacks work correctly (...) without stack switching". We should not even be using growable stacks without stack switching. So saying that they "work correctly" is odd, and the check we do here is irrelevant.

The actual difference between the old and the new check is that `...->parent == nullptr` checks that we are on the root of the stack chain, while `IsOnCentralStack()` would also return true if we are in a wasm stack that temporarily switched to the central stack for an external call.

Both should hold, so we can keep the new (stronger) check. And we should even turn it into a `CHECK`, we should never even call that function from the central stack.

File test/mjsunit/regress/wasm/regress-520827581.js
Line 59, Patchset 17: let res = await wrapper(101);
Thibaud Michaud . unresolved

This magic number didn't work for me, and it is probably very platform and tier dependent. Given the complexity of the change, I think it's worth having more robust tests.

Thibaud Michaud

I'm still not convinced by this test. It expects the stack growth to happen at a specific stack depth that will probably be different depending on the tier, platform, etc.
I sent you a test that reliably crashes by doing a tail-call at each stack depth. It looks like you have added it as "variant3" below. Does regress-520827581.js cover a different code path? If yes, can we make it more robust in a similar way? If not, let's remove it.
Same question for variant 1 and 2.

File test/mjsunit/wasm/test-growable-tail-call-variant1.js
Line 5, Patchset 43 (Latest):// Flags: --wasm-growable-stacks --no-liftoff --no-wasm-inlining --experimental-wasm-wasmfx --allow-natives-syntax
Thibaud Michaud . unresolved

Running with and without Liftoff is already covered by the test variants on the CI. Best to drop this flag to avoid conflicting flags and increase coverage. Same for the other tests.

Line 37, Patchset 43 (Latest):f_grow.addLocals(kWasmF64, 600);
Thibaud Michaud . unresolved

Given that you have `--no-liftoff` above, aren't these locals optimized away?

As mentioned above, the runner should run it with and without Liftoff, so maybe it helps with Liftoff. But I'm wondering how you have been running it locally, and if these locals made any difference.

Same question for variant 2.

File test/mjsunit/wasm/test-growable-tail-call-variant3.js
Line 50, Patchset 43 (Latest): let depth = 60;
Thibaud Michaud . unresolved

60 is not enough on my config to hit the stack growth.
I set it back to 200 like in my original test, and it still segfaults after the change. Maybe because of the return address issue I mentioned in the comment above. Can you reproduce it?

Open in Gerrit

Related details

Attention is currently required from:
  • Francis McCabe
  • Ryan Diaz
Submit Requirements:
  • requirement is not satisfiedCode-Owners
  • requirement is not satisfiedCode-Review
  • requirement is not satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: v8/v8
Gerrit-Branch: main
Gerrit-Change-Id: Idad7ac0dab8190a1caf4ece677e8477b75ec02b2
Gerrit-Change-Number: 7926084
Gerrit-PatchSet: 43
Gerrit-Owner: Ryan Diaz <ryan...@chromium.org>
Gerrit-Reviewer: Francis McCabe <f...@chromium.org>
Gerrit-Reviewer: Ryan Diaz <ryan...@chromium.org>
Gerrit-Reviewer: Thibaud Michaud <thib...@chromium.org>
Gerrit-CC: Code Review Nudger <android-build...@prod.google.com>
Gerrit-Attention: Ryan Diaz <ryan...@chromium.org>
Gerrit-Attention: Francis McCabe <f...@chromium.org>
Gerrit-Comment-Date: Thu, 13 Aug 2026 15:29:56 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
Comment-In-Reply-To: Thibaud Michaud <thib...@chromium.org>
Comment-In-Reply-To: Francis McCabe <f...@chromium.org>
unsatisfied_requirement
open
diffy

Thibaud Michaud (Gerrit)

unread,
Aug 14, 2026, 12:05:38 PM (yesterday) Aug 14
to Ryan Diaz, Francis McCabe, Code Review Nudger, android-bu...@system.gserviceaccount.com, v8-s...@luci-project-accounts.iam.gserviceaccount.com, v8-ppc...@googlegroups.com, v8-mip...@googlegroups.com, v8-risc...@chromium.org, dmercadi...@chromium.org, v8-re...@googlegroups.com, was...@google.com
Attention needed from Francis McCabe and Ryan Diaz

Thibaud Michaud added 14 comments

Patchset-level comments
File-level comment, Patchset 49 (Latest):
Thibaud Michaud . resolved

Another round of comments before I log out for the week-end. Mostly reviewed ia32 this time, but some comments apply to all platforms.

File src/compiler/backend/arm/code-generator-arm.cc
Line 844, Patchset 49 (Latest): __ ldr(scratch, MemOperand(sp, (idx + 1) * kSystemPointerSize));
Thibaud Michaud . unresolved

Where does this `+ 1` come from? I would expect the stack parameters to be directly under `sp`.

Line 845, Patchset 49 (Latest): __ str(scratch, MemOperand(fp, (idx + 2 - stack_param_delta) *
Thibaud Michaud . unresolved

And if I understand the meaning of this `+ 2` correctly, I would suggest the following to make it clearer:

```suggestion
__ str(scratch, MemOperand(fp, kCallerSPOffset + (idx - stack_param_delta) *
```

(also on other platforms).

Line 851, Patchset 49 (Latest): __ ldr(r1, MemOperand(fp, 0));
Thibaud Michaud . unresolved

Doesn't that clobber r1? What if it holds a register parameter?
See also comment below about testing tail-call parameters.

File src/compiler/backend/ia32/code-generator-ia32.cc
Line 840, Patchset 49 (Latest): __ mov(Operand(eax, (1 - stack_param_delta) * kSystemPointerSize), edi);
Thibaud Michaud . unresolved
```suggestion
__ mov(Operand(eax, kCallerPCOffset - stack_param_delta * kSystemPointerSize), edi);
```
Line 844, Patchset 49 (Latest): __ mov(edi, Operand(esi, (idx + 3) * kSystemPointerSize));
Thibaud Michaud . unresolved

Why `+ 3`? We push `esi` and `edi` before saving the `sp` so I would only expect `+ 2`.

Line 846, Patchset 49 (Latest): Operand(eax, (idx + 2 - stack_param_delta) * kSystemPointerSize),
Thibaud Michaud . unresolved
```suggestion
Operand(eax, kCallerSPOffset + (idx - stack_param_delta) * kSystemPointerSize),
```
Line 851, Patchset 49 (Latest): __ lea(esp, Operand(eax, (1 - stack_param_delta) * kSystemPointerSize));
Thibaud Michaud . unresolved
```suggestion
__ lea(esp, Operand(eax, kCallerPCOffset - stack_param_delta * kSystemPointerSize));
```
Line 858, Patchset 49 (Latest): __ bind(&no_shrink);
Thibaud Michaud . unresolved

Should we be jumping to `done_prepare` here like on other platforms? The label is unused at the moment.

Line 859, Patchset 49 (Latest): if (frame_access_state()->has_frame()) {
Thibaud Michaud . unresolved

Don't we already assume that the function has a frame from the very start of this code, by checking the frame type marker, and then by using the caller FP slot, etc.?

I think we need to check that at the very beginning. And if the code has no frame, then it should be safe to assume that we are not at a segment start, and we can skip the whole thing. A function with no frame does not have a stack check, so it cannot have grown the stack.

Line 861, Patchset 49 (Latest): Operand(ebp, (1 - stack_param_delta) * kSystemPointerSize));
Thibaud Michaud . unresolved
```suggestion
Operand(ebp, kCallerPCOffset - stack_param_delta * kSystemPointerSize));
```
File src/wasm/baseline/arm64/liftoff-assembler-arm64-inl.h
Line 288, Patchset 49 (Latest): LiftoffRegList regs_to_save = cache_state()->used_registers;
Thibaud Michaud . unresolved

This is the only platform and tier where we save/restore these additional registers on top of kGpParamRegisters. Is there a reason for it?

File test/mjsunit/wasm/test-growable-tail-call-variant1.js
Line 13, Patchset 49 (Latest):const sig1 = builder.addType(makeSig([kWasmI32, kWasmF64, kWasmI32], returns1));
Thibaud Michaud . unresolved

Related to the comment above about a clobbered register on arm: this test has parameters, but it doesn't seem to use them. It would be nice to have at least one test that uses parameters to check that they don't get clobbered during the new tail-call sequence. This could also be added to variant3, which already checks multi-return stack parameters.

Line 37, Patchset 43:f_grow.addLocals(kWasmF64, 600);
Thibaud Michaud . unresolved

Given that you have `--no-liftoff` above, aren't these locals optimized away?

As mentioned above, the runner should run it with and without Liftoff, so maybe it helps with Liftoff. But I'm wondering how you have been running it locally, and if these locals made any difference.

Same question for variant 2.

Thibaud Michaud

BTW: maybe we can force a large frame on Turboshaft by using many parameters instead of many locals.
Until recently, this wouldn't have worked because the stack checks didn't take outgoing stack parameters into account. But this has been fixed now, so if a function has many parameters, this can easily trigger a stack growth for the *caller*.

Open in Gerrit

Related details

Attention is currently required from:
  • Francis McCabe
  • Ryan Diaz
Submit Requirements:
  • requirement is not satisfiedCode-Owners
  • requirement is not satisfiedCode-Review
  • requirement is not satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: v8/v8
Gerrit-Branch: main
Gerrit-Change-Id: Idad7ac0dab8190a1caf4ece677e8477b75ec02b2
Gerrit-Change-Number: 7926084
Gerrit-PatchSet: 49
Gerrit-Owner: Ryan Diaz <ryan...@chromium.org>
Gerrit-Reviewer: Francis McCabe <f...@chromium.org>
Gerrit-Reviewer: Ryan Diaz <ryan...@chromium.org>
Gerrit-Reviewer: Thibaud Michaud <thib...@chromium.org>
Gerrit-CC: Code Review Nudger <android-build...@prod.google.com>
Gerrit-Attention: Ryan Diaz <ryan...@chromium.org>
Gerrit-Attention: Francis McCabe <f...@chromium.org>
Gerrit-Comment-Date: Fri, 14 Aug 2026 16:05:31 +0000
unsatisfied_requirement
open
diffy
Reply all
Reply to author
Forward
0 new messages