Commit Detail
Commit 945b89a
Fix use-after-free in drainingRead caused by premature Consumer destruction The pumpToImpl coroutine uses DrainingReader which calls `consumer->drainingRead()``. That call may trigger `onConsumerWantsData` -> `forcePull` -> `pull` callback -> synchronous close/error -> `deferTransitionTo<Closed>`. Previously, both `ValueReadable::drainingRead()` and the caller (`ReadableStreamJsController::drainingRead`) each called `beginOperation()`/`endOperation()` independently. The inner `endOperation()` in ValueReadable/ByteReadable fired the deferred state transition before the caller's `wrapDrainingRead` could set up `.then()` callbacks on the returned promise. Since those callbacks capture `this` (the Consumer), the transition destroyed the Consumer out from under them triggering the UAF. The fix removes `beginOperation()`/`endOperation()` from `ValueReadable::drainingRead()` and `ByteReadable::drainingRead()`, and moves the single `beginOperation()` call to before `consumer->drainingRead()` at each call site in `ReadableStreamJsController::drainingRead`. The matching `endOperation()` remains in the `.then()`/`.catch()` callbacks of `wrapDrainingRead`, ensuring the deferred state change only fires after the Consumer's this-capturing callbacks have already run. `js.tryCatch` wraps each call site for exception safety. The tests do not perfectly catch the UAF even with ASAN because the conditions are extremely timing-sensitive and I haven't yet found a way to reproduce the exact timing reliably in workerd.
Files changed
2 files changed~2 modified