code wiki / _hdl_build / nx_boundedprobe.nx
nx_boundedprobe.nx source
↩ module page · 134 lines · 6348 B
1// nx_boundedprobe -- DOES THE ESTATE'S BOUNDED RUNNER ACTUALLY BOUND A SUBJECT THAT FORKS?
2//
3// gk_run_capture_ms exists because a hanging subject starved three estate sweeps (2026-08-07). Its
4// watchdog kills the direct child with nx_kill(pid, 9). But the child's stdout/stderr are dup3'd
5// onto a PIPE, and any grandchild it forks INHERITS those descriptors. Kill the child alone and the
6// grandchild still holds the write end, so the parent's blocking read never sees EOF -- the watchdog
7// fires, the child dies, and the sweep starves anyway.
8//
9// That is a hypothesis. This organ is the experiment, and it is self-contained so there is no
10// /bin/sh on the path and no second organ to keep in sync:
11// --forkling : fork a 60s sleeper, then exit IMMEDIATELY (the direct child is gone in ~0ms)
12// (no args) : run MYSELF as that subject under a 2000ms bound, and report what came back
13// Wall time is measured by the caller. ~2s => the runner bounds a forking subject. ~60s => it does
14// not, and every gate in the fleet that runs a forking subject is exposed to the same starvation.
15// license_tier: ORIGINAL No hw writes (Rule 26). Read-only apart from its own child process.
16import "nx_gatekit_lib.nx"
17const BP_MAGIC_5000: i64 = 5000
18
19const BP_BOUND_MS: i64 = 2000
20const BP_SLEEP_MS: i64 = 60000
21const BP_CAP: i64 = 65536
22// payload for the positive control: big enough to span several read() calls, so a truncating
23// deadline shows up as a short outlen rather than hiding inside one buffer
24const BP_EMIT: i64 = 40000
25
26func main(argc: i64, argv: *i64) -> i64 {
27 if argc > 1 {
28 let a1: *u8 = argv[1] as *u8
29 let fl: *u8 = "--forkling" as *u8
30 let pg: *u8 = "--pgcheck" as *u8
31 // Does sys_setpgid actually work on this target? A wrong syscall number fails SILENTLY:
32 // the child stays in the parent's group, kill(-pid) finds no such group, and the "fix"
33 // reproduces the original behaviour exactly. Measure the conjunct, do not infer it.
34 let kc: *u8 = "--killcheck" as *u8
35 // Does nx_kill ITSELF work here? In the forking experiment the direct child exits on its
36 // own, so the watchdog's kill was a no-op either way -- I never actually observed nx_kill
37 // killing anything. If its number is as wrong as setpgid's, the bound is fictional.
38 // ASSERT THE FIXTURE REACHED THE CONDITION: fork a subject that will NOT exit on its own.
39 if gk_streq(a1, kc) == 1 {
40 let p: i64 = sys_fork()
41 if p == 0 { sys_sleep_ms(BP_SLEEP_MS); sys_exit(0) }
42 sys_sleep_ms(200)
43 let krc: i64 = nx_kill(p, 9)
44 let st2: *i64 = sys_mmap(16) as *i64
45 st2[0] = 0
46 sys_wait4(p, st2, 0)
47 let ob3: *u8 = sys_mmap(256)
48 var o3: i64 = gk_cat(ob3, 0, "nx_kill rc=" as *u8)
49 o3 = gk_catn(ob3, o3, krc)
50 o3 = gk_cat(ob3, o3, " reaped (if this printed fast, kill works)" as *u8)
51 ob3[o3] = 10 as u8
52 o3 = o3 + 1
53 gk_say(ob3, o3)
54 return 0
55 }
56 if gk_streq(a1, pg) == 1 {
57 let rc: i64 = sys_setpgid(0, 0)
58 let ob2: *u8 = sys_mmap(256)
59 var o2: i64 = gk_cat(ob2, 0, "SYS_SETPGID=" as *u8)
60 // print the CONST the build actually selected. -38 tells me the call failed; only this
61 // tells me WHICH branch of the @ifdef compiled, i.e. whether TARGET_X86_64 is even set.
62 o2 = gk_catn(ob2, o2, SYS_SETPGID)
63 o2 = gk_cat(ob2, o2, " setpgid(0,0) rc=" as *u8)
64 o2 = gk_catn(ob2, o2, rc)
65 ob2[o2] = 10 as u8
66 o2 = o2 + 1
67 gk_say(ob2, o2)
68 return 0
69 }
70 if gk_streq(a1, fl) == 1 {
71 let p: i64 = sys_fork()
72 if p == 0 {
73 // grandchild: holds the inherited pipe write-end open well past the bound
74 sys_sleep_ms(BP_SLEEP_MS)
75 sys_exit(0)
76 }
77 // direct child returns at once -- the runner's wait4 is satisfied almost immediately,
78 // so anything that hangs after this is the PIPE, not the process
79 return 0
80 }
81
82 // POSITIVE CONTROL. A poll deadline that truncates a healthy subject's output would be a
83 // worse defect than the hang it fixes, and every negative test would still pass.
84 // --emit : write a known payload and exit cleanly (a NON-forking, well-behaved subject)
85 // --normal : run --emit under a generous bound; outlen MUST equal the full payload
86 let em: *u8 = "--emit" as *u8
87 if gk_streq(a1, em) == 1 {
88 let pay: *u8 = sys_mmap(BP_EMIT + 16)
89 var q: i64 = 0
90 while q < BP_EMIT { pay[q] = 65 as u8; q = q + 1 }
91 gk_say(pay, BP_EMIT)
92 return 0
93 }
94 let nm2: *u8 = "--normal" as *u8
95 if gk_streq(a1, nm2) == 1 {
96 let self2: *u8 = argv[0] as *u8
97 let buf2: *u8 = sys_mmap(BP_CAP + 16)
98 let ol2: *i64 = sys_mmap(16) as *i64
99 ol2[0] = 0
100 let rc2: i64 = gk_run_capture_ms(self2, em, 0 as *u8, 0 as *u8, 0 as *u8, BP_MAGIC_5000, buf2, BP_CAP, ol2)
101 let ob4: *u8 = sys_mmap(256)
102 var o4: i64 = gk_cat(ob4, 0, "normal rc=" as *u8)
103 o4 = gk_catn(ob4, o4, rc2)
104 o4 = gk_cat(ob4, o4, " outlen=" as *u8)
105 o4 = gk_catn(ob4, o4, ol2[0])
106 o4 = gk_cat(ob4, o4, " expected=" as *u8)
107 o4 = gk_catn(ob4, o4, BP_EMIT)
108 ob4[o4] = 10 as u8
109 o4 = o4 + 1
110 gk_say(ob4, o4)
111 if ol2[0] != BP_EMIT { return 1 }
112 return 0
113 }
114 }
115
116 let self: *u8 = argv[0] as *u8
117 let fl2: *u8 = "--forkling" as *u8
118 let buf: *u8 = sys_mmap(BP_CAP + 16)
119 let ol: *i64 = sys_mmap(16) as *i64
120 ol[0] = 0
121 let rc: i64 = gk_run_capture_ms(self, fl2, 0 as *u8, 0 as *u8, 0 as *u8, BP_BOUND_MS, buf, BP_CAP, ol)
122
123 let ob: *u8 = sys_mmap(512)
124 var o: i64 = gk_cat(ob, 0, "boundedprobe rc=" as *u8)
125 o = gk_catn(ob, o, rc)
126 o = gk_cat(ob, o, " outlen=" as *u8)
127 o = gk_catn(ob, o, ol[0])
128 o = gk_cat(ob, o, " bound_ms=" as *u8)
129 o = gk_catn(ob, o, BP_BOUND_MS)
130 ob[o] = 10 as u8
131 o = o + 1
132 gk_say(ob, o)
133 return 0
134}