# How the Ladybird‑WASM‑ESM‑Host‑Function‑RCE‑PoC Achieves Remote Code Execution

> Discover how the ladybird-wasm-esm-host-function-rce-poc exploits JavaScript host functions, Wasm registers, and Memory64 leaks for remote code execution in Ladybird's WebContent process.

- Repository: [bikini/exploitarium](https://github.com/bikini/exploitarium)
- Tags: deep-dive
- Published: 2026-09-06

---

**The PoC exploits a dangling `FunctionType` reference in JavaScript host functions, a stale Wasm register in the bytecode interpreter, and a Memory64 leak via `ImageData`/`WebGL` to achieve full native code execution in Ladybird's WebContent process.**

The `ladybird-wasm-esm-host-function-rce-poc` demonstrates a complete browser exploit chain against Ladybird's WebAssembly implementation. By chaining three independent primitives—a use‑after‑free in host‑function type handling, a register lifetime bug in the Wasm interpreter, and a type confusion in memory‑backed `ImageData`—the proof‑of‑concept turns a Wasm bug into arbitrary native code execution. This article breaks down each stage of the exploit with direct references to the source code in `bikini/exploitarium`.

---

## Vulnerability 1: Dangling `FunctionType` Reference in Host Functions

Ladybird's WebAssembly host function creation captures the `Wasm::FunctionType` by reference rather than by value. In [`WebAssembly.cpp`](https://github.com/bikini/exploitarium/blob/main/WebAssembly.cpp), when `Detail::create_host_function` is invoked, it stores `Wasm::FunctionType const& type` as a member of the generated host callback.

The ESM import path constructs this `FunctionType` as a stack‑local variable. Once the caller returns, the host callback retains a reference to now‑dead stack memory. When the callback later accesses `type.parameters()` or `type.results()`, it reads from freed stack space—classic use‑after‑free behavior.

From the PoC README **lines 59‑82**, this primitive manifests as:

```javascript
// Import a JS function with a static result type "arrayref"
WebAssembly.instantiateStreaming(fetch("module.wasm"), {
  env: { host_func: () => {} }   // host_func captures dangling type reference
});

```

The `host_func` callback, when later invoked from Wasm, dereferences the stale `FunctionType` pointer to determine expected argument and return types.

---

## Vulnerability 2: Stale Register + Unchecked GC Array Write

Ladybird's Wasm interpreter in [`BytecodeInterpreter.cpp`](https://github.com/bikini/exploitarium/blob/main/BytecodeInterpreter.cpp) fails to validate that the host function's actual return vector length matches the static `type.results()` length. When the stale `type.results()` indicates a non‑empty result but the host returns an empty vector, the destination register preserves its previous attacker‑controlled value.

This stale register becomes the "abstract reference" for a subsequent `array.set` operation. The interpreter performs only a null‑check on the reference's low bits before writing, trusting the value as a valid `Wasm::ArrayInstance` pointer.

From **lines 92‑104** of the README:

1. Host callback returns empty vector despite `type.results()` claiming `[arrayref]`
2. Interpreter writes stale register value into result slot
3. `array.set` interprets stale value as `ArrayInstance*`, enabling arbitrary address write

The attacker controls the stale register's value through prior Wasm execution, achieving a constrained but powerful write primitive.

---

## Vulnerability 3: Memory64 Leak via `ImageData` and WebGL

Ladybird's `ImageData` implementation wraps `Uint8ClampedArray` backing stores through `Bitmap::create_wrapper`, storing the raw pointer without ownership. When a Wasm Memory64 instance grows, its backing store reallocates to a new address, but existing `ImageData` objects continue pointing to the old region.

The PoC fills the abandoned memory region with JavaScript objects and Wasm GC objects, then uses `WebGL2RenderingContext.texImage2D` followed by `readPixels` to exfiltrate pointer‑shaped values.

From **lines 13‑36**:

```javascript
// Wrap a Uint8ClampedArray in ImageData
const img = new ImageData(new Uint8ClampedArray(buf), width, height);

// Old backing store pointer cached in ImageData
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);

// Memory64 grow moves backing store; img still points to old region
memory.grow(1);

// Fill old region with objects, then read back via WebGL
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, leakBuf);

```

This leak yields:
- `ArrayBuffer` object addresses
- Backing store pointers
- `Wasm::ArrayInstance` locations
- `liblagom-wasm` base address
- libc base via `__cxa_finalize@GLOB_DAT`

---

## Full Exploit Chain: From Wasm to Native Code Execution

The PoC combines these primitives into a complete RCE sequence (**lines 41‑49** of the README):

1. **Build fake `ArrayInstance`** — craft a structure whose element pointer targets attacker‑chosen native memory using leaked heap addresses.

2. **Trigger host‑function bug** — import JavaScript function returning `arrayref`, invoke with ESM path to activate dangling `FunctionType` reference.

3. **Force stale register** — return zero Wasm values from host callback, preserving attacker‑controlled register content.

4. **Arbitrary write via `array.set`** — use the stale register as fake `ArrayInstance*`, write through crafted element pointer.

5. **Retarget `DataView`** — modify a `DataView`'s backing store pointer to achieve arbitrary native read/write.

6. **Resolve library bases** — parse leaked pointers to locate `liblagom-wasm` and libc.

7. **Forge virtual dispatch table** — construct fake vtable and `setcontext` frame for stack pivot.

8. **Trigger WebGL virtual call** — execute controlled virtual function that ultimately runs:

```javascript
const commandBytes = new TextEncoder().encode("touch /tmp/ladybird_wasm_esm_rce\0");

```

The final WebGL call executes the marker command, proving native code execution within the Ladybird browser process.

---

## Key Source Files in bikini/exploitarium

| File | Purpose | Location |
|:---|:---|:---|
| [`poc.html`](https://github.com/bikini/exploitarium/blob/main/poc.html) | Standalone exploit driver containing payload, Wasm instantiation logic, and WebGL leak implementation | [`ladybird-wasm-esm-host-function-rce-poc/poc.html`](https://github.com/bikini/exploitarium/blob/main/ladybird-wasm-esm-host-function-rce-poc/poc.html) |
| [`README.md`](https://github.com/bikini/exploitarium/blob/main/README.md) | Complete reproduction notes, source code traces, and architectural vulnerability analysis | [`ladybird-wasm-esm-host-function-rce-poc/README.md`](https://github.com/bikini/exploitarium/blob/main/ladybird-wasm-esm-host-function-rce-poc/README.md) |

Both files are available at the repository root: `https://github.com/bikini/exploitarium/tree/main/ladybird-wasm-esm-host-function-rce-poc`

---

## Summary

- **Root cause**: Ladybird's [`WebAssembly.cpp`](https://github.com/bikini/exploitarium/blob/main/WebAssembly.cpp) captures `FunctionType` by reference in host functions, creating a use‑after‑free when the ESM import path uses stack‑allocated types.

- **Intermediate primitive**: The Wasm interpreter in [`BytecodeInterpreter.cpp`](https://github.com/bikini/exploitarium/blob/main/BytecodeInterpreter.cpp) fails to validate host return vector length, enabling stale register exploitation for type confusion.

- **Address leak**: Memory64 growth combined with `ImageData`/`Bitmap::create_wrapper` pointer caching creates a reliable infoleak via WebGL texture operations.

- **Final execution**: Fake `ArrayInstance` construction → arbitrary write → `DataView` backing store hijack → libc resolution → fake vtable + `setcontext` → WebGL virtual call hijacking.

- **Impact**: Full remote code execution in Ladybird's WebContent process, demonstrated by arbitrary file creation.

---

## Frequently Asked Questions

### How does the dangling `FunctionType` reference occur in Ladybird?

Ladybird's [`WebAssembly.cpp`](https://github.com/bikini/exploitarium/blob/main/WebAssembly.cpp) stores `Wasm::FunctionType const& type` as a reference in generated host callbacks. The ESM import path creates this `FunctionType` as a stack‑local variable. After the import call returns, the callback holds a dangling reference to freed stack memory. Subsequent accesses to `type.parameters()` or `type.results()` read stale or attacker‑controlled data.

### Why does the stale register enable arbitrary memory writes?

The Wasm interpreter trusts the length from `type.results()` without checking the actual returned vector length. When the host returns an empty vector but `type.results()` claims a non‑empty result, the destination register retains its previous value. This stale value becomes an "abstract reference" that passes the interpreter's null‑check, allowing `array.set` to write through an attacker‑controlled pointer.

### What makes the Memory64 `ImageData` leak reliable?

`Bitmap::create_wrapper` caches the raw pointer to a `Uint8ClampedArray` backing store without ownership semantics. Memory64 growth reallocates the backing store, but existing `ImageData` objects maintain the old pointer. By filling the abandoned region with objects and reading via `texImage2D`/`readPixels`, the attacker obtains pointer values with deterministic memory layouts.

### Can this exploit chain work without the WebGL component?

No—the WebGL leak is essential for defeating ASLR. The exploit requires leaked pointers to `liblagom-wasm` and libc bases to construct the fake virtual table and `setcontext` frame. Without address disclosure, the final arbitrary write primitive cannot be targeted to executable or writable regions necessary for code execution.