Files
bun.sh/test/regression/issue/atomics-waitasync-wtftimer-uaf.test.ts
robobun 8826b4f5f5 Fix WTFTimer issues with Atomics.waitAsync (#23442)
## Summary

Fixes two critical issues in `WTFTimer` when `Atomics.waitAsync` creates
multiple timer instances.

## Problems

### 1. Use-After-Free in `WTFTimer.fire()`

**Location:** `/workspace/bun/src/bun.js/api/Timer/WTFTimer.zig:70-82`

```zig
pub fn fire(this: *WTFTimer, _: *const bun.timespec, _: *VirtualMachine) EventLoopTimer.Arm {
    this.event_loop_timer.state = .FIRED;
    this.imminent.store(null, .seq_cst);
    this.runWithoutRemoving();  // ← Callback might destroy `this`
    return if (this.repeat)     // ← UAF: accessing freed memory
        .{ .rearm = this.event_loop_timer.next }
    else
        .disarm;
}
```

When `Atomics.waitAsync` creates a `DispatchTimer` with a timeout, the
timer fires and the callback destroys `this`, but we continue to access
it.

### 2. Imminent Pointer Corruption

**Location:** `/workspace/bun/src/bun.js/api/Timer/WTFTimer.zig:36-42`

```zig
pub fn update(this: *WTFTimer, seconds: f64, repeat: bool) void {
    // Multiple WTFTimers unconditionally overwrite the shared imminent pointer
    this.imminent.store(if (seconds == 0) this else null, .seq_cst);
    // ...
}
```

All `WTFTimer` instances share the same
`vm.eventLoop().imminent_gc_timer` atomic pointer. When multiple timers
are created (GC timer + Atomics.waitAsync timers), they stomp on each
other's imminent state.

## Solutions

### 1. UAF Fix

Read `this.repeat` and `this.event_loop_timer.next` **before** calling
`runWithoutRemoving()`:

```zig
const should_repeat = this.repeat;
const next_time = this.event_loop_timer.next;
this.runWithoutRemoving();
return if (should_repeat)
    .{ .rearm = next_time }
else
    .disarm;
```

### 2. Imminent Pointer Fix

Use compare-and-swap to only set imminent if it's null, and only clear
it if this timer was the one that set it:

```zig
if (seconds == 0) {
    _ = this.imminent.cmpxchgStrong(null, this, .seq_cst, .seq_cst);
    return;
} else {
    _ = this.imminent.cmpxchgStrong(this, null, .seq_cst, .seq_cst);
}
```

## Test Plan

Added regression test at
`test/regression/issue/atomics-waitasync-wtftimer-uaf.test.ts`:

```javascript
const buffer = new SharedArrayBuffer(16);
const view = new Int32Array(buffer);
Atomics.store(view, 0, 0);

const result = Atomics.waitAsync(view, 0, 0, 10);
setTimeout(() => {
  console.log("hi");
}, 100);
```

**Before:** Crashes with UAF under ASAN  
**After:** Runs cleanly

All existing atomics tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2025-10-10 03:47:38 -07:00

56 lines
1.9 KiB
TypeScript

// This test reproduces a UAF bug where Atomics.waitAsync creates a DispatchTimer
// which creates a new WTFTimer, violating Bun's assumption that there's only one WTFTimer per VM.
// The UAF occurs when the timer fires and continues to reference `this` after it's been freed.
import { expect, test } from "bun:test";
import { isWindows } from "harness";
test.todoIf(isWindows)("Atomics.waitAsync with setTimeout does not crash (UAF bug)", async () => {
// Run 2 times to trigger the UAF with ASAN
for (let i = 0; i < 2; i++) {
const buffer = new SharedArrayBuffer(16);
const view = new Int32Array(buffer);
Atomics.store(view, 0, 0);
const result = Atomics.waitAsync(view, 0, 0, 1); // 1ms timeout
expect(result.async).toBe(true);
expect(result.value).toBeInstanceOf(Promise);
// This setTimeout would trigger the UAF bug by creating another WTFTimer
const timeoutPromise = new Promise<string>(resolve => {
setTimeout(() => {
resolve("hi");
}, 5); // 5ms timeout
});
const [waitResult, timeoutResult] = await Promise.all([result.value, timeoutPromise]);
expect(waitResult).toBe("timed-out");
expect(timeoutResult).toBe("hi");
}
});
test.todoIf(isWindows)("Multiple Atomics.waitAsync calls do not crash", async () => {
const buffer = new SharedArrayBuffer(16);
const view = new Int32Array(buffer);
Atomics.store(view, 0, 0);
Atomics.store(view, 1, 0);
Atomics.store(view, 2, 0);
const result1 = Atomics.waitAsync(view, 0, 0, 10);
const result2 = Atomics.waitAsync(view, 1, 0, 20);
const result3 = Atomics.waitAsync(view, 2, 0, 30);
expect(result1.async).toBe(true);
expect(result2.async).toBe(true);
expect(result3.async).toBe(true);
const [r1, r2, r3] = await Promise.all([result1.value, result2.value, result3.value]);
expect(r1).toBe("timed-out");
expect(r2).toBe("timed-out");
expect(r3).toBe("timed-out");
});