Skip to content

net,server: suppress SIGPIPE on writes to a departed peer - #24

Open
yosuke-wolfssl wants to merge 1 commit into
wolfSSL:mainfrom
yosuke-wolfssl:fix/f_9769
Open

net,server: suppress SIGPIPE on writes to a departed peer#24
yosuke-wolfssl wants to merge 1 commit into
wolfSSL:mainfrom
yosuke-wolfssl:fix/f_9769

Conversation

@yosuke-wolfssl

@yosuke-wolfssl yosuke-wolfssl commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Problem

wolfCert is a library, but every socket write left SIGPIPE suppression to the
caller. A peer that disappears mid-write raises SIGPIPE in the embedding
application
, 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_FLAGS resolves to MSG_NOSIGNAL where the platform has
    it, 0 otherwise. posix_write() and wolfcert_io_send() pass it on every
    send().
  • wolfcert_sock_nosigpipe() sets SO_NOSIGPIPE on a connected socket, and
    compiles to nothing where the option is absent. Called from
    wolfcert_posix_connect(), wolfcert_server_run() and
    wolfcert_server_serve_fd().
  • wolfSSL_SetIOWriteFlags() carries the same flags into wolfSSL's own
    send() in the server accept loop, which still uses wolfSSL_set_fd().
  • _DARWIN_C_SOURCE in both files exposes SO_NOSIGPIPE past the
    _POSIX_C_SOURCE they already set.
Write path Suppressed by
Client plaintext and TLS posix_write() — TLS records route through wolfcert_cbio_send into the same transport
Server plaintext wolfcert_io_send()
Server TLS wolfSSL_SetIOWriteFlags()

Closes f-9769.

Tests

Three cases write to a socketpair whose peer is already closed.
tests/unit/test_net.c goes through wolfcert_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.c goes through
wolfcert_server_serve_fd(). Each installs a SIGPIPE handler that records
delivery, asserts it never fired, and restores the previous disposition; the
transport cases also require WOLFCERT_ERR_IO, which only comes back once
send() has actually failed.

A handler rather than SIG_DFL or SIG_IGN: the default kills the process
before 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 reset
TCP peer returns ECONNRESET on the first failing send(), raising no signal,
so such a test could not fail.

Verification

  • 27/27 on CMake and autoconf; clean under -Wall -Wextra -Werror.
  • Negative controls run per arm. Reverting only server.c fails
    est_chunked_robustness alone. Removing MSG_NOSIGNAL leaves the
    both-arms case passing — SO_NOSIGPIPE carries it — while the
    send-flag-only case fails, so neither arm can regress unnoticed.
  • Build-system parity and the freestanding ARM compile gate pass.

Not in this PR

The server TLS arm (wolfSSL_SetIOWriteFlags) is asserted structurally rather
than 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.

@yosuke-wolfssl yosuke-wolfssl self-assigned this Sep 10, 2026
Copilot AI lite review requested due to automatic review settings September 10, 2026 05:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 SIGPIPE handler 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.

Comment thread src/internal.h
Comment thread src/server.c
Comment thread tests/integration/test_est_chunked_robustness.c
Comment thread tests/unit/test_net.c

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Frauschi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tests/unit/test_net.c
Comment thread tests/unit/test_net.c Outdated
Comment thread src/server.c
- 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.
@yosuke-wolfssl

Copy link
Copy Markdown
Contributor Author

Thank you @Frauschi for reviewing, I reworked on it.
Please check inline comments for details.

@Frauschi Frauschi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tests/unit/test_net.c
{
int rc = 0;

REQUIRE(write_to_dead_peer(1, &rc) == 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants