code wiki / _hdl_build / nx_dos_timeout_scan.nx

nx_dos_timeout_scan.nx source

↩ module page · 172 lines · 9373 B

1// nx_dos_timeout_scan.nx -- FLEET DoS-STARVATION GUARD (seq321). A single-threaded accept-loop daemon that 2// loop-reads to Content-Length can be starved FOREVER by one peer that declares a body it never finishes sending 3// = a one-request DoS (hostile OR merely buggy client). The 2-line cure is sys_set_socket_timeout(cfd,N) right 4// after accept (RCVTIMEO+SNDTIMEO). This guard SUPERVISES the class: it scans the organ tree and flags every 5// DAEMON source that has an accept() call but NO socket-timeout tooth, so the fix (fold-in-on-touch) is a visible 6// standing signal, not a one-time audit. Read-only (never patches). Pattern: nx_dup_source_check. 7// nx_dos_timeout_scan -- self-test the detector, then scan buildroot/runtime/_hdl_build; RED if vulns 8// license_tier: ORIGINAL Sovereign: nx_syscalls. expect_exit: 0 when clean. 9import "nx_syscalls.nx" 10const K_MAGIC_131072: i64 = 131072 11const K_MAGIC_524288: i64 = 524288 12const K_MAGIC_4096: i64 = 4096 13const K_MAGIC_524287: i64 = 524287 14 15func dw(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 16func dwn(v: i64) -> i64 { 17 var t: i64=v; if t<0 { sys_write(1,"-" as *u8,1); t=0-t } 18 let tm: *u8=sys_mmap(24); var k: i64=0; if t==0 { tm[0]=48 as u8; k=1 } 19 while t>0 { tm[k]=(48+(t%10)) as u8; t=t/10; k=k+1 } 20 let b: *u8=sys_mmap(24); var j: i64=0; while j<k { b[j]=tm[k-1-j]; j=j+1 } sys_write(1,b,k); return 0 21} 22func dslen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 23func ends_nx(nm: *u8) -> i64 { 24 let n: i64 = dslen(nm) 25 if n < 3 { return 0 } 26 if nm[n-3]==(46 as u8) { if nm[n-2]==(110 as u8) { if nm[n-1]==(120 as u8) { return 1 } } } 27 return 0 28} 29// substring present in buf[0..n)? 30func has_sub(buf: *u8, n: i64, needle: *u8) -> i64 { 31 let m: i64 = dslen(needle) 32 if m == 0 { return 0 } 33 var i: i64 = 0 34 while i + m <= n { 35 var k: i64=0; var hit: i64=1 36 while k < m { if buf[i+k] != needle[k] { hit=0; k=m } else { k=k+1 } } 37 if hit==1 { return 1 } 38 i=i+1 39 } 40 return 0 41} 42// is the basename a LIVE-daemon candidate? must contain "daemon" or "serve"; exclude test/gate/ref/tamper/mock/example. 43func name_is_daemon(nm: *u8) -> i64 { 44 let n: i64 = dslen(nm) 45 if has_sub(nm, n, "daemon" as *u8)==0 { if has_sub(nm, n, "serve" as *u8)==0 { return 0 } } 46 if has_sub(nm, n, "_test" as *u8)==1 { return 0 } 47 if has_sub(nm, n, "_gate" as *u8)==1 { return 0 } 48 if has_sub(nm, n, "_ref" as *u8)==1 { return 0 } 49 if has_sub(nm, n, "_tamper" as *u8)==1 { return 0 } 50 if has_sub(nm, n, "_mock" as *u8)==1 { return 0 } 51 if nm[0]==(95 as u8) { return 0 } // leading '_' = example/scratch 52 return 1 53} 54func join_path(buf: *u8, dir: *u8, name: *u8) -> i64 { 55 var o: i64=0; var i: i64=0 56 while dir[i]!=(0 as u8) { buf[o]=dir[i]; o=o+1; i=i+1 } 57 buf[o]=47 as u8; o=o+1 58 i=0; while name[i]!=(0 as u8) { buf[o]=name[i]; o=o+1; i=i+1 } 59 buf[o]=0 as u8 60 return 0 61} 62func read_file(path: *u8, buf: *u8, cap: i64) -> i64 { 63 let fd: i64 = sys_openat_rd(path) 64 if fd < 0 { return 0-1 } 65 var n: i64=0 66 var go: i64=1 67 while go==1 { let r: i64=sys_read(fd, ((buf as i64)+n) as *u8, cap-n); if r<=0 { go=0 } else { n=n+r } if n>=cap { go=0 } } 68 sys_close(fd) 69 return n 70} 71// a source is DoS-VULNERABLE iff it does an accept() but has NO socket-timeout tooth. 72func is_vuln(buf: *u8, n: i64) -> i64 { 73 var accepts: i64=0 74 if has_sub(buf, n, "sys_accept" as *u8)==1 { accepts=1 } 75 if has_sub(buf, n, "accept4" as *u8)==1 { accepts=1 } 76 if accepts==0 { return 0 } 77 if has_sub(buf, n, "set_socket_timeout" as *u8)==1 { return 0 } 78 if has_sub(buf, n, "SO_RCVTIMEO" as *u8)==1 { return 0 } 79 return 1 80} 81// scan a dir; flag daemon .nx that are vulnerable. verbose=1 prints each. returns the vuln count. 82func scan(dir: *u8, verbose: i64) -> i64 { 83 let fd: i64 = sys_openat_rd(dir) 84 if fd < 0 { return 0 } 85 let dbuf: *u8 = sys_mmap(K_MAGIC_131072) 86 let fbuf: *u8 = sys_mmap(K_MAGIC_524288) 87 // ★NEVER ALLOCATE IN A HOT LOOP -- HOIST THE BUFFER, DO NOT FREE IT PER ITERATION. The path buffer 88 // below used to be mmap'd INSIDE the per-entry loop and never released: 4 KiB of real VMA per 89 // daemon-named .nx in the tree, which is the RLIMIT_AS/vm.max_map_count exhaustion nx_syscalls 90 // documents. It is a fixed-size scratch fully rewritten by join_path each iteration, so one 91 // allocation serves every entry. Found by nx_mmapbal `hot` (page-backed AND in a loop). 92 let p: *u8 = sys_mmap(K_MAGIC_4096) 93 var vulns: i64=0 94 var go: i64=1 95 while go==1 { 96 let nr: i64 = sys_getdents64(fd, dbuf, K_MAGIC_131072) 97 if nr<=0 { go=0 } else { 98 var off: i64=0 99 while off<nr { 100 let rec: *u8 = (dbuf as i64 + off) as *u8 101 let ty: i64 = dirent_type(rec) 102 let nm: *u8 = dirent_name(rec) 103 if ty != 4 { 104 if ends_nx(nm)==1 { if name_is_daemon(nm)==1 { 105 join_path(p, dir, nm) 106 let fn: i64 = read_file(p, fbuf, K_MAGIC_524287) 107 if fn>0 { if is_vuln(fbuf, fn)==1 { 108 vulns=vulns+1 109 if verbose==1 { dw(" DoS-VULN (accept-loop, no socket timeout): " as *u8); dw(nm); dw("\n" as *u8) } 110 } } 111 } } 112 } 113 off = off + dirent_reclen(rec) 114 } 115 } 116 } 117 sys_close(fd) 118 // Release every mapping on the way out. These are ~656 KiB of real VMA per call; scan() is invoked 119 // per root, so leaving them mapped grows the process with each root scanned. 120 sys_munmap(p, K_MAGIC_4096) 121 sys_munmap(fbuf, K_MAGIC_524288) 122 sys_munmap(dbuf, K_MAGIC_131072) 123 return vulns 124} 125func wfile(path: *u8, s: *u8) -> i64 { let fd: i64=sys_openat_wr(path, 420); if fd>=0 { sys_write(fd, s, dslen(s)); sys_close(fd) } return 0 } 126// prove the detector: a daemon-named file with accept + NO timeout -> vuln; one WITH the timeout -> safe; 127// a non-daemon (no accept) -> ignored. scan must report exactly 1. 128func selftest() -> i64 { 129 sys_mkdir("/tmp/nxdos" as *u8, 0x1ed) 130 wfile("/tmp/nxdos/nx_bad_daemon.nx" as *u8, "func serve() { let cfd: i64 = sys_accept(lfd); handle(cfd) }\x00" as *u8) 131 wfile("/tmp/nxdos/nx_good_daemon.nx" as *u8, "func serve() { let cfd: i64 = sys_accept(lfd); sys_set_socket_timeout(cfd, 25); handle(cfd) }\x00" as *u8) 132 wfile("/tmp/nxdos/nx_util_serve.nx" as *u8, "func compute() { return 42 }\x00" as *u8) // no accept -> ignored 133 let v: i64 = scan("/tmp/nxdos" as *u8, 0) 134 if v==1 { return 1 } 135 dw(" SELFTEST FAIL: expected exactly 1 vuln, got "); dwn(v); dw("\n" as *u8) 136 return 0 137} 138func main() -> i64 { 139 dw("nx_dos_timeout_scan (fleet DoS-starvation guard -- flags accept-loop daemons missing the socket-timeout tooth, seq321)\n" as *u8) 140 if selftest() != 1 { dw("verdict=RED (detector self-test failed)\n" as *u8); sys_exit(1); return 1 } 141 dw(" self-test OK (accept-without-timeout caught, with-timeout + non-daemon ignored)\n" as *u8) 142 dw(" scanning buildroot/runtime/_hdl_build ...\n" as *u8) 143 let v: i64 = scan("buildroot/runtime/_hdl_build" as *u8, 1) 144 dw(" DoS-vulnerable daemon sources: "); dwn(v); dw("\n" as *u8) 145 let logf: i64 = sys_openat_wr("knowledge/status/dos_timeout_scan.log" as *u8, 0x1a4) 146 if logf>=0 { 147 // Lengths are DERIVED via dslen, never hand-counted. These two writes carried hardcoded 96 and 148 // 127 while the strings are 102 and 131 bytes, so BOTH log lines were truncated mid-word with no 149 // trailing newline -- which is exactly why nx_gate_rollup filed this row as "unreadable dialect": 150 // it was reading a line that stopped at "(seq3". dslen() already existed in THIS FILE, one 151 // function above the defect. A hand-counted length beside a literal is a magic number that 152 // silently truncates the moment either changes. 153 // The canonical verdict token goes LAST (the rollup anchors by POSITION); the rich domain row is 154 // kept as-is -- fix the emitter, never widen the reader's vocabulary. 155 if v==0 { 156 let gl: *u8 = "VERDICT=GREEN dos-timeout 0 accept-loop daemons missing the socket-timeout tooth (seq321 class clean) verdict=GREEN\n" as *u8 157 sys_write(logf, gl, dslen(gl)) 158 } else { 159 let vl: *u8 = "VERDICT=VULN dos-timeout accept-loop daemons missing socket-timeout tooth -- fold sys_set_socket_timeout in on next touch (seq321) verdict=RED\n" as *u8 160 sys_write(logf, vl, dslen(vl)) 161 } 162 sys_close(logf) 163 } 164 if v==0 { dw("verdict=GREEN (no DoS-vulnerable daemons)\n" as *u8); sys_exit(0); return 0 } 165 dw("verdict=VULN (fold sys_set_socket_timeout(cfd,N) after accept into each -- 2-line cure, seq321; supervised now)\n" as *u8) 166 // Exit NON-ZERO when vulns are present. This organ's own header declares "expect_exit: 0 when clean" 167 // and "RED if vulns", yet BOTH paths exited 0 -- so a scanner that FOUND a live DoS-starvation class 168 // reported success to every exit-code caller. A verdict that does not reach the exit code silently 169 // blesses the failure it just measured. 170 sys_exit(1) 171 return 1 172}