CVE-2026-6307 (Part 3): Escaping the V8 Sandbox Through WasmFX
CVE-2026-6307 (Part 3): Escaping the V8 Sandbox Through WasmFX
Authored by stratan
At this point, we are still inside the V8 heap sandbox. CVE-2026-6307 gives us addrof, fakeobj, and in-cage read/write.
For native code execution, we need a way out. We’ll use Chromium bug 502229895 for that step. WasmFX is still an experimental feature (--experimental-wasm-wasmfx) and thus safe for our demo, still present in V8 revision 09c9603016d80cc7a8ab9017e5ebbb57bc315941, and finally it’s an interesting upcoming feature/surface. More specifically, bug 502229895 is a WasmFX continuation type confusion. With one in-cage write, we can swap a continuation-reference field and make resume run with the wrong continuation signature. As we go through the sections, we’ll turn this into an out-of-cage (native) read/write, as well as code execution.
At this point, we are still inside the V8 heap sandbox. CVE-2026-6307 gives us addrof, fakeobj, and in-cage read/write.
For native code execution, we need a way out. We’ll use Chromium bug 502229895 for that step. WasmFX is still an experimental feature (--experimental-wasm-wasmfx) and thus safe for our demo, still present in V8 revision 09c9603016d80cc7a8ab9017e, and finally it’s an interesting upcoming feature/surface. More specifically, bug 502229895 is a WasmFX continuation type confusion. With one in-cage write, we can swap a continuation-reference field and make
5ebbb57bc315941resume run with the wrong continuation signature. As we go through the sections, we’ll turn this into an out-of-cage (native) read/write, as well as code execution.
Disclaimer
This article has been prepared for educational and informational purposes in the field of cybersecurity.
The vulnerability analyzed (CVE-2026-6307) was publicly identified, is known to the vendor, and has been fully patched in current versions of the affected software (fixed in revision 1b0cbb19f825, released on Tue Mar 31 11:05:12 2026).
The content is intended for information security professionals and does not constitute a functional exploitation guide nor does it provide tools to compromise third-party systems.
Tashita Software Security condemns any use of this information for unlawful purposes.
Unauthorized access to computer systems constitutes a criminal offense under the Spanish Penal Code.
A Short Detour Through WasmFX
WasmFX adds stack switching to WebAssembly. According to the stack-switching proposal:
This proposal adds typed stack-switching to WebAssembly, enabling a single WebAssembly instance to manage multiple execution stacks concurrently.
In simple terms, one Wasm instance can manage multiple execution stacks. That is useful for coroutines, async runtimes, generators, lightweight threads, and effect handlers.
The proposal’s star is the continuation:
A continuation represents a snapshot of execution on a particular stack.
A continuation can be created, resumed, and suspended again. resume continues a suspended stack. suspend returns control to the handler installed by the resume site.
For instance:
In V8, the WasmFX instructions are listed in wasm-opcodes.h:
V8 describes WasmContinuationObject as the “token used” for the suspend/resume flow:
The proposal defines cont.new as the instruction that turns a function reference into a suspended continuation:
V8 implements that instruction in the decoder. cont.new checks that WasmFX is enabled, pops the function reference, and pushes a continuation reference:
After decoding cont.new, the continuation needs its actual runtime state. Runtime_WasmAllocateContinuation allocates a wasm::StackMemory from the stack pool and creates a WasmStackObject that points to it:
The function signature decides the argument-buffer size. The buffer is placed at the top of the new stack and stored in StackMemory. Finally, the runtime marks the jump buffer as suspended and sets the initial PC to the stack-entry wrapper. So cont.new returns a continuation reference, but that reference has native stack state behind it.
The other instruction we care about is resume. The proposal gives its type like this:
and then states:
The
resumeinstruction is parameterised by a continuation type and a handler dispatch tablehdl.
So the continuation type $ct decides the arguments passed into the resumed computation (t1*) and the values produced when it returns (t2*).
resume reads the continuation type from the instruction, loads the matching function signature, pops those arguments, and pushes those returns:
The important part is that resume is typed by the instruction itself. A resume instruction names a continuation type. The decoder uses that type to decide which values must already be on the Wasm stack before the continuation reference, and which values the resume will return. For example, if the instruction resumes a continuation type whose function is i64 -> i64, the decoder pops one i64 argument before the continuation reference. If the instruction resumes a continuation type whose function is ref $Box -> i64, the decoder expects a ref $Box instead.
That is the detail bug 502229895 depends on. resume decides the argument layout from the continuation type written in the instruction. If a continuation reference is changed after validation, resume can accept values for one type while the resumed function reads them as another. More on that soon.
Continuations And Stack Memory
wasm::StackMemory is the native object used for a resumable Wasm stack. It holds the stack bounds, the saved jump buffer, the current continuation token, and the argument buffer used by resume. The relevant parts of StackMemory are:
The continuation reaches this native stack through WasmStackObject, which stores an external pointer to wasm::StackMemory:
The saved machine state lives in JumpBuffer:
The argument buffer is important for us. It is where resume arguments are stored. cont.new stores it in StackMemory, and resume loads it before entering WasmFXResume:
At this point, the object chain looks like this:
Triggering Suspend And Resume
Triggering Suspend And Resume
We enter the WasmFX resume path through an exported Wasm function. It takes a boxed continuation and an i64 value, reads the continuation reference from the box, then executes resume contA:
In WAT slang:
When V8 lowers that resume, CheckContAndGetStack checks the continuation and gets its StackMemory:
After that, Resume loads the stack’s argument buffer and calls the WasmFXResume builtin:
The builtin is where stack switching actually happens. WasmFXResume receives the target StackMemory and the argument buffer, switches to that stack, then loads the saved jump buffer:
Our exported Wasm function reaches WasmFXResume through resume contA. Because contA has an i64 -> i64 signature, the value before the continuation reference is accepted as an i64. The part to remember is that resume gets us into native stack-switching code with a StackMemory pointer. That StackMemory also carries the saved jump buffer, and later we use out-of-cage write to replace jmpbuf_.pc.
The Missing Signature Check
The Missing Signature Check
The previous section showed that resume checks whether the continuation reference is the current continuation for the target StackMemory. In the vulnerable build, that was the main runtime check:
That check only proves that the continuation belongs to this stack. It does not prove that the stack has the signature expected by this resume instruction. The patch for Chromium bug 502229895 adds that missing check. First, StackMemory gets a signature_hash_ field:
Then CheckContAndGetStack takes the expected continuation type, loads the stack’s stored signature hash, and traps if the two do not match:
Our vulnerable 09c960 build has no signature_hash_ field and no hash comparison. That’s the bug. A swapped continuation reference can still pass the “current continuation” check, even when its signature does not match the resume instruction.
Using 6307 For The Swap
Using 6307 For The Swap
The next step is to craft the resume instruction and continuation reference mismatch. Our exported function is resume_contA(boxA, value). The instruction inside it is resume contA, so value is used as the i64 argument for contA. The continuation itself is loaded from boxA.field0:
When boxA is created normally, boxA.field0 contains contA. We also create boxB, whose field 0 contains contB. The only thing we need from CVE-2026-6307 is an in-cage write that copies boxB.field0 into boxA.field0:
After the write, we still execute resume contA. What changed is the continuation reference loaded from boxA.field0. It now points to contB, so the argument that entered as i64 is received as ref boxC. This means we can pass an address-like i64, then have the resumed function use it as the base object for a WasmGC field access. From there, the WasmFX bug takes care of the rest.
Out-of-Cage Read/Write
Out-of-Cage Read/Write
Equipped with the continuation swap, we can now build out-of-cage read/write. For the read primitive, we create two continuation types with the same return type and different parameter types:
In WAT slang:
contA takes an i64. contB takes a ref boxC. CVE-2026-6307 swaps the continuation field so that a resume contA call reaches contB instead. The value accepted as an i64 by resume is then read as ref boxC by the resumed function.
Before the resume call, we adjust the address so field 0 lands exactly where we want:
The read call creates new continuations, swaps the field, and enters resume_contA:
After the continuation swap, resume_contA still passes the first argument as i64, but the resumed function is contB‘s fB. It receives that same value as ref boxC and runs struct.get boxC, 0, which reads from the chosen address.
For write, we do the same thing with one extra argument. The resumed function performs struct.set instead of struct.get:
In WAT slang:
The write call also creates a new pair, performs the same field swap, and enters resume_contA with one extra i64 value:
After the swap, contB‘s fB receives the first argument as ref boxD and the second as the i64 value to store. struct.set boxD, 0 writes that value to the chosen address.
That is the jump outside the cage. CVE-2026-6307 sets up the WasmFX continuation type confusion by swapping one field inside the V8 heap. The actual out-of-cage read/write happens when the resumed WasmGC function uses our address-like value as the object for struct.get or struct.set.
Going After The Saved PC
Going After The Saved PC
Now we have out-of-cage write. The next question is where to write.
The clean target is the continuation’s own StackMemory. Earlier, WasmFXResume reached LoadJumpBuffer with load_pc=true. That function restores the saved frame pointer, loads the saved PC from the jump buffer, and branches to it:
So the final target is jmpbuf_.pc. Starting from the saved WasmContinuationObject, we walk the object chain we saw earlier: WasmContinuationObject -> WasmStackObject -> wasm::StackMemory. Once we have the native StackMemory address, we keep the stack switch valid and replace the saved PC:
If the stack already returned, V8 marks its jump buffer as Retired, meaning the stack is finished and the jump buffer is not valid for resume. The resume path expects the target stack to be Suspended, so we restore the state needed for the final resume:
The last step is another resume:
The path follows the usual stack-switching code, reaches LoadJumpBuffer, loads our jmpbuf_.pc, and branches to it:
The End
The End
We got into the cage through Wasm. We get out through Wasm too. With out-of-cage read/write, the road to code execution is known and documented.
CVE-2026-6307 (Part 3): Escaping the V8 Sandbox Through WasmFX
CVE-2026-6307 (Part 3): Escaping the V8 Sandbox Through WasmFX
Authored by stratan
At this point, we are still inside the V8 heap sandbox. CVE-2026-6307 gives us addrof, fakeobj, and in-cage read/write.
For native code execution, we need a way out. We’ll use Chromium bug 502229895 for that step. WasmFX is still an experimental feature (--experimental-wasm-wasmfx) and thus safe for our demo, still present in V8 revision 09c9603016d80cc7a8ab9017e5ebbb57bc315941, and finally it’s an interesting upcoming feature/surface. More specifically, bug 502229895 is a WasmFX continuation type confusion. With one in-cage write, we can swap a continuation-reference field and make resume run with the wrong continuation signature. As we go through the sections, we’ll turn this into an out-of-cage (native) read/write, as well as code execution.
At this point, we are still inside the V8 heap sandbox. CVE-2026-6307 gives us addrof, fakeobj, and in-cage read/write.
For native code execution, we need a way out. We’ll use Chromium bug 502229895 for that step. WasmFX is still an experimental feature (--experimental-wasm-wasmfx) and thus safe for our demo, still present in V8 revision 09c9603016d80cc7a8ab9017e, and finally it’s an interesting upcoming feature/surface. More specifically, bug 502229895 is a WasmFX continuation type confusion. With one in-cage write, we can swap a continuation-reference field and make
5ebbb57bc315941resume run with the wrong continuation signature. As we go through the sections, we’ll turn this into an out-of-cage (native) read/write, as well as code execution.
Disclaimer
This article has been prepared for educational and informational purposes in the field of cybersecurity.
The vulnerability analyzed (CVE-2026-6307) was publicly identified, is known to the vendor, and has been fully patched in current versions of the affected software (fixed in revision 1b0cbb19f825, released on Tue Mar 31 11:05:12 2026).
The content is intended for information security professionals and does not constitute a functional exploitation guide nor does it provide tools to compromise third-party systems.
Tashita Software Security condemns any use of this information for unlawful purposes.
Unauthorized access to computer systems constitutes a criminal offense under the Spanish Penal Code.
A Short Detour Through WasmFX
WasmFX adds stack switching to WebAssembly. According to the stack-switching proposal:
This proposal adds typed stack-switching to WebAssembly, enabling a single WebAssembly instance to manage multiple execution stacks concurrently.
In simple terms, one Wasm instance can manage multiple execution stacks. That is useful for coroutines, async runtimes, generators, lightweight threads, and effect handlers.
The proposal’s star is the continuation:
A continuation represents a snapshot of execution on a particular stack.
A continuation can be created, resumed, and suspended again. resume continues a suspended stack. suspend returns control to the handler installed by the resume site.
For instance:
In V8, the WasmFX instructions are listed in wasm-opcodes.h:
V8 describes WasmContinuationObject as the “token used” for the suspend/resume flow:
The proposal defines cont.new as the instruction that turns a function reference into a suspended continuation:
V8 implements that instruction in the decoder. cont.new checks that WasmFX is enabled, pops the function reference, and pushes a continuation reference:
After decoding cont.new, the continuation needs its actual runtime state. Runtime_WasmAllocateContinuation allocates a wasm::StackMemory from the stack pool and creates a WasmStackObject that points to it:
The function signature decides the argument-buffer size. The buffer is placed at the top of the new stack and stored in StackMemory. Finally, the runtime marks the jump buffer as suspended and sets the initial PC to the stack-entry wrapper. So cont.new returns a continuation reference, but that reference has native stack state behind it.
The other instruction we care about is resume. The proposal gives its type like this:
and then states:
The
resumeinstruction is parameterised by a continuation type and a handler dispatch tablehdl.
So the continuation type $ct decides the arguments passed into the resumed computation (t1*) and the values produced when it returns (t2*).
resume reads the continuation type from the instruction, loads the matching function signature, pops those arguments, and pushes those returns:
The important part is that resume is typed by the instruction itself. A resume instruction names a continuation type. The decoder uses that type to decide which values must already be on the Wasm stack before the continuation reference, and which values the resume will return. For example, if the instruction resumes a continuation type whose function is i64 -> i64, the decoder pops one i64 argument before the continuation reference. If the instruction resumes a continuation type whose function is ref $Box -> i64, the decoder expects a ref $Box instead.
That is the detail bug 502229895 depends on. resume decides the argument layout from the continuation type written in the instruction. If a continuation reference is changed after validation, resume can accept values for one type while the resumed function reads them as another. More on that soon.
Continuations And Stack Memory
wasm::StackMemory is the native object used for a resumable Wasm stack. It holds the stack bounds, the saved jump buffer, the current continuation token, and the argument buffer used by resume. The relevant parts of StackMemory are:
The continuation reaches this native stack through WasmStackObject, which stores an external pointer to wasm::StackMemory:
The saved machine state lives in JumpBuffer:
The argument buffer is important for us. It is where resume arguments are stored. cont.new stores it in StackMemory, and resume loads it before entering WasmFXResume:
At this point, the object chain looks like this:
Triggering Suspend And Resume
Triggering Suspend And Resume
We enter the WasmFX resume path through an exported Wasm function. It takes a boxed continuation and an i64 value, reads the continuation reference from the box, then executes resume contA:
In WAT slang:
When V8 lowers that resume, CheckContAndGetStack checks the continuation and gets its StackMemory:
After that, Resume loads the stack’s argument buffer and calls the WasmFXResume builtin:
The builtin is where stack switching actually happens. WasmFXResume receives the target StackMemory and the argument buffer, switches to that stack, then loads the saved jump buffer:
Our exported Wasm function reaches WasmFXResume through resume contA. Because contA has an i64 -> i64 signature, the value before the continuation reference is accepted as an i64. The part to remember is that resume gets us into native stack-switching code with a StackMemory pointer. That StackMemory also carries the saved jump buffer, and later we use out-of-cage write to replace jmpbuf_.pc.
The Missing Signature Check
The Missing Signature Check
The previous section showed that resume checks whether the continuation reference is the current continuation for the target StackMemory. In the vulnerable build, that was the main runtime check:
That check only proves that the continuation belongs to this stack. It does not prove that the stack has the signature expected by this resume instruction. The patch for Chromium bug 502229895 adds that missing check. First, StackMemory gets a signature_hash_ field:
Then CheckContAndGetStack takes the expected continuation type, loads the stack’s stored signature hash, and traps if the two do not match:
Our vulnerable 09c960 build has no signature_hash_ field and no hash comparison. That’s the bug. A swapped continuation reference can still pass the “current continuation” check, even when its signature does not match the resume instruction.
Using 6307 For The Swap
Using 6307 For The Swap
The next step is to craft the resume instruction and continuation reference mismatch. Our exported function is resume_contA(boxA, value). The instruction inside it is resume contA, so value is used as the i64 argument for contA. The continuation itself is loaded from boxA.field0:
When boxA is created normally, boxA.field0 contains contA. We also create boxB, whose field 0 contains contB. The only thing we need from CVE-2026-6307 is an in-cage write that copies boxB.field0 into boxA.field0:
After the write, we still execute resume contA. What changed is the continuation reference loaded from boxA.field0. It now points to contB, so the argument that entered as i64 is received as ref boxC. This means we can pass an address-like i64, then have the resumed function use it as the base object for a WasmGC field access. From there, the WasmFX bug takes care of the rest.
Out-of-Cage Read/Write
Out-of-Cage Read/Write
Equipped with the continuation swap, we can now build out-of-cage read/write. For the read primitive, we create two continuation types with the same return type and different parameter types:
In WAT slang:
contA takes an i64. contB takes a ref boxC. CVE-2026-6307 swaps the continuation field so that a resume contA call reaches contB instead. The value accepted as an i64 by resume is then read as ref boxC by the resumed function.
Before the resume call, we adjust the address so field 0 lands exactly where we want:
The read call creates new continuations, swaps the field, and enters resume_contA:
After the continuation swap, resume_contA still passes the first argument as i64, but the resumed function is contB‘s fB. It receives that same value as ref boxC and runs struct.get boxC, 0, which reads from the chosen address.
For write, we do the same thing with one extra argument. The resumed function performs struct.set instead of struct.get:
In WAT slang:
The write call also creates a new pair, performs the same field swap, and enters resume_contA with one extra i64 value:
After the swap, contB‘s fB receives the first argument as ref boxD and the second as the i64 value to store. struct.set boxD, 0 writes that value to the chosen address.
That is the jump outside the cage. CVE-2026-6307 sets up the WasmFX continuation type confusion by swapping one field inside the V8 heap. The actual out-of-cage read/write happens when the resumed WasmGC function uses our address-like value as the object for struct.get or struct.set.
Going After The Saved PC
Going After The Saved PC
Now we have out-of-cage write. The next question is where to write.
The clean target is the continuation’s own StackMemory. Earlier, WasmFXResume reached LoadJumpBuffer with load_pc=true. That function restores the saved frame pointer, loads the saved PC from the jump buffer, and branches to it:
So the final target is jmpbuf_.pc. Starting from the saved WasmContinuationObject, we walk the object chain we saw earlier: WasmContinuationObject -> WasmStackObject -> wasm::StackMemory. Once we have the native StackMemory address, we keep the stack switch valid and replace the saved PC:
If the stack already returned, V8 marks its jump buffer as Retired, meaning the stack is finished and the jump buffer is not valid for resume. The resume path expects the target stack to be Suspended, so we restore the state needed for the final resume:
The last step is another resume:
The path follows the usual stack-switching code, reaches LoadJumpBuffer, loads our jmpbuf_.pc, and branches to it:
The End
The End
We got into the cage through Wasm. We get out through Wasm too. With out-of-cage read/write, the road to code execution is known and documented.
