mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
## 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>
56 lines
1.9 KiB
TypeScript
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");
|
|
});
|