Add non-blocking I/O support to UnixStream#2
Conversation
Add close!, set-nonblocking, send-nb, read-append-nb, read-blocked, send-len, and prn to UnixStream, mirroring the non-blocking API that TcpStream already provides. Includes C implementations and tests for non-blocking read/write, send-len, and close-by-reference.
There was a problem hiding this comment.
Build & Tests
Build: pass. All 6 unix tests, 6 tcp tests, and 4 poll tests pass on this machine (aarch64 Linux). CI is still pending on both ubuntu-latest and macos-latest — cannot recommend merge until CI completes.
Findings
Should fix
-
send_nb_negative offset → UB (unix_stream.h:117): The offset parameter is a signedintfrom Carp'sInttype. The bounds checkif (offset >= data->len) return 0does not catch negative values. A negative offset causes pointer arithmetic underflow ((char*)data->data + offsetwraps) and a hugesize_tfor the send length. This is undefined behavior. Fix:if (offset < 0 || offset >= data->len) return 0. (The same bug exists inTcpStream—tcp_stream.h:104— but that's pre-existing.) -
send_len_OOB read (unix_stream.h:112-114): Iflen > strlen(*msg),send_allreads past the NUL terminator into adjacent heap memory. The doc says "sends a string with known length (avoids strlen)" but does not document the precondition thatlenmust be ≤ string length. At minimum, add a note to thesend-lendoc (unix_stream.carp:77) warning about this. Better: addif (len > (int)strlen(*msg)) return -1in the C code. (Same issue exists in TcpStream, but this is a new API surface being added.)
Nice to have (not blocking)
-
set-nonblockingsilently swallows errors (unix_stream.h:107-109): Iffcntl(F_GETFL)fails (e.g. bad fd), the function does nothing and the socket stays blocking. The void return type means callers can't detect failure. Consistent with TcpStream, so not a regression, but worth noting. -
Test gap —
send-nbwith non-zero offset (unix_test.carp:68): The test always passes offset=0. A test with offset > 0 would exercise the bounds-check path and catch the negative-offset bug. -
Test gap — EOF detection: No test verifies that
read-append-nbreturns 0 when the peer closes the connection. -
Missing
clear-buf: TcpStream hasclear-bufwhich UnixStream lacks. Minor parity gap, could be a follow-up.
Verified correct
- C function names match registered Carp names — all spot-checked.
close!is safe to call multiple times (fd=-1 guard).prn_handles negative fd correctly (printsUnixStream(-1)).read-append-nbrealloc strategy matches existing codebase patterns.read-blockedsentinel (-2) is consistent with TcpStream.- The 50ms sleep in the non-blocking test is reasonable for Unix domain sockets.
Verdict: revise
Two issues need attention before merge: (1) the negative-offset UB in send_nb_ is a real correctness bug — easy one-line fix; (2) send-len should either bounds-check or clearly document the precondition. CI must also complete before merge.
- Add offset < 0 guard to send_nb_ to prevent pointer arithmetic underflow on negative offset values - Add len bounds check to send_len_ to reject negative len or len exceeding the string length (returns -1/error) - Update send-len doc to state the precondition - Add tests for send-nb with non-zero and negative offset
|
Addressed the two issues flagged by @carpentry-reviewer (confirmed by @hellerve):
All 16 tests pass (6 unix, 6 tcp, 4 poll). |
There was a problem hiding this comment.
Build & Tests
CI passes on both ubuntu-latest and macos-latest. No local Carp compiler available, so build/test verified via CI only.
Prior feedback
Both issues from the first review round (confirmed by @hellerve) are addressed:
send_nb_negative offset UB (High): Fixed —unix_stream.h:118now guardsoffset < 0 || offset >= data->len, returning 0 for out-of-range offsets. Tests added for offset=1 and offset=-1.send_len_OOB read (High): Fixed —unix_stream.h:113now checkslen < 0 || len > (int)strlen(*msg), returning -1 (mapped toResult.Errorin Carp). Doc updated to state the precondition.
Findings
1. Missing regression tests for send-len edge cases (Low)
The bounds check at unix_stream.h:113 was just added to fix a real bug, but there are no tests exercising it:
send-lenwith negativelen(should returnResult.Error)send-lenwithlen > strlen(msg)(should returnResult.Error)
The send-nb negative offset has a test (offset=-1), which is good — send-len deserves the same treatment for the bounds it just gained.
2. Pre-existing: TcpStream has the same unfixed bugs (Non-blocking, follow-up)
tcp_stream.h:54 (send_len_) has no bounds check, and tcp_stream.h:104 (send_nb_) has no negative offset guard. These are the exact same bugs that were just fixed in UnixStream. Not this PR's scope, but worth a follow-up PR.
3. Verified correct
send_nb_: offset bounds check,send()call, EAGAIN/EWOULDBLOCK/EINTR handling all correct. Cast tosize_tis safe because both values are non-negative at that point.read_append_nb_: realloc strategy matches existing patterns,read()offset correct, three-way return (positive/0/-1/-2) is clean.send_len_: bounds check correctly rejects negative and over-length values.close_ref: double-close safe (fd >= 0guard, setsfd = -1).set_nonblocking: correctfcntlusage, identical to TcpStream.prn_: correct two-passsnprintfpattern with proper allocation.- All Carp
registerdeclarations match C function signatures exactly. read-blockedsentinel (-2) matches C return value fromread_append_nb_.- Test coverage: non-blocking roundtrip, send-nb with multiple offsets including negative, send-len partial string, close-by-reference. Solid.
Verdict: merge
Both flagged bugs are properly fixed, CI passes, and the code faithfully mirrors TcpStream patterns. The missing send-len edge case tests are a nice-to-have but not blocking — the C code is correct and the happy-path tests cover the core functionality.
Summary
UnixStream was missing all non-blocking I/O functionality that TcpStream already has, making it a second-class citizen in event-loop architectures. This PR brings UnixStream to parity by adding:
close!— close by reference (sets fd to -1 to prevent double-close), for use when the stream lives in a collectionset-nonblocking— puts the socket into non-blocking mode viafcntlsend-nb— non-blocking send with offset, returns bytes written (0 if would-block)read-append-nb— non-blocking append-read, returns bytes read, 0 for EOF, orread-blockedsentinel (-2) for EAGAINread-blocked— sentinel constant (-2) for would-block detectionsend-len— send a string with known length (avoidsstrlen)prn— string representation (UnixStream(fd))All C implementations and Carp bindings mirror TcpStream's existing patterns exactly.
Tests
Added three new tests to
test/unix_test.carp:read-blockedsentinel, reading after peer write, and non-blocking sendsend-len: verifies partial string send (only first N bytes)close!: verifies close-by-reference prevents further I/OAll 6 unix tests and all 6 tcp tests pass.
Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.