Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 6 additions & 1 deletion src/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Comment thread
yosuke-wolfssl marked this conversation as resolved.
Expand Down
26 changes: 25 additions & 1 deletion src/net_posix.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

#define _POSIX_C_SOURCE 200809L
#define _DEFAULT_SOURCE
#define _DARWIN_C_SOURCE

#include <wolfcert/http.h>
#include <wolfcert/errors.h>
Expand All @@ -42,6 +43,27 @@
#include <time.h>
#include <unistd.h>

/* 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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand Down
16 changes: 15 additions & 1 deletion src/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

#define _POSIX_C_SOURCE 200809L
#define _DEFAULT_SOURCE
#define _DARWIN_C_SOURCE

#include <wolfcert/server.h>
#include <wolfcert/errors.h>
Expand All @@ -42,6 +43,14 @@

#include <wolfssl/ssl.h>

/* WOLFCERT_SEND_FLAGS suppresses SIGPIPE per send(), leaving the process
Comment thread
Frauschi marked this conversation as resolved.
* 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.
*/
Expand All @@ -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)
Expand Down Expand Up @@ -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);
Comment thread
yosuke-wolfssl marked this conversation as resolved.

if ((ret = wolfSSL_accept(ssl)) == WOLFSSL_SUCCESS) {
srv->tls_current = ssl;
Expand Down Expand Up @@ -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);
}

Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
51 changes: 51 additions & 0 deletions tests/integration/test_est_chunked_robustness.c
Original file line number Diff line number Diff line change
Expand Up @@ -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]);

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.


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
Expand Down Expand Up @@ -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);
Expand Down
78 changes: 78 additions & 0 deletions tests/unit/test_net.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@

#include <wolfcert/wolfcert.h>
#include <wolfcert/http.h>
#include "internal.h"

#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
Expand All @@ -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);
Comment thread
Frauschi marked this conversation as resolved.
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);

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.

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
Expand Down Expand Up @@ -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;
}
Loading