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}