code wiki / _hdl_build / nx_doc_constscan.nx
nx_doc_constscan.nx source
↩ module page · 122 lines · 6086 B
1// nx_doc_constscan.nx -- static detector for the LM-030 footgun (sibling of nx_doc_ubscan/LM-031): indexing a
2// `const *T` global DIRECTLY in expression position -- CONST[i] -- silently miscompiles (it crashed nx_cc with a
3// parser desync historically; today it returns a garbage value, measured: nx_doc_demo_const SM_LIB[2] ran 156 for
4// expected 99). The fix-by-construction: bind the const to a local first (`let p = CONST; p[i]`) or pass it to a
5// func that indexes its param. 2-pass: (1) collect every `const NAME: *...` pointer-const name; (2) flag any
6// `NAME[` direct-index usage (the declaration is `NAME:` so it never matches). LINEAGE: the NISHI RECYCLER's
7// hardening for the memory-safety/UB critiques (recyc_crit_ub.raw / recyc_crit_memsafety.raw). READ-ONLY. ORIGINAL
8import "nx_syscalls.nx"
9import "nx_doc_heal_let.nx"
10
11func cs_skipws(src: *u8, n: i64, p0: i64) -> i64 {
12 var p: i64=p0; var go: i64=1
13 while go==1 { if p<n { if src[p]==(32 as u8){p=p+1} else { if src[p]==(9 as u8){p=p+1} else { if src[p]==(10 as u8){p=p+1} else {go=0} } } } else {go=0} }
14 return p
15}
16// 1 if 'const' is a whole word at i.
17func cs_is_const_at(src: *u8, n: i64, i: i64) -> i64 {
18 if i+5 > n { return 0 }
19 if src[i]!=(99 as u8) { return 0 }
20 if src[i+1]!=(111 as u8) { return 0 }
21 if src[i+2]!=(110 as u8) { return 0 }
22 if src[i+3]!=(115 as u8) { return 0 }
23 if src[i+4]!=(116 as u8) { return 0 }
24 if i>0 { if dhl_isident(src[i-1] as i64)==1 { return 0 } }
25 if dhl_isident(src[i+5] as i64)==1 { return 0 }
26 return 1
27}
28// if a `const NAME: *...` pointer-const decl starts at i, record NAME into names[]/nlens[].
29func cs_record(src: *u8, n: i64, i: i64, names: *u8, nlens: *i64, ncountp: *i64, capn: i64) -> i64 {
30 var p: i64 = cs_skipws(src, n, i+5)
31 if p>=n { return 0 }
32 if dhl_isident(src[p] as i64)==0 { return 0 }
33 let nb: *u8 = sys_mmap(64)
34 let nl: i64 = dhl_name_at(src, n, p, nb)
35 if nl >= 63 { return 0 }
36 var q: i64 = cs_skipws(src, n, p+nl)
37 if q>=n { return 0 }
38 if src[q]!=(58 as u8) { return 0 } // ':'
39 q = cs_skipws(src, n, q+1)
40 if q>=n { return 0 }
41 if src[q]!=(42 as u8) { return 0 } // '*' -> a pointer const
42 let cnt: i64 = ncountp[0]
43 if cnt >= capn { return 0 }
44 var k: i64=0; while k<nl { names[cnt*64+k]=nb[k]; k=k+1 }
45 nlens[cnt]=nl; ncountp[0]=cnt+1
46 return 0
47}
48// 1 if src[from..from+len) equals a recorded const-pointer name.
49func cs_is_const_name(names: *u8, nlens: *i64, ncount: i64, src: *u8, from: i64, len: i64) -> i64 {
50 var e: i64=0
51 while e < ncount {
52 if nlens[e]==len { var k: i64=0; var m: i64=1; while k<len { if names[e*64+k]!=src[from+k]{m=0;k=len} else {k=k+1} } if m==1 { return 1 } }
53 e=e+1
54 }
55 return 0
56}
57// flag the first direct index of a const-pointer (CONST[...]). returns offset or -1; fills outname.
58func cs_scan(src: *u8, n: i64, outname: *u8, outpos: *i64) -> i64 {
59 let names: *u8 = sys_mmap(64*64)
60 let nlens: *i64 = sys_mmap(64*8) as *i64
61 let ncountp: *i64 = sys_mmap(16) as *i64; ncountp[0]=0
62 // pass 1: collect const-pointer names
63 var i: i64=0
64 while i < n {
65 if cs_is_const_at(src, n, i)==1 { cs_record(src, n, i, names, nlens, ncountp, 64); i=i+5 } else { i=i+1 }
66 }
67 let ncount: i64 = ncountp[0]
68 // pass 2: find a NAME[ usage (the decl is NAME: so it never matches '[')
69 let nb: *u8 = sys_mmap(64)
70 i=0
71 while i < n {
72 // LINE-COMMENT SKIP (2026-07-30). Without this the scanner flags PROSE: 4 of 6 tree-wide hits were
73 // inside // comments -- including this detector's OWN header, its gate's header, and an organ where
74 // the author had already documented the workaround. A 67pct false-positive rate is exactly how an
75 // instrument earns the right to be ignored, and this one was never deployed at all. Worse, it meant
76 // ANYONE WHO DOCUMENTS THE LANDMINE TRIPS THE DETECTOR -- it punished writing about the bug.
77 // NishiLang has no `continue`, so the skip MUST suppress the rest of this iteration. Without the
78 // flag the body fell through to `i = i + 1`, landing on the SECOND '/' of the comment opener -- at
79 // which point src[i+1] is no longer '/', the skip can never re-fire, and the scanner walks straight
80 // into the comment text. That is why the first version of this fix appeared to do nothing.
81 var skipped: i64 = 0
82 if src[i] == (47 as u8) {
83 if i + 1 < n {
84 if src[i + 1] == (47 as u8) {
85 skipped = 1
86 var cj: i64 = i + 2
87 var cstop: i64 = n
88 var cgo: i64 = 1
89 while cgo == 1 {
90 if cj >= n {
91 cgo = 0
92 } else {
93 if src[cj] == (10 as u8) {
94 cstop = cj + 1
95 cgo = 0
96 } else {
97 cj = cj + 1
98 }
99 }
100 }
101 i = cstop
102 }
103 }
104 }
105 var isstart: i64=0
106 // bounds guard: the comment skip above can advance i to n, so src[i] must not be read blind.
107 if skipped == 0 { if i < n { if dhl_isident(src[i] as i64)==1 { if i==0 { isstart=1 } else { if dhl_isident(src[i-1] as i64)==0 { isstart=1 } } } } }
108 if isstart==1 {
109 let nl: i64 = dhl_name_at(src, n, i, nb)
110 if cs_is_const_name(names, nlens, ncount, src, i, nl)==1 {
111 let a: i64 = i+nl
112 if a<n { if src[a]==(91 as u8) { // immediately '[' -> direct index
113 var c: i64=0; while c<nl { outname[c]=nb[c]; c=c+1 } outname[nl]=0 as u8
114 outpos[0]=i
115 return i
116 } }
117 }
118 i = i + nl
119 } else { if skipped == 0 { i=i+1 } }
120 }
121 return 0-1
122}