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}