diff --git a/Makefile.am b/Makefile.am index ccd7d24..4dbd8d3 100644 --- a/Makefile.am +++ b/Makefile.am @@ -132,6 +132,7 @@ test_transport_SOURCES = tests/unit/test_transport.c test_transport_LDADD = libwolfcert.la $(WOLFSSL_LIBS) test_net_SOURCES = tests/unit/test_net.c +test_net_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/src test_net_LDADD = libwolfcert.la $(WOLFSSL_LIBS) test_http_SOURCES = tests/unit/test_http.c test_http_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/src diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d864ca3..b83efa0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -455,6 +455,12 @@ The contract: path included. A failed `connect` is never paired with one. - **`ctx` is transport-wide** (the stack instance, say), distinct from the per-connection handle. Whatever it points at must outlive the connection. +- **Signal safety is the transport's.** wolfCert cannot reach your descriptor, + so a write to a peer that has gone away must not raise `SIGPIPE` in the + embedding application. The built-in POSIX instance sets `SO_NOSIGPIPE` at + `socket()` and passes `MSG_NOSIGNAL` on every `send()`. Do the same if your + transport uses BSD sockets. If it uses a stack that never raises `SIGPIPE`, + there is nothing to do. - **The struct itself need not.** Opening a connection copies it, so the config may be a temporary — and a later change to your copy has no effect on a connection already open. diff --git a/src/internal.h b/src/internal.h index 02821df..155a0e1 100644 --- a/src/internal.h +++ b/src/internal.h @@ -207,6 +207,11 @@ struct WolfCertServer { ssize_t wolfcert_io_recv(WolfCertServer* srv, int fd, void* buf, size_t len); ssize_t wolfcert_io_send(WolfCertServer* srv, int fd, const void* buf, size_t len); +/* Best-effort SO_NOSIGPIPE on a connected socket, so a write to a departed peer + * cannot raise SIGPIPE in the embedding application. A no-op where the platform + * has no such option, and on an fd that is not a socket. */ +WOLFCERT_TEST_VIS void wolfcert_sock_nosigpipe(int fd); + /* Factories supplied by est/est_server.c and scep/scep_server.c. */ WOLFCERT_API const WolfCertServerOps* wolfcert_est_server_ops(void); WOLFCERT_API const WolfCertServerOps* wolfcert_scep_server_ops(void); @@ -312,7 +317,7 @@ WOLFCERT_TEST_VIS int wolfcert_parse_ip(const char* s, uint8_t out[16], size_t* out_len); /* Built-in POSIX transport. */ -extern const WolfCertTransport wolfcert_posix_transport; +WOLFCERT_TEST_VIS extern const WolfCertTransport wolfcert_posix_transport; /* The descriptor behind a wolfcert_posix_transport connection; -1 for a * handle any other transport minted. */ int wolfcert_transport_fd(const WolfCertTransport* t, void* conn); diff --git a/src/net_posix.c b/src/net_posix.c index a74046f..a030e41 100644 --- a/src/net_posix.c +++ b/src/net_posix.c @@ -26,6 +26,7 @@ #define _POSIX_C_SOURCE 200809L #define _DEFAULT_SOURCE +#define _DARWIN_C_SOURCE #include #include @@ -42,6 +43,27 @@ #include #include +/* WOLFCERT_SEND_FLAGS suppresses SIGPIPE per send(), leaving the process + * signal disposition to the embedding application. */ +#ifdef MSG_NOSIGNAL +#define WOLFCERT_SEND_FLAGS MSG_NOSIGNAL +#else +#define WOLFCERT_SEND_FLAGS 0 +#endif + +void wolfcert_sock_nosigpipe(int fd) +{ +#ifdef SO_NOSIGPIPE + int on = 1; + + /* Advisory: an fd that is not a socket fails here with ENOTSOCK, which is + * not an error for the caller. */ + (void)setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &on, sizeof(on)); +#else + (void)fd; +#endif +} + static long mono_ms(void) { struct timespec ts; @@ -144,6 +166,8 @@ int wolfcert_posix_connect(const char* host, int port, int timeout_ms, void* ctx if (fd < 0) continue; + wolfcert_sock_nosigpipe(fd); + if (connect_timeout(fd, rp->ai_addr, rp->ai_addrlen, attempt_ms) == 0) break; @@ -279,7 +303,7 @@ static int posix_write(void* ctx, void* conn, const uint8_t* buf, size_t len, return rc; do { - n = send(fd, buf, len, 0); + n = send(fd, buf, len, WOLFCERT_SEND_FLAGS); } while (n < 0 && errno == EINTR); if (n > 0) diff --git a/src/server.c b/src/server.c index 96cf744..aadc373 100644 --- a/src/server.c +++ b/src/server.c @@ -26,6 +26,7 @@ #define _POSIX_C_SOURCE 200809L #define _DEFAULT_SOURCE +#define _DARWIN_C_SOURCE #include #include @@ -42,6 +43,14 @@ #include +/* WOLFCERT_SEND_FLAGS suppresses SIGPIPE per send(), leaving the process + * signal disposition to the embedding application. */ +#ifdef MSG_NOSIGNAL +#define WOLFCERT_SEND_FLAGS MSG_NOSIGNAL +#else +#define WOLFCERT_SEND_FLAGS 0 +#endif + /* accept() poll cadence: how often wolfcert_server_run() wakes to re-check the * stopping flag while idle. Bounds shutdown latency; not performance-critical. */ @@ -66,7 +75,7 @@ ssize_t wolfcert_io_send(WolfCertServer* srv, int fd, const void* buf, size_t le return r <= 0 ? -1 : (ssize_t)r; } - return send(fd, buf, len, 0); + return send(fd, buf, len, WOLFCERT_SEND_FLAGS); } static int tls_setup(WolfCertServer* s, const WolfCertServerCfgSrv* cfg) @@ -326,12 +335,15 @@ int wolfcert_server_run(WolfCertServer* srv) return WOLFCERT_ERR_IO; } + wolfcert_sock_nosigpipe(cs); + if (srv->tls_ctx != NULL) { /* Terminate TLS on this accepted fd. The protocol handler sees * plaintext HTTP through wolfcert_io_{recv,send}. */ WOLFSSL* ssl = wolfSSL_new(srv->tls_ctx); if (ssl != NULL) { wolfSSL_set_fd(ssl, cs); + wolfSSL_SetIOWriteFlags(ssl, WOLFCERT_SEND_FLAGS); if ((ret = wolfSSL_accept(ssl)) == WOLFSSL_SUCCESS) { srv->tls_current = ssl; @@ -381,6 +393,8 @@ int wolfcert_server_serve_fd(WolfCertServer* srv, int fd) if (srv == NULL || fd < 0) return WOLFCERT_ERR_BAD_ARG; + wolfcert_sock_nosigpipe(fd); + return srv->ops->serve_fd(srv, fd); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d40688b..64584bc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,6 +18,7 @@ add_test(NAME store COMMAND test_store) if(WOLFCERT_ENABLE_BUILTIN_TRANSPORT) add_executable(test_net unit/test_net.c) + target_include_directories(test_net PRIVATE ${CMAKE_SOURCE_DIR}/src) target_link_libraries(test_net PRIVATE wolfcert) add_test(NAME net COMMAND test_net) endif() diff --git a/tests/integration/test_est_chunked_robustness.c b/tests/integration/test_est_chunked_robustness.c index 7c2702d..9803c84 100644 --- a/tests/integration/test_est_chunked_robustness.c +++ b/tests/integration/test_est_chunked_robustness.c @@ -393,6 +393,55 @@ static int keepalive_after_split_trailer(uint16_t port) return 0; } +/* Set by note_sigpipe(); a server write must leave it clear. */ +static volatile sig_atomic_t g_sigpipe_raised; + +static void note_sigpipe(int sig) +{ + (void)sig; + g_sigpipe_raised = 1; +} + +/* Queue a full request, then close the peer: the queued bytes still reach the + * handler's response write. Own server, so no constraint on the accept loop. */ +static int no_sigpipe_on_response(void) +{ + static const char http_req[] = + "GET /nope HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"; + WolfCertServerCfgSrv cfg = { + .protocol = WOLFCERT_PROTO_EST, + .bind_host = "127.0.0.1", .bind_port = 0, + }; + WolfCertServer* srv = NULL; + struct sigaction sa, old; + int sv[2]; + + REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); + REQUIRE(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); + REQUIRE(write(sv[1], http_req, sizeof(http_req) - 1) + == (ssize_t)(sizeof(http_req) - 1)); + close(sv[1]); + + /* Catch, not ignore, so "not raised" differs from "raised and + * swallowed"; main() ignores it for the other cases. */ + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = note_sigpipe; + sigemptyset(&sa.sa_mask); + REQUIRE(sigaction(SIGPIPE, &sa, &old) == 0); + g_sigpipe_raised = 0; + + /* An unwritable peer errors either way; the signal is the assertion. */ + (void)wolfcert_server_serve_fd(srv, sv[0]); + + REQUIRE(sigaction(SIGPIPE, &old, NULL) == 0); + close(sv[0]); + wolfcert_server_free(srv); + + REQUIRE(g_sigpipe_raised == 0); + + return 0; +} + int main(void) { /* A truncated request makes the server respond and close while the @@ -422,6 +471,8 @@ int main(void) rc = accept_multisegment_chunked_body(port); if (rc == 0) rc = keepalive_after_split_trailer(port); + if (rc == 0) + rc = no_sigpipe_on_response(); wolfcert_server_stop(srv); pthread_join(tid, NULL); diff --git a/tests/unit/test_net.c b/tests/unit/test_net.c index bc9160d..27b49fb 100644 --- a/tests/unit/test_net.c +++ b/tests/unit/test_net.c @@ -31,9 +31,12 @@ #include #include +#include "internal.h" #include #include +#include +#include #include #include #include @@ -55,6 +58,73 @@ static long mono_ms(void) return (long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; } +/* Set by note_sigpipe(); a library write must leave it clear. */ +static volatile sig_atomic_t g_sigpipe_raised; + +static void note_sigpipe(int sig) +{ + (void)sig; + g_sigpipe_raised = 1; +} + +/* Write to a socketpair whose peer is closed, handler armed. `nosigpipe` + * applies the socket option wolfcert_posix_connect() sets. */ +static int write_to_dead_peer(int nosigpipe, int* out_rc) +{ + static const uint8_t body[256] = { 0 }; + struct sigaction sa, old; + int sv[2]; + + /* Before closing the peer: setsockopt(SO_NOSIGPIPE) fails with EINVAL + * once the peer is gone. */ + REQUIRE(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); + if (nosigpipe) + wolfcert_sock_nosigpipe(sv[0]); + close(sv[1]); + + /* Catch, not ignore, so "not raised" differs from "raised and + * swallowed"; CI runs every test with SIGPIPE ignored. */ + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = note_sigpipe; + sigemptyset(&sa.sa_mask); + REQUIRE(sigaction(SIGPIPE, &sa, &old) == 0); + g_sigpipe_raised = 0; + + *out_rc = wolfcert_posix_transport.write(NULL, (void*)(intptr_t)sv[0], + body, sizeof(body), 0); + + REQUIRE(sigaction(SIGPIPE, &old, NULL) == 0); + close(sv[0]); + + return 0; +} + +/* Both arms in force. WOLFCERT_ERR_IO pins that the write reached send(). */ +static int test_no_sigpipe_on_dead_peer(void) +{ + int rc = 0; + + REQUIRE(write_to_dead_peer(1, &rc) == 0); + REQUIRE(g_sigpipe_raised == 0); + REQUIRE(rc == WOLFCERT_ERR_IO); + + return 0; +} + +#ifdef MSG_NOSIGNAL +/* No socket option: the send flag alone must suppress the signal. */ +static int test_send_flag_alone_suppresses(void) +{ + int rc = 0; + + REQUIRE(write_to_dead_peer(0, &rc) == 0); + REQUIRE(g_sigpipe_raised == 0); + REQUIRE(rc == WOLFCERT_ERR_IO); + + return 0; +} +#endif + int main(void) { /* Success path with a positive timeout: stand up a loopback listener and @@ -88,6 +158,14 @@ int main(void) REQUIRE(fd2 < 0); REQUIRE(elapsed < 3000); + if (test_no_sigpipe_on_dead_peer()) + return 1; + +#ifdef MSG_NOSIGNAL + if (test_send_flag_alone_suppresses()) + return 1; +#endif + printf("OK (unreachable connect returned in %ldms)\n", elapsed); return 0; }