net,server: suppress SIGPIPE on writes to a departed peer - #24
net,server: suppress SIGPIPE on writes to a departed peer#24yosuke-wolfssl wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
There are confirmed build/visibility issues (server calling a function compiled only with the builtin transport enabled; and wolfcert_posix_transport visibility not applied at the definition) that can break some configurations.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR prevents SIGPIPE from being delivered to applications embedding wolfCert when a peer disconnects during a socket write, covering both the built-in POSIX transport and server write paths. It introduces per-send() suppression (via MSG_NOSIGNAL when available) and best-effort per-socket suppression (via SO_NOSIGPIPE when available), plus tests that assert no SIGPIPE is delivered.
Changes:
- Add
MSG_NOSIGNAL-based send flags to the client POSIX transport and server plaintext send path, and propagate the same flags into wolfSSL for server TLS writes. - Add
wolfcert_sock_nosigpipe()(SO_NOSIGPIPE) and call it from client connect and server accept/serve-fd paths. - Add unit/integration tests that install a
SIGPIPEhandler and assert it never fires when writing to a closed socketpair peer.
File summaries
| File | Description |
|---|---|
| tests/unit/test_net.c | Adds a unit test asserting no SIGPIPE on a failed transport write. |
| tests/integration/test_est_chunked_robustness.c | Adds an integration case asserting the server doesn’t raise SIGPIPE when replying to a departed peer. |
| tests/CMakeLists.txt | Adds src/ include path for test_net so it can include internal headers. |
| src/server.c | Suppresses SIGPIPE on server writes and carries flags into wolfSSL TLS I/O. |
| src/net_posix.c | Suppresses SIGPIPE on client transport writes and adds wolfcert_sock_nosigpipe(). |
| src/internal.h | Declares wolfcert_sock_nosigpipe() and exports wolfcert_posix_transport for tests. |
| Makefile.am | Adds -I$(top_srcdir)/src for test_net to match the CMake change. |
| docs/ARCHITECTURE.md | Documents that transports must not allow SIGPIPE to reach the embedding application. |
Review details
- Files reviewed: 8/8 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #24
Scan targets checked: wolfcert-bugs
Fenrir result: Approved ✅
No new issues found in the changed files.
Advisory only — this automated result does not count as a GitHub approval.
Frauschi
left a comment
There was a problem hiding this comment.
Nice change - the mechanism is right, and splitting it across MSG_NOSIGNAL, SO_NOSIGPIPE and wolfSSL_SetIOWriteFlags() covers all three write paths. The per-arm negative controls in the PR description are the part that convinced me.
Three things, none blocking. Two are about the new unit case: it bypasses the connect path so it only ever covers the MSG_NOSIGNAL arm, and REQUIRE(rc < 0) can pass without reaching send() at all. The third is the duplicated WOLFCERT_SEND_FLAGS block.
I also looked at whether server.c calling wolfcert_sock_nosigpipe() creates a link problem for a no-builtin-transport build, and concluded it does not - server.c already calls socket(), bind(), accept() and friends directly, so that configuration never built regardless. Nothing to do here.
- posix_write() and wolfcert_io_send() pass WOLFCERT_SEND_FLAGS, defined in net_posix.c and server.c as MSG_NOSIGNAL where the platform has it and 0 elsewhere. The server accept loop passes the same flags to wolfSSL_SetIOWriteFlags() after wolfSSL_set_fd(). - net_posix.c gains wolfcert_sock_nosigpipe(), setting SO_NOSIGPIPE, called from wolfcert_posix_connect(), wolfcert_server_run() and wolfcert_server_serve_fd(). Both files define _DARWIN_C_SOURCE to expose the option. - test_net.c writes through wolfcert_posix_transport.write() to a socketpair whose peer is closed, once with both arms in force and once with the send flag alone; test_est_chunked_robustness.c does the same through wolfcert_server_serve_fd(). Each installs a SIGPIPE handler, asserts it never fired, and restores the previous disposition; the transport cases also require WOLFCERT_ERR_IO. - wolfcert_sock_nosigpipe() and wolfcert_posix_transport gain WOLFCERT_TEST_VIS, test_net gains -I src in both build systems, and ARCHITECTURE.md 4.6 records the transport's signal-safety duty.
46af8cd to
79e786a
Compare
|
Thank you @Frauschi for reviewing, I reworked on it. |
Frauschi
left a comment
There was a problem hiding this comment.
Both fixes look right. I reproduced the per-arm controls you describe: stripping MSG_NOSIGNAL from net_posix.c fails test_send_flag_alone_suppresses alone while the both-arms case still passes, and reverting server.c fails est_chunked_robustness alone. 27/27 on CMake and autoconf here, clean under -Wall -Wextra -Werror, leaks-clean on both touched tests, and a shared build with hidden visibility genuinely active (wolfcert_io_recv and wolfcert_transport_fd stay unexported) exports both WOLFCERT_TEST_VIS symbols - so the Copilot visibility point is a non-issue, as you said.
The #ifdef MSG_NOSIGNAL gate on the flag-alone case is a better answer than what I asked for: where the flag is absent the case is not compiled, rather than failing against a library that is still correct there.
Two things left, neither blocking. The WOLFCERT_SEND_FLAGS duplication is settled separately - keep it as it is.
| { | ||
| int rc = 0; | ||
|
|
||
| REQUIRE(write_to_dead_peer(1, &rc) == 0); |
There was a problem hiding this comment.
The split fixes the MSG_NOSIGNAL side, but wolfcert_sock_nosigpipe() itself still has no behavioural coverage anywhere. I replaced its setsockopt() call with (void)on; (void)fd; and all 27 tests still passed - on Darwin MSG_NOSIGNAL carries this case, and the discarded setsockopt return hides the rest, so a wrong level/optname or a bad call order is invisible.
You are right that it cannot be isolated through the transport write, since the transport always passes the flag. A direct send() with flags 0 does isolate it: after wolfcert_sock_nosigpipe(sv[0]), a raw send(sv[0], buf, len, 0) to the closed peer returns -1 with no signal, and raises SIGPIPE when the option was not set. I confirmed both directions standalone. Under #ifdef SO_NOSIGPIPE that is a handful of lines, and it is the case that would have caught the EINVAL ordering trap you found by hand instead of leaving it to inspection.
Not blocking, but it is the half of my earlier comment that is still open, so I would rather it landed here than in a follow-up.
| g_sigpipe_raised = 0; | ||
|
|
||
| /* An unwritable peer errors either way; the signal is the assertion. */ | ||
| (void)wolfcert_server_serve_fd(srv, sv[0]); |
There was a problem hiding this comment.
Minor, and the inverse of the WOLFCERT_ERR_IO fix you applied to the unit cases: this one discards the return, so nothing pins that the handler reached a response write. serve_fd returns WOLFCERT_ERR_NOT_FOUND (-11) here - the 404 for GET /nope - so REQUIRE(rc == WOLFCERT_ERR_NOT_FOUND) is the cheap equivalent.
It is weaker than the unit assertion, since -11 comes back whether or not the write succeeded, and the five sibling cases in this file already assert that requests get responses - a refactor that stopped the server responding would fail loudly there anyway. Take it or leave it.
Problem
wolfCert is a library, but every socket write left
SIGPIPEsuppression to thecaller. A peer that disappears mid-write raises
SIGPIPEin the embeddingapplication, whose default disposition terminates it — an application that
merely links wolfCert never opted into that. Both sides were affected: the
built-in POSIX transport and the EST/SCEP test server. Finding f-9769.
Fix (
src/net_posix.c)WOLFCERT_SEND_FLAGSresolves toMSG_NOSIGNALwhere the platform hasit,
0otherwise.posix_write()andwolfcert_io_send()pass it on everysend().wolfcert_sock_nosigpipe()setsSO_NOSIGPIPEon a connected socket, andcompiles to nothing where the option is absent. Called from
wolfcert_posix_connect(),wolfcert_server_run()andwolfcert_server_serve_fd().wolfSSL_SetIOWriteFlags()carries the same flags into wolfSSL's ownsend()in the server accept loop, which still useswolfSSL_set_fd()._DARWIN_C_SOURCEin both files exposesSO_NOSIGPIPEpast the_POSIX_C_SOURCEthey already set.posix_write()— TLS records route throughwolfcert_cbio_sendinto the same transportwolfcert_io_send()wolfSSL_SetIOWriteFlags()Closes f-9769.
Tests
Three cases write to a socketpair whose peer is already closed.
tests/unit/test_net.cgoes throughwolfcert_posix_transport.write()twice —once with both arms in force, once with only the send flag, so each is pinned
separately — and
tests/integration/test_est_chunked_robustness.cgoes throughwolfcert_server_serve_fd(). Each installs aSIGPIPEhandler that recordsdelivery, asserts it never fired, and restores the previous disposition; the
transport cases also require
WOLFCERT_ERR_IO, which only comes back oncesend()has actually failed.A handler rather than
SIG_DFLorSIG_IGN: the default kills the processbefore it can report, and ignoring makes "never raised" indistinguishable from
"raised and swallowed" — which is what CI already does for every test via
scripts/ci/sigpipe-launcher.sh. A socketpair rather than a TCP peer: a resetTCP peer returns
ECONNRESETon the first failingsend(), raising no signal,so such a test could not fail.
Verification
-Wall -Wextra -Werror.server.cfailsest_chunked_robustnessalone. RemovingMSG_NOSIGNALleaves theboth-arms case passing —
SO_NOSIGPIPEcarries it — while thesend-flag-only case fails, so neither arm can regress unnoticed.
Not in this PR
The server TLS arm (
wolfSSL_SetIOWriteFlags) is asserted structurally ratherthan behaviourally. Covering it needs a completed handshake followed by a peer
that vanishes before the response write, which reintroduces the timing race the
cases above are built to avoid. On Linux that line is the only guard for a
TLS-terminated connection, so it is worth revisiting separately.