Repository navigation
Conversation
7b28383 to
d3f340e
Compare
d3f340e to
d5ad9b9
Compare
d5ad9b9 to
d82dd52
Compare
|
Independent review of head What was checked1. Lifecycle of a waiter (select, poll, kqueue run locally with ASan; epoll from CI). I traced these paths: destroyed before the first Poll, after registration, after its event was collected, by a timer in the same iteration, from inside another waiter's resume (the
To test this I wrote a randomized model checker. It uses 6 socketpairs and random reader and writer waiters, with the frame registered in the poller freed on completion, so a second resume is a use-after-free. Random actions: destroy while suspended, replace, close with or without destroying the waiters, reopen (fd reuse), timer-driven and in-resume actions, and
2. epoll generation.
3. uring and iocp.
4. kqueue and poll.
5. Hot path. No map or unordered_map, and no per-event or per-change allocation.
6. Tests.
P3
|
|
Independent review of head The rebase onto cae2aef leaves the library part of this PR's diff unchanged. I compared the two heads with
Checked locally on d82dd52 (macOS, ASan): the build succeeds, P3 notes 1-5 from the previous review stand. P3 #3 ( |
|
Independent review of head Checked with
|
Problem
The poller could resume a coroutine handle that was stale: its frame already destroyed, or the coroutine already resumed for the same registration. This matches a SIGSEGV seen in an HTTP server on
TLoop<TEPoll>(a jump through a freed coroutine frame into glibc'smain_arena).ReadSome/WriteSome/Monitor,Accept,Connect) left their handle inChanges_,ReadyEvents_and the backend tables when the coroutine was destroyed while suspended:RemoveEvent(THandle)was a no-op.dup) does not remove its epoll registration, and the deferredEPOLL_CTL_DELfails withEBADF. Its events keep arriving with the olddata.fd, which may already belong to a new socket: the new waiter is resumed spuriously, or twice in one iteration when both files are ready, and the second resume hits a freed frame.InEvents_pointing pastFds_.size(); a later registration of that descriptor was written out of bounds and never polled (lost wakeup).Fix
TPollerBase::RemoveEvent(int fd, THandle h). It dropshfrom ready events and queues a cancellation (TEvent::CANCEL) that each backend turns (ResolveCancel) into a removal of only the directions still registered toh, so a newer waiter on a reused descriptor number keeps its registration. The old no-opRemoveEvent(THandle)stays as a deprecated overload.TLoopandRemoveEvent.epoll_dataand ignores events of older registrations.The hot path gains one branch per change and one compare per epoll event.
Tests
New tests in
tests/tests.cppfor select, poll, epoll and kqueue:test_destroyed_waiter_not_resumed: read and write waiters destroyed before the first poll, after registration, after being collected as ready, from a timer in the same iteration, and replaced by a new waiter.test_destroyed_accept_and_connect_not_resumed: the same forAcceptandConnect, plus aConnecttimeout in the iteration where the socket becomes writable (covered by theRemoveEvent(fd)from Report connect errors from Connect #141).test_reused_descriptor_wakes_new_waiter: the descriptor number is reused before the next poll, with and without the old file kept open elsewhere.test_reuse_after_closing_several: two registered descriptors closed in one iteration, then reused.test_hangup_wakes_reader_and_writer_once: a reader and a writer on one socket when the peer closes.test_short_connections: short and aborted connections, with accepted sockets kept open for a while as a forked child would; every complete request must get its response.On master with ASan:
test_destroyed_waiter_not_resumedtest_destroyed_accept_and_connect_not_resumedtest_reused_descriptor_wakes_new_waitertest_reuse_after_closing_severaltest_hangup_wakes_reader_and_writer_oncetest_short_connectionsFds_(epoll from Linux CI on the tests-only revision, kqueue on macOS, select and poll on both.) All pass with the fix.