Repository navigation
fix(inputs/dnstap): release disconnected clients and refuse a Unix socket another server uses - #806
Conversation
The TCP and Unix servers keyed each client's session by its fd and erased it from the close handler. By then the handle is closing and uv_fileno() fails, so uvw's fd() returned an uninitialized value: the session, and the client handle it holds, stayed in the map. An erase that happened to hit a live client's fd would drop that client's session, and its next read would dereference a null session. Sessions are now keyed by their client handle, which every event for the client receives. The stream also reports its connected clients in info_json as dnstap.clients, which the tests use to see that a disconnected client is released. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before binding, the Unix server removed whatever was at its socket path. A second dnstap input on the same path therefore unlinked the first server's socket and took its new clients, while the first kept running with no way to be reached. A regular file at the path was deleted too. The file is now removed only when it is a socket that refuses connections, which is what a server that is gone leaves behind. A socket a server still listens on makes the bind fail with "address already in use", and a path that is not a socket is refused and left alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…essions clean A frame stream session threw, and its client was dropped, whenever a socket read ended inside a frame's 4-byte length, or inside a control frame after its escape code had been consumed. Reads can end anywhere, so a busy client was eventually dropped. The session now waits for the rest of the frame and consumes a control frame only once all of it is here. The session's bidirectional flag had no initial value. Now that a disconnected client's session is freed, the next session can be built in its memory: on glibc it then started as bidirectional, and a unidirectional client's START was refused as out of order. The Unix socket probe now reports why a stale socket could not be removed, instead of the bind's "address already in use", and also probes a path that fills sun_path, which libuv binds without a NUL. The client tests move off port 5355, which systemd-resolved holds for LLMNR, and the refused second server's test checks the socket file was not replaced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… kept The stale-socket probe treated every connect error but "connection refused" as a live server, so a socket it could not connect to at all, such as one left by a server that ran as root, failed the start with the bind's "address already in use". That socket is still kept, since a server could be behind it, but the start now fails with the probe's own error. A live socket of another type (EPROTOTYPE) still counts as a server, and a file that went away between the check and the probe (ENOENT) leaves the path free for the bind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
|
/ai-review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
AI Code Review — risk tier: Advisory only. A human owns the merge decision. Summary — Net-positive fix: resolves the stale-session/fd-reuse bug, the Unix-socket-takeover bug, and the frame-reassembly drops, backed by tests that fail on develop and pass here. Two low-severity robustness notes below, neither blocking. Findings
🤖 AI Code Review · run · prompts: |
|
AI Code Review — risk tier: Advisory only. A human owns the merge decision. Summary — Net-positive fix: client sessions are now keyed by handle instead of fd, the Unix-socket takeover bug is closed with a proper stale-socket probe, frame-stream reads correctly await partial frames, and Resolved since last review
Previously raised — still open
🤖 AI Code Review · run · prompts: Previous review · 2026-10-29 · 40eab19AI Code Review — risk tier: Advisory only. A human owns the merge decision. Summary — Net-positive fix: client sessions are now keyed by handle instead of fd (closing the stale-session/null-deref bug), the Unix-socket takeover is closed with a proper stale-socket probe, and frame-stream reads correctly await partial frames. One low-severity robustness gap remains in the new probe. Findings
Previously raised — still open
🤖 AI Code Review · run · prompts: Previous review · 2026-10-29 · 40eab19AI Code Review — risk tier: Advisory only. A human owns the merge decision. Summary — Net-positive fix: client sessions are now keyed by handle instead of fd, the Unix-socket takeover bug is closed with a proper stale-socket probe, frame-stream reads correctly await partial frames, and Findings
Nits — none beyond the above. 🤖 AI Code Review · run · prompts: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 40eab1911c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
On the AI review's finding "TOCTOU window between stat/probe and remove() of the Unix socket path": declined. The window is the few microseconds between |
|
@codex review |
|
/ai-review |
|
Codex Review: Didn't find any major issues. Breezy! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…cking If fcntl() failed to set O_NONBLOCK, the stale-socket probe would connect on a blocking socket, and a live server with a full backlog could stall the start. The probe now returns fcntl's error, which keeps the file and fails the start with that reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
On the AI review's finding "probe_unix_socket ignores fcntl's return value, risking a blocking connect() hang": fixed in afc246e. If |
|
@codex review |
|
/ai-review |
|
🔁 AI Code Review updated for |
LCOV of commit
|
|
Codex Review: Didn't find any major issues. 👍 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Summary
Follow-up to #805: problems in the dnstap TCP and Unix-socket servers found while fixing it.
Disconnected clients were never released. Each client's session was keyed by its fd and erased from the client's close handler. By then the handle is closing,
uv_fileno()fails, and uvw'sfd()returns an uninitialized value, so the session and the client handle it owns stayed in the map. An erase that happened to hit a live client's fd dropped that client's session instead, and its next read dereferenced a null session. Sessions are now keyed by their client handle. The stream reports its connected clients in its info asdnstap.clients, which is how the tests see a disconnected client being released.A second Unix input took over the first one's socket. Before binding, the Unix server removed whatever was at its socket path. A second dnstap input on the same path unlinked the first server's socket and took its new clients, while the first kept running unreachable. A regular file at the path was deleted too. Now the server first checks what is at the path:
address already in usethe path exists and is not a socket, file keptunable to remove the stale socket there: <reason>unable to tell whether a server listens on the socket there: <reason>, file keptThe check is a non-blocking connect: only "connection refused" means stale. The last row is a behaviour change: a root-owned socket that a non-root pktvisord cannot connect to looks the same whether a server is behind it or not, so it is kept and the error says why. Remove it by hand if nothing uses it.
Clients were dropped when a frame spanned two reads. The frame-stream session threw, and the client was dropped, whenever a socket read ended inside a frame's 4-byte length, or inside a control frame after its escape code had been consumed. Reads can end anywhere, so a busy client was eventually dropped. The session now waits for the rest of the frame, and consumes a control frame only once all of it has arrived.
A new session could inherit the previous one's state.
_is_bidirhad no initial value. Now that a disconnected client's session is actually freed, the next session can be built in the same memory: on glibc it then started as bidirectional, and a unidirectional client's START was refused as out of order. It now startsfalse, as inWinFrameSession.h.Tests
All 25 ctest targets pass on macOS (Release). The new dnstap tests:
sun_pathis handled; a stale socket that cannot be removed, and one that cannot be checked, are reported. The last two skip as root, so the Linux CI job skips them and the macOS job runs them.All of them except the two stale-socket replacements, which keep develop's behaviour, failed on develop or before their fix. The two client-count tests use ports 15355 and 15356, because 5355 is LLMNR, which systemd-resolved can hold.
Not changed here
EAGAIN, which is treated as live._decode_control_frameleaks itsfstrm_controlon its error paths; erasing the front of the buffer after every frame is quadratic for reads full of small frames.accept()stops the server was wrong: in libuv 1.48 the listening socket stays watched after a faileduv_accept. Only a client handle that cannot be created stops it, anduv_tcp_init/uv_pipe_initdo not fail in practice.🤖 Generated with Claude Code