code wiki / (root) / nx_sigpipe_gate.nx

nx_sigpipe_gate.nx source

↩ module page · 127 lines · 6159 B

1// nx_sigpipe_gate.nx -- proves the SIGPIPE root fix, and proves the tooth can 2// SEE the disease it cures (debt seq1261/seq1126 family). 3// 4// THE FLAW: a process that writes to a peer which has closed its end takes 5// SIGPIPE's default action -- TERMINATE. For a daemon that is an outage with 6// no diagnosis: it dies holding a healthy listening socket, the supervisor 7// reports a crash-loop, and the request that triggered it looks failed even 8// when the work completed. Every long-running server in the tree ran this way 9// (a tree-wide grep for SIGPIPE handling returned NOTHING before this gate). 10// 11// THE PAIR (why this gate is not vacuous): T1 forks a child that does NOT 12// install the ignore and confirms the kernel KILLS it with signal 13 -- if 13// this ever goes green-by-accident the tooth has stopped measuring anything. 14// T2 forks a child that DOES install it and confirms the same write instead 15// returns -EPIPE and the child exits 0. One tooth shows the disease, the 16// other shows the cure, and neither can pass for the other's reason. 17import "nx_syscalls.nx" 18import "nx_gate_verdict.nx" 19import "nx_http_server.nx" // T5: the fix is bound to CREATING A LISTENER, not to remembering 20 21const SG_EPIPE: i64 = 0 - 32 // -errno the kernel returns once SIGPIPE is ignored 22const SG_SIGPIPE: i64 = 13 // signal number (also the rt_sigaction syscall nr) 23const SG_CHILD_OK: i64 = 0 // child exit when it saw EPIPE and survived 24const SG_CHILD_WRONGRC: i64 = 7 // child survived but the write rc was not -EPIPE 25 26// Fork a child that writes to a pipe whose READ end is already closed. 27// ignore=1 installs the SIGPIPE ignore first. Returns the raw wait4 status. 28func sg_probe(ignore: i64) -> i64 { 29 let fds: *i64 = sys_mmap(16) as *i64 30 if sys_pipe2(fds, 0) != 0 { return 0 - 1 } 31 let packed: i64 = fds[0] 32 let rfd: i64 = packed & 0xFFFFFFFF 33 let wfd: i64 = (packed >> 32) & 0xFFFFFFFF 34 let pid: i64 = sys_fork() 35 if pid < 0 { sys_close(rfd); sys_close(wfd); return 0 - 2 } 36 if pid == 0 { 37 if ignore == 1 { sys_ignore_sigpipe() } 38 sys_close(rfd) 39 let rc: i64 = sys_write(wfd, "x" as *u8, 1) 40 if rc == SG_EPIPE { sys_exit(SG_CHILD_OK) } 41 sys_exit(SG_CHILD_WRONGRC) 42 return 0 43 } 44 sys_close(rfd) 45 sys_close(wfd) 46 let st: *i64 = sys_mmap(16) as *i64 47 st[0] = 0 48 if sys_wait4(pid, st, 0) < 0 { return 0 - 3 } 49 return st[0] 50} 51 52func main() -> i64 { 53 let ctr: *i64 = gv_ctr() 54 gv_head("=== nx_sigpipe_gate (the SIGPIPE outage class: disease + cure) ===" as *u8) 55 56 // T1: WITHOUT the ignore the kernel must KILL the writer with signal 13. 57 // This is the disease tooth -- it keeps T2 honest. 58 let s1: i64 = sg_probe(0) 59 var t1: i64 = 0 60 if s1 >= 0 { if wait_term_signal(s1) == SG_SIGPIPE { t1 = 1 } } 61 gv_check("T1 default action: writing to a closed peer KILLS the process (signal 13)" as *u8, t1, ctr) 62 63 // T2: WITH the ignore the same write returns -EPIPE and the process LIVES. 64 let s2: i64 = sg_probe(1) 65 var t2: i64 = 0 66 if s2 >= 0 { if wait_term_signal(s2) == 0 { if wait_exit_code(s2) == SG_CHILD_OK { t2 = 1 } } } 67 gv_check("T2 sys_ignore_sigpipe: same write returns -EPIPE, process survives (exit 0)" as *u8, t2, ctr) 68 69 // T3: the installer itself reports success, and is idempotent -- a daemon 70 // that calls it twice (restart path, re-exec) must not start failing. 71 let r1: i64 = sys_ignore_sigpipe() 72 let r2: i64 = sys_ignore_sigpipe() 73 var t3: i64 = 0 74 if r1 == 0 { if r2 == 0 { t3 = 1 } } 75 gv_check("T3 rt_sigaction accepted and idempotent (rc 0 twice)" as *u8, t3, ctr) 76 77 // T4: ignoring SIGPIPE must not blunt ordinary write errors -- a write to 78 // a plainly bad fd still fails loudly (-EBADF), so this fix cannot hide a 79 // real I/O fault behind a survived process. 80 let bad: i64 = sys_write(0 - 7, "x" as *u8, 1) 81 var t4: i64 = 0 82 if bad < 0 { if bad != SG_EPIPE { t4 = 1 } } 83 gv_check("T4 unrelated write errors still surface (bad fd -> negative, not EPIPE)" as *u8, t4, ctr) 84 85 // T5: BY CONSTRUCTION. Creating a listening socket must ITSELF install the 86 // ignore -- 52 files build servers on this one primitive, and a fix each of 87 // them has to remember is a fix that will be missed. The child below never 88 // calls sys_ignore_sigpipe; it only asks for a listener, then writes to a 89 // closed peer. If it survives with -EPIPE, the primitive carried the fix. 90 // (Run in a CHILD: a false result must not kill the gate, and the gate 91 // process has already installed the ignore itself by T3.) 92 let fds5: *i64 = sys_mmap(16) as *i64 93 var t5: i64 = 0 94 if sys_pipe2(fds5, 0) == 0 { 95 let pk5: i64 = fds5[0] 96 let rf5: i64 = pk5 & 0xFFFFFFFF 97 let wf5: i64 = (pk5 >> 32) & 0xFFFFFFFF 98 let kid: i64 = sys_fork() 99 if kid == 0 { 100 // fresh process: SIGPIPE is back to its inherited-default disposition 101 // (fork does NOT reset it, so ask for a listener on a throwaway port 102 // and let the primitive be the only thing that could have installed it) 103 let ad5: *u8 = sys_mmap(16) 104 let vd5: *i64 = sys_mmap(16) as *i64 105 nx_http_server_addr_loopback(ad5, 47251) 106 let lf5: i64 = nx_http_server_listen(ad5, 4, vd5) 107 if lf5 >= 0 { sys_close(lf5) } 108 sys_close(rf5) 109 let wr5: i64 = sys_write(wf5, "x" as *u8, 1) 110 if wr5 == SG_EPIPE { sys_exit(SG_CHILD_OK) } 111 sys_exit(SG_CHILD_WRONGRC) 112 return 0 113 } 114 sys_close(rf5) 115 sys_close(wf5) 116 let st5: *i64 = sys_mmap(16) as *i64 117 st5[0] = 0 118 if sys_wait4(kid, st5, 0) >= 0 { 119 if wait_term_signal(st5[0]) == 0 { if wait_exit_code(st5[0]) == SG_CHILD_OK { t5 = 1 } } 120 } 121 } 122 gv_check("T5 by construction: creating a listener installs it -- a daemon cannot forget" as *u8, t5, ctr) 123 124 let rc: i64 = gv_verdict("SIGPIPE-GATE" as *u8, ctr, "a daemon must survive a client that walks away mid-response" as *u8) 125 sys_exit(rc) 126 return rc 127}