nx_sigfixture.nx source
↩ module page · 97 lines · 5214 B
1// nx_sigfixture.nx -- A PROCESS THAT DIES BY THE SIGNAL YOU NAME: THE ONE FIXTURE FOR EVERY WAIT-STATUS RULER.
2//
3// WHY (2026-09-05, measured on the promote path). nx_behaveprobe reported rc=0 for a gate the crash guard
4// had killed with SIGSEGV, twice, and that receipt cleared a crashing gate for promote. The estate's rule
5// already existed (wait_status_rc: a signal death reads 128+signal, never 0; nx_waitrc_gate proves it
6// in-process with a forked child) -- the served probe was simply older than the rule. What the estate had
7// no way to do was ASK A LIVE, REGISTERED RULER how it reads a signal death, because no registered subject
8// dies by a signal on purpose. This organ is that subject: nx_behaveprobe, nx_job_run, /api/gate_run and
9// every other consumer of the tr_run_capture family can be pointed at it and their rc read back.
10//
11// usage: nx_sigfixture <signal 1..64> -- prints one line, then kill(getpid(), signal) on ITSELF
12// nx_sigfixture exit <code> -- prints one line, then exits with that code (the control arm)
13// SIGTERM (15) is the honest signal: no guard catches it, so the wait status carries a TRUE signal death
14// and a correct ruler reads 143 where the old accessor read 0. SIGKILL (9) is the watchdog class (the
15// bounded capture maps its own kill to TR_ERR_TIMEOUT). A guarded fault signal (SIGSEGV 11) is caught by the
16// default-on crash guard and surfaces as exit 139 -- that is the guard's contract and this fixture does not
17// pretend otherwise: pass 11 and you measure the guard, pass 15 and you measure the ruler.
18// The line it prints BEFORE dying exists so a capture is never empty by cause: an empty capture plus rc=0
19// is the shape of a tool that never ran, and this fixture must never be mistaken for one.
20// If the signal is blocked or ignored the process is still alive afterwards; it says so and exits
21// SF_EXIT_SURVIVED, never 0, so a ruler cannot be acquitted by a signal that never landed.
22// license_tier: ORIGINAL expect_exit: 143
23import "nx_syscalls.nx"
24
25const SF_SYS_GETPID_RV64: i64 = 172 // rv64 getpid, translated by __syscall (witness: nx_getpid_const_probe)
26const SF_EXIT_USAGE: i64 = 2
27const SF_EXIT_SURVIVED: i64 = 3 // the fleet's UNOBSERVABLE code: the signal never landed
28const SF_SIGNAL_MIN: i64 = 1
29const SF_SIGNAL_MAX: i64 = 64
30const SF_DIGIT_ZERO: i64 = 48
31const SF_DIGIT_NINE: i64 = 57
32const SF_DEC_BASE: i64 = 10
33const SF_NUM_CAP: i64 = 32
34
35func sf_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
36func sf_pn(v: i64) -> i64 {
37 if v == 0 { sf_puts("0" as *u8); return 0 }
38 var x: i64 = v
39 if x < 0 { sf_puts("-" as *u8); x = 0 - x }
40 let t: *u8 = sys_mmap(SF_NUM_CAP)
41 var n: i64 = 0
42 while x > 0 { t[n] = (SF_DIGIT_ZERO + (x % SF_DEC_BASE)) as u8; n = n + 1; x = x / SF_DEC_BASE }
43 let o: *u8 = sys_mmap(SF_NUM_CAP)
44 var i: i64 = n
45 var j: i64 = 0
46 while i > 0 { i = i - 1; o[j] = t[i]; j = j + 1 }
47 o[j] = 0 as u8
48 sf_puts(o)
49 return 0
50}
51// decimal parse; -1 when any byte is not a digit or the string is empty
52func sf_atoi(s: *u8) -> i64 {
53 var v: i64 = 0
54 var i: i64 = 0
55 while s[i] != (0 as u8) {
56 let c: i64 = s[i] as i64
57 if c < SF_DIGIT_ZERO { return 0 - 1 }
58 if c > SF_DIGIT_NINE { return 0 - 1 }
59 v = v * SF_DEC_BASE + (c - SF_DIGIT_ZERO)
60 i = i + 1
61 }
62 if i == 0 { return 0 - 1 }
63 return v
64}
65func sf_eq(a: *u8, b: *u8) -> i64 {
66 var i: i64 = 0
67 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
68 if b[i] != (0 as u8) { return 0 }
69 return 1
70}
71func sf_usage() -> i64 {
72 sf_puts("usage: nx_sigfixture <signal 1..64> | nx_sigfixture exit <code> -- dies by that signal on itself (SIGTERM=15 is a true signal death, SIGKILL=9 the watchdog class, SIGSEGV=11 measures the crash guard); a wait-status ruler must read 128+signal, never 0\n" as *u8)
73 return SF_EXIT_USAGE
74}
75
76func main(argc: i64, argv: *i64) -> i64 {
77 if argc < 2 { let u: i64 = sf_usage(); sys_exit(u); return u }
78 let a1: *u8 = argv[1] as *u8
79 if sf_eq(a1, "exit" as *u8) == 1 {
80 if argc < 3 { let u2: i64 = sf_usage(); sys_exit(u2); return u2 }
81 let code: i64 = sf_atoi(argv[2] as *u8)
82 if code < 0 { let u3: i64 = sf_usage(); sys_exit(u3); return u3 }
83 sf_puts("SIGFIXTURE control arm: exiting with code " as *u8); sf_pn(code); sf_puts(" -- a ruler must read exactly this\n" as *u8)
84 sys_exit(code)
85 return code
86 }
87 let sig: i64 = sf_atoi(a1)
88 if sig < SF_SIGNAL_MIN { let u4: i64 = sf_usage(); sys_exit(u4); return u4 }
89 if sig > SF_SIGNAL_MAX { let u5: i64 = sf_usage(); sys_exit(u5); return u5 }
90 let pid: i64 = __syscall(SF_SYS_GETPID_RV64, 0, 0, 0, 0, 0, 0)
91 sf_puts("SIGFIXTURE dying by signal " as *u8); sf_pn(sig); sf_puts(" on pid " as *u8); sf_pn(pid)
92 sf_puts(" -- a ruler that reads this run as rc=0 has lost the signal; the honest value is 128+signal\n" as *u8)
93 nx_kill(pid, sig)
94 sf_puts("SIGFIXTURE SURVIVED the signal -- it was blocked or ignored; exiting the UNOBSERVABLE code, never 0\n" as *u8)
95 sys_exit(SF_EXIT_SURVIVED)
96 return SF_EXIT_SURVIVED
97}