Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **A stream write's completion wrote into a request its listener had freed, and the next write on that handle crashed** (#310). `io_pipe_write_cb` cleared `req->io` after notifying the handle's listeners. A listener that disposes the request inside the notify and submits the next write there, as true-async-server's log sink does, frees it at once, and the next request takes the same address; the store after the notify then cleared `io` on that in-flight request, and its completion dereferenced a NULL handle. The awaited `writev` completion had the same order. Both now detach the request before the notify. Evidence: `sapi/asynctest/tests/test_write_resubmit.c` (true-async/php-src) crashes 3 of 3 on `ef85610` and passes here; true-async-server `core/030` built with `--enable-http-server-test-hooks` crashed 2 of 2 and passes 5 of 5.

- **`stop()` on a listen event did not stop accepting** (#305). libuv has no call that undoes `uv_listen`, and `libuv_listen_stop` only dropped the event's reference count, so a stopped listener went on accepting and every connection reached its callbacks: true-async-server, which pauses its listeners at its connection cap, answered each connection that arrived during a pause with 503 instead of leaving it in the kernel backlog, 5 of 5 runs. A stopped listener now leaves the next connection unaccepted; libuv then stops watching the socket on Unix and posts no new AcceptEx on Windows, so the rest of the queue waits in the backlog. A later `start()` delivers the held connection on the next loop tick, never from inside `start()`, and a callback that pauses again during that delivery stops it. A Windows TCP listener posts one AcceptEx at a time instead of libuv's 32: libuv keeps completed accepts in a LIFO stack, so a pause that held several handed the newest over first, and on the server's Windows CI a keep-alive connection that overtook an older one kept it waiting past its 5 s timeout (`h1/064`). With one, the listener holds at most one connection and the rest wait in the kernel backlog in arrival order, and worker threads that share one socket get an even share; the cost is one accept per loop iteration, not measured. A failed `uv_accept` on Unix no longer leaves the socket unwatched: libuv restarts the watcher only after a successful one, and the listener now restarts it itself. Evidence: `sapi/asynctest/tests/test_listen_pause.c` in php-src, 5 of 5 cases red before and green after (3 of 3 runs); true-async-server's `h1/062`, `h1/063` and `h1/064` answer 503 before and pass 10 of 10 after.

- **`graceful_shutdown()` ignored its `$cancellationError` argument** (#302). The argument was parsed and dropped: `cancel_queued_coroutines()` built its own `AsyncCancellation("Graceful shutdown")` and every coroutine received that one, so a caller passing `new ServerShutdown('...')` could not tell its own shutdown from the default. The PHP function now hands the object to the scheduler, which cancels the coroutines with it and falls back to the default only when none was given; a shutdown already in progress keeps its own cancellation. Evidence: `tests/edge_cases/015-graceful_shutdown_custom_cancellation.phpt`, red before and green after.
Expand Down
15 changes: 9 additions & 6 deletions libuv_reactor.c
Original file line number Diff line number Diff line change
Expand Up @@ -5055,16 +5055,18 @@ static void io_pipe_write_cb(uv_write_t *write_request, int status)
GC_ADDREF(exc);
}

/* Detached before the notify, never after it: an awaiter that resumes later
* disposes the request once the handle may be closed and freed, and dispose
* reads req->io; a listener may dispose it inside the notify, and the next
* write it submits there can be allocated at the same address. */
req->io = NULL;

ZEND_ASYNC_CALLBACKS_NOTIFY(&io->base.event, &req->base, exc);

if (exc != NULL) {
OBJ_RELEASE(exc);
}

/* The awaiter disposes this request after a resume later than this callback,
* and dispose reads req->io — which by then may be closed and freed. */
req->io = NULL;

IF_EXCEPTION_STOP_REACTOR;
}

Expand Down Expand Up @@ -5114,9 +5116,10 @@ static void io_pipe_writev_cb(uv_write_t *write_request, int status)
/* No exception on the broadcast: every listener forwards one
* unconditionally and filters by result only otherwise, so it
* would wake the whole handle instead of the one that asked. */
ZEND_ASYNC_CALLBACKS_NOTIFY(&req->io->base.event, &req->base, NULL);
async_io_t *const io = req->io;

req->io = NULL; /* see io_pipe_write_cb: dispose runs after the handle may be gone */
req->io = NULL; /* before the notify, as in io_pipe_write_cb */
ZEND_ASYNC_CALLBACKS_NOTIFY(&io->base.event, &req->base, NULL);
}

IF_EXCEPTION_STOP_REACTOR;
Expand Down
Loading