libxev is a cross-platform, high-performance event loop that provides abstractions for non-blocking IO, timers, events, and more and works on Linux (io_uring or epoll), macOS (kqueue), and Wasm + WASI. Available as both a Zig and C API.
by mitchellhZig
Last 12 weeks · 0 commits
2 of 6 standards met
Three fixes for the kqueue backend, each with a reproducing test: 1. now flushes pending EV_DELETE changes before returning. When a disarm occurs in the event-processing path, the EV_DELETE was queued but exited before flushing it to the kernel, leaving a stale filter with a dangling udata pointer. Related to PR #209. 2. Completions that return without kqueue registration (close, shutdown, no-threadpool fallback) now set state to before re-entering the submissions queue. Previously the stale state caused to route through (cancellation) instead of (resubmit). Related to PR #170. 3. Defensive before pushing kevent-returned completions to the completions queue in . Prevents the queue.zig assertion failure when a completion is present in multiple queues concurrently. Fixes issue #169. 10 tests added (all fail without their respective fixes): tick(0) EV_DELETE flush (6 tests including a synchronous reproduction of the race path via cross-pipe callback write) rearm state for non-kqueue completions (1 test) queue re-enqueue invariants (3 tests)
When a completion's fd is already ready at submission time, the call in returns its event, and the callback runs from the completion queue in . If that callback returns , the kqueue filter stays registered, but the completion is left in the state. Anything that relies on that state then misbehaves. For example, a cancel sees a finished completion and does nothing, so the callback keeps firing after the owner has cancelled it, and possibly freed it. Reproducer A pipe that is readable before the read is submitted. The callback rearms once, then the read is cancelled and more data is written: On (macOS): With this change: We hit this in a server whose accept and recv completions rearm. Under load, it showed up as log lines and callbacks running on completions that had already been cancelled. Fix In the completion-queue path of , a completion that was (still registered) and rearms is put back to , with its stale result cleared, instead of being left . Completions that were never registered keep the existing resubmit path. The same reproducer is added as a regression test. It fails on and passes here, and passes (60/60) on macOS. Relation to #224 #224 also touches the branch, but for the other case: completions that return without being registered with kqueue (close, shutdown, thread pool fallback). Those need so that starts them again. This PR covers completions that are still registered. The two changes are complementary; whichever lands second needs a small rebase on that line. Disclosure: the fix, the test and this description were written with AI assistance (Claude Code).
Summary The epoll backend's dispatch loop invokes a completion's callback for every kernel event it receives, without checking that the completion is still outstanding. The submit path already guards on state (, ), but dispatch does not. When a completion is retired — disarmed, cancelled, or already dispatched earlier in the same batch — while a stale event for its former registration is still in that batch, its callback runs a second time. Impact For a queued write this is fatal. 's generated callback () opens with: under a comment asserting the queue cannot be empty. On the second invocation the queue is empty, and in the is unchecked, producing a null dereference. This is the crash tracked in #234: Ghostty 1.3.1 pins and, with , SIGSEGVs on its IO thread at address (, ). The same build aborts at under . Fix Only completions are legitimately registered with epoll, so any other state in the event batch is stale and must be dropped: The change is deliberately minimal: it does not touch the active counter (that was already adjusted when the completion was retired) and leaves the path intact. Testing libxev's own test suite gives identical results with and without the patch, in both Debug and ReleaseSafe — i.e. no regression in the completion state machine (add → dispatch → rearm/disarm → cancel → cancel-after-complete) that the guard touches: The 4 skips are platform-gated ( checks: macOS/WASI/Windows and one x86_64 case), not failures, and are the same on both builds. Filtering to (94 matching tests) likewise gives 91/94 passed, 3 skipped, identically on stock and patched. This suite does not reproduce #234 on its own — both builds pass it. It is evidence of non-regression, not an independent reproduction. Ghostty reproducer (end-to-end) Built Ghostty 1.3.1 (ReleaseFast, , ) against libxev , stock vs. this patch, and ran the #234 reproducer (). Epoll was confirmed active in every run via . Stock failures are exactly the #234 crash — thread , , , and the faulting instruction which matches the epoll / layout described in #234. The reproducer is a race (stock fired in roughly 2 in 5 runs), so the table is a rate comparison rather than a deterministic reproduction. No missed wakeups, hangs, or other failures were observed in the patched runs. Limitations Does not address the fd-reuse case in #230, where a genuinely completion receives an event belonging to a previous registration. Assumes the completion's memory is still valid when the stale event is dispatched. That holds for the case but is not guaranteed in general; a full fix may need lifetime tracking rather than a state read. Relation to existing work Addresses #234, the epoll member of the completion-reuse class alongside #169, #227, and #224 (kqueue). #237's cancellation/dup-fd changes are complementary but, as noted in #234, insufficient on their own.
Repository: mitchellh/libxev. Description: libxev is a cross-platform, high-performance event loop that provides abstractions for non-blocking IO, timers, events, and more and works on Linux (io_uring or epoll), macOS (kqueue), and Wasm + WASI. Available as both a Zig and C API. Stars: 3582, Forks: 191. Primary language: Zig. Languages: Zig (98%), CSS (0.9%), C++ (0.6%), Nix (0.3%), JavaScript (0.1%). License: MIT. Topics: async, c, epoll, io-uring, kqueue, wasi, webassembly, zig. Open PRs: 26, open issues: 41. Last activity: 2mo ago. Community health: 42%. Top contributors: mitchellh, dependabot[bot], Corendos, charlesrocket, ianic, steeve, recursiveGecko, linuxy, kcbanner, rockorager and others.