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}