/* test_http_sigpipe.c — a client that hangs up mid-response must not kill the * server. Integration probe: point it at a running engram. * * WHAT THIS REPRODUCES. `send()` to a socket whose peer has closed raises * SIGPIPE, whose default disposition is terminate. Nothing in the runtime * suppressed it, so any abandoned request could kill the process. In production * the abandoning client was the heartbeat: ai.neuron.engram-tick.plist runs on * StartInterval 600 and calls `curl -s -m10 -X POST /api/tick`; when the endpoint * exceeded ten seconds curl hung up, and the eventual response write killed the * server. 254 restarts between 2026-08-13T19:37 and 2026-08-16T18:16, with * `launchctl list ai.neuron.engram` reporting LastExitStatus = 13 — raw wait * status 13, killed by signal 13, SIGPIPE. * * TWO THINGS THIS PROBE GETS RIGHT, both of which a naive version gets wrong and * both of which cost me a false PASS on the unpatched build before I caught them: * * 1. CLOSE MODE. Closing with SO_LINGER=0 emits RST, and the server's FIRST * write then returns ECONNRESET rather than raising SIGPIPE: http_send_all * sees w <= 0, returns -1, and stops. No signal, no crash — the test passes * on the UNPATCHED build and proves nothing. A NORMAL close (FIN) is what * kills: the first write succeeds, the peer answers RST because it is fully * closed, and the SECOND write raises SIGPIPE. A response is emitted as four * http_send_all calls (status line, headers, tail, body), so the sequence is * always reached. * * 2. TIMING. The kill does not land when the client closes. It lands when the * server reaches its write, which on a heavy endpoint is many seconds later * (~16s measured on /api/activate over a 13,632-node store). Checking * liveness a second after the hang-up finds the process healthy and reports * a false pass. * * Usage: test_http_sigpipe [rounds] [settle_seconds] * Exit 0 = server survived every round (PASS), 1 = server died (FAIL), * 2 = bad precondition. */ #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include #include /* The request is deliberately expensive: the body must be large enough and slow * enough that the server is still working when the client walks away. */ static const char* REQ_PATH = "/api/activate?q=will%20anderson&depth=3&_auth=ntn-user-2026"; static int alive(pid_t pid) { return kill(pid, 0) == 0 || errno == EPERM; } static void sleep_ms(long ms) { struct timespec ts = { ms / 1000, (ms % 1000) * 1000000L }; nanosleep(&ts, NULL); } /* Connect, ask for the expensive body, then hang up NORMALLY without reading. */ static int abort_request(int port) { int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) return -1; struct sockaddr_in a; memset(&a, 0, sizeof a); a.sin_family = AF_INET; a.sin_port = htons((uint16_t)port); a.sin_addr.s_addr = inet_addr("127.0.0.1"); if (connect(fd, (struct sockaddr*)&a, sizeof a) != 0) { close(fd); return -1; } char req[512]; int n = snprintf(req, sizeof req, "GET %s HTTP/1.1\r\nHost: probe\r\nConnection: close\r\nAccept: */*\r\n\r\n", REQ_PATH); if (write(fd, req, (size_t)n) != n) { close(fd); return -1; } sleep_ms(200); /* ensure the request landed and work has begun */ close(fd); /* NORMAL close -> FIN. Never SO_LINGER=0 / RST. */ return 0; } int main(int argc, char** argv) { if (argc < 3) { fprintf(stderr, "usage: %s [rounds=1] [settle_seconds=45]\n", argv[0]); return 2; } int port = atoi(argv[1]); pid_t pid = (pid_t)atoi(argv[2]); int rounds = argc > 3 ? atoi(argv[3]) : 1; int settle = argc > 4 ? atoi(argv[4]) : 45; if (!alive(pid)) { printf(" PRECONDITION FAILED: pid %d is not running before the probe\n", pid); return 2; } printf(" precondition: pid %d alive, target port %d\n", pid, port); for (int i = 1; i <= rounds; i++) { if (abort_request(port) != 0) { printf(" round %d: could not reach the server on port %d\n", i, port); return 2; } printf(" round %d: client hung up; waiting up to %ds for the server to " "reach its write...\n", i, settle); for (int s = 0; s < settle * 2; s++) { if (!alive(pid)) { printf(" round %d: SERVER DIED %.1fs after the hang-up *** FAIL ***\n", i, s / 2.0); return 1; } sleep_ms(500); } printf(" round %d: server survived\n", i); } printf(" PASS: the server survived %d abandoned request(s)\n", rounds); return 0; }