You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
main/streams/plain_wrapper.c:1192 allocates the flock task struct on
the caller's stack, queues it to a libuv worker thread, then suspends:
```c
php_stdiop_flock_task_data_t flock_data = { .fd = fd, .operation = value };
zend_async_task_t *task = ZEND_ASYNC_NEW_TASK(php_stdiop_flock_task_run, &flock_data);
…
if (UNEXPECTED(!ZEND_ASYNC_SUSPEND())) { … } // ← can throw AsyncCancellation
…
if (flock_data.result == 0) { … }
```
If the coroutine is cancelled at ZEND_ASYNC_SUSPEND(), the function
unwinds and flock_data is gone. The worker thread that's running php_stdiop_flock_task_run (in libuv's thread pool) keeps writing into
the freed stack slot:
Heap-allocate the task data and let the task callback / dispose chain own
its lifetime, mirroring how other thread-pool tasks in ext/async do it.
Two viable shapes:
`emalloc(sizeof(flock_data))` + `efree` in a task->dispose hook —
the dispose runs after the worker callback completes regardless of
whether the originating coroutine was cancelled.
The caller can then check `flock_data->result` only after the suspend
returned not via cancellation; on cancellation, the task is detached
and dispose-when-done.
Found by
Drafting `fuzzy-tests/io/flock_chaos.feature` (#143). The two cancel-
mid-flock scenarios SEGV instantly on ASAN-ZTS; the two non-cancel
scenarios (lock contention, holder + waiter) pass clean. Filed before
shipping; the cancel scenarios stay commented under `# Blocked: #146`
in the feature.
Acceptance
Cancelling a coroutine parked in `flock()` does not corrupt memory; ASAN
runs clean. The cancel scenarios in `fuzzy-tests/io/flock_chaos.feature`
can be reinstated and run green on the fuzzy CI matrix.
Bug
main/streams/plain_wrapper.c:1192allocates the flock task struct onthe caller's stack, queues it to a libuv worker thread, then suspends:
```c
php_stdiop_flock_task_data_t flock_data = { .fd = fd, .operation = value };
zend_async_task_t *task = ZEND_ASYNC_NEW_TASK(php_stdiop_flock_task_run, &flock_data);
…
if (UNEXPECTED(!ZEND_ASYNC_SUSPEND())) { … } // ← can throw AsyncCancellation
…
if (flock_data.result == 0) { … }
```
If the coroutine is cancelled at
ZEND_ASYNC_SUSPEND(), the functionunwinds and
flock_datais gone. The worker thread that's runningphp_stdiop_flock_task_run(in libuv's thread pool) keeps writing intothe freed stack slot:
```c
static void php_stdiop_flock_task_run(zend_async_task_t *task) {
php_stdiop_flock_task_data_t *flock_data = (...) task->data;
flock_data->result = flock(flock_data->fd, flock_data->operation); // ← UAF
…
}
```
ASAN trace (chaos repro, scheduler-fifo / ASAN-ZTS)
Suggested fix
Heap-allocate the task data and let the task callback / dispose chain own
its lifetime, mirroring how other thread-pool tasks in
ext/asyncdo it.Two viable shapes:
task->disposehook —the dispose runs after the worker callback completes regardless of
whether the originating coroutine was cancelled.
CALLBACK_DONE/
DISPOSE_PENDINGflags" pattern referenced in async I/O: per-request completion events instead of broadcasting on the handle event #130). Same idea.The caller can then check `flock_data->result` only after the suspend
returned not via cancellation; on cancellation, the task is detached
and dispose-when-done.
Found by
Drafting `fuzzy-tests/io/flock_chaos.feature` (#143). The two cancel-
mid-flock scenarios SEGV instantly on ASAN-ZTS; the two non-cancel
scenarios (lock contention, holder + waiter) pass clean. Filed before
shipping; the cancel scenarios stay commented under `# Blocked: #146`
in the feature.
Acceptance
Cancelling a coroutine parked in `flock()` does not corrupt memory; ASAN
runs clean. The cancel scenarios in `fuzzy-tests/io/flock_chaos.feature`
can be reinstated and run green on the fuzzy CI matrix.