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}