code wiki / (root) / nx_lockwatch.nx

nx_lockwatch.nx source

↩ module page · 329 lines · 14685 B

1// nx_lockwatch.nx -- WHO HOLDS THIS PLANE'S LOCK, AND WHAT ARE THEY DOING? 2// 3// WHY THIS EXISTS. On 2026-08-06 a gate went RED with an UNCHANGED binary. The cause was not in the 4// diff at all: one worker sat in kernel state D, wchan=wait_for_commit -- a filesystem journal commit 5// -- HOLDING a plane lock, while its siblings sat in wchan=locks_lock_inode_wait. Establishing that 6// took reading /proc/locks by hand, mapping an inode back to a lock file, then reading 7// /proc/<pid>/wchan and /proc/<pid>/fd for every candidate. Three separate investigations that day 8// ran the same manual procedure, and ss_plane_lock's own BUSY message now TELLS an operator to do it: 9// "check /proc/<pid>/wchan: locks_lock_inode_wait = queued, wait_for_commit = wedged on disk" 10// ★TELLING SOMEONE TO RUN A PROCEDURE IS NOT THE SAME AS SHIPPING IT. A diagnostic that only exists 11// in a message is a diagnostic nobody runs at 3am. This is that procedure, as an organ. 12// 13// AND THE DISTINCTION IT DRAWS IS THE WHOLE POINT -- these look identical from outside and need 14// OPPOSITE responses: 15// QUEUED (wchan=locks_lock_inode_wait) -- healthy contention. Someone is working; wait. 16// WEDGED (state D, wchan=wait_for_commit / io_schedule / similar) -- the holder is stuck in the 17// kernel on I/O. Waiting will not help; the disk is the problem, and every writer behind 18// it is now blocked for as long as that lasts. 19// STALE -- a lock file whose recorded holder is GONE. flock releases on process death, so this 20// should be impossible; if it ever prints, the assumption is wrong and that matters more 21// than anything else on the report. 22// 23// READ-ONLY. Opens nothing but /proc and the lock files' metadata; writes nothing, locks nothing -- 24// deliberately, because an instrument that took the lock it is diagnosing would be the defect. 25// usage: nx_lockwatch [store-dir] default knowledge/store 26// exit: 0 = no held plane locks, 1 = at least one held, 2 = usage/unreadable 27// license_tier: ORIGINAL No hw writes (Rule 26). 28import "nx_syscalls.nx" 29 30const LW_STORE: *u8 = "knowledge/store" 31const LW_LOCKS: *u8 = "/proc/locks" 32const LW_DIRBUF: i64 = 262144 33const LW_LOCKBUF: i64 = 262144 34const LW_PATH: i64 = 512 35const LW_SMALL: i64 = 256 36const LW_STATBUF: i64 = 256 37const LW_OFF_INO: i64 = 8 // x86-64 struct stat: st_dev@0, st_ino@8 (st_size@48 is the 38 // offset the rest of the estate already relies on, same layout) 39const LW_MAXLOCKS: i64 = 4096 40const LW_ZERO: i64 = 48 41const LW_NINE: i64 = 57 42const LW_NL: i64 = 10 43const LW_COLON: i64 = 58 44const LW_SP: i64 = 32 45const LW_B10: i64 = 10 46const LW_EXIT_CLEAR: i64 = 0 47const LW_EXIT_HELD: i64 = 1 48const LW_EXIT_USAGE: i64 = 2 49 50func lw_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 51func lw_n(v: i64) -> i64 { 52 if v == 0 { lw_w("0" as *u8); return 0 } 53 var x: i64 = v 54 if x < 0 { lw_w("-" as *u8); x = 0 - x } 55 let b: *u8 = sys_mmap(32) 56 var i: i64 = 0 57 while x > 0 { b[i] = ((x % LW_B10) + LW_ZERO) as u8; x = x / LW_B10; i = i + 1 } 58 while i > 0 { i = i - 1; sys_write(1, ((b as i64) + i) as *u8, 1) } 59 return 0 60} 61func lw_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 62 63// /proc files report st_size 0, so a stat-then-read slurp returns nothing. Read until EOF instead. 64// âš This is exactly the trap that makes /proc look empty to any helper written for regular files. 65func lw_slurp(path: *u8, buf: *u8, cap: i64) -> i64 { 66 let fd: i64 = sys_openat_rd(path) 67 if fd < 0 { return 0 - 1 } 68 var off: i64 = 0 69 var go: i64 = 1 70 while go == 1 { 71 let n: i64 = sys_read(fd, ((buf as i64) + off) as *u8, cap - off - 1) 72 if n <= 0 { go = 0 } else { 73 off = off + n 74 if off >= cap - 1 { go = 0 } 75 } 76 } 77 sys_close(fd) 78 buf[off] = 0 as u8 79 return off 80} 81 82// 1 if `nm` ends with `suf` 83func lw_ends(nm: *u8, suf: *u8) -> i64 { 84 let ln: i64 = lw_len(nm) 85 let ls: i64 = lw_len(suf) 86 if ln < ls { return 0 } 87 var i: i64 = 0 88 while i < ls { if nm[ln - ls + i] != suf[i] { return 0 } i = i + 1 } 89 return 1 90} 91 92// parse the decimal starting at buf[i]; advances nothing, returns the value (0 if none) 93func lw_num_at(buf: *u8, i: i64, n: i64) -> i64 { 94 var v: i64 = 0 95 var j: i64 = i 96 var go: i64 = 1 97 while go == 1 { 98 if j >= n { go = 0 } else { 99 let c: i64 = buf[j] as i64 100 if c >= LW_ZERO { if c <= LW_NINE { v = v * LW_B10 + (c - LW_ZERO); j = j + 1 } else { go = 0 } } else { go = 0 } 101 } 102 } 103 return v 104} 105 106// "FLOCK" at b[i..i+5)? Flat, one comparison per statement with an early return -- the shape the rest 107// of the estate uses. A five-deep nested-if with a multi-statement innermost block silently matched 108// NOTHING here while a bare byte compare in the same file found 61 'F' bytes: the primitive was fine, 109// the SHAPE was not. Do not re-nest this for brevity. 110// Start offset of whitespace-separated field `idx` (0-based) in [p, stop), or -1. 111// ONE loop, a state flag, no break-by-overshoot. NishiLang has no `break`, and faking one by setting 112// the loop variable past its bound is what corrupted the previous parser. 113func lw_field_start(b: *u8, p: i64, stop: i64, idx: i64) -> i64 { 114 var i: i64 = p 115 var f: i64 = 0 - 1 116 var inword: i64 = 0 117 var found: i64 = 0 - 1 118 while i < stop { 119 var issp: i64 = 0 120 if b[i] == (32 as u8) { issp = 1 } 121 if issp == 0 { 122 if inword == 0 { 123 inword = 1 124 f = f + 1 125 if f == idx { if found < 0 { found = i } } 126 } 127 } 128 if issp == 1 { inword = 0 } 129 i = i + 1 130 } 131 return found 132} 133 134// /proc/locks field 5 is "major:minor:inode" -- take the digits after the LAST colon. 135func lw_inode_of(b: *u8, fstart: i64, stop: i64) -> i64 { 136 var i: i64 = fstart 137 var lastc: i64 = 0 - 1 138 var go: i64 = 1 139 while go == 1 { 140 if i >= stop { go = 0 } else { 141 if b[i] == (32 as u8) { go = 0 } else { 142 if b[i] == (58 as u8) { lastc = i } 143 i = i + 1 144 } 145 } 146 } 147 if lastc < 0 { return 0 } 148 return lw_num_at(b, lastc + 1, stop) 149} 150 151func lw_at_flock(b: *u8, i: i64) -> i64 { 152 if b[i] != (70 as u8) { return 0 } 153 if b[i + 1] != (76 as u8) { return 0 } 154 if b[i + 2] != (79 as u8) { return 0 } 155 if b[i + 3] != (67 as u8) { return 0 } 156 if b[i + 4] != (75 as u8) { return 0 } 157 return 1 158} 159 160func main(argc: i64, argv: *i64) -> i64 { 161 var store: *u8 = LW_STORE 162 if argc >= 2 { store = argv[1] as *u8 } 163 164 let lbuf: *u8 = sys_mmap(LW_LOCKBUF) 165 let ln: i64 = lw_slurp(LW_LOCKS, lbuf, LW_LOCKBUF) 166 if ln <= 0 { 167 lw_w("LOCKWATCH: cannot read /proc/locks -- no lock visibility on this host\n" as *u8) 168 sys_exit(LW_EXIT_USAGE) 169 return LW_EXIT_USAGE 170 } 171 172 // --- collect FLOCK (pid, inode) pairs --- 173 let pids: *i64 = sys_mmap(8 * LW_MAXLOCKS) as *i64 174 let inos: *i64 = sys_mmap(8 * LW_MAXLOCKS) as *i64 175 var nlocks: i64 = 0 176 var nflock: i64 = 0 177 var nlines: i64 = 0 178 var p: i64 = 0 179 while p < ln { 180 var stop: i64 = p 181 var seek: i64 = 1 182 while seek == 1 { 183 if stop >= ln { seek = 0 } else { 184 if lbuf[stop] == (LW_NL as u8) { seek = 0 } else { stop = stop + 1 } 185 } 186 } 187 // is it a FLOCK row? scan the line for "FLOCK" 188 var isf: i64 = 0 189 var s: i64 = p 190 let flim: i64 = stop - 5 191 while s <= flim { 192 if lw_at_flock(lbuf, s) == 1 { isf = 1 } 193 s = s + 1 194 } 195 if isf == 1 { 196 nflock = nflock + 1 197 // FIELD EXTRACTION VIA FLAT HELPERS. The previous version walked fields with nested 198 // while-loops that used "k = stop + 1" to break and then clamped back -- so it never 199 // landed on a field start and every pid/inode came out 0. Detection said 18 FLOCK lines 200 // while the report said 0 locks, because my own counter conflated DETECTED with PARSED. 201 // >>A COUNTER THAT MEASURES TWO STAGES AT ONCE CANNOT TELL YOU WHICH ONE FAILED.<< 202 let f4: i64 = lw_field_start(lbuf, p, stop, 4) 203 let f5: i64 = lw_field_start(lbuf, p, stop, 5) 204 var pidv: i64 = 0 205 var inov: i64 = 0 206 if f4 >= 0 { pidv = lw_num_at(lbuf, f4, stop) } 207 if f5 >= 0 { inov = lw_inode_of(lbuf, f5, stop) } 208 if pidv > 0 { if inov > 0 { if nlocks < LW_MAXLOCKS { 209 pids[nlocks] = pidv 210 inos[nlocks] = inov 211 nlocks = nlocks + 1 212 } } } 213 } 214 nlines = nlines + 1 215 p = stop + 1 216 } 217 218 lw_w("=== nx_lockwatch -- who holds a plane lock, and what are they doing ===\n" as *u8) 219 var fcount: i64 = 0 220 var fi2: i64 = 0 221 while fi2 < ln { if lbuf[fi2] == (70 as u8) { fcount = fcount + 1 } fi2 = fi2 + 1 } 222 lw_w(" FLOCK rows visible in /proc/locks: " as *u8); lw_n(nlocks); lw_w("\n" as *u8) 223 224 // --- walk the store for *slock / *plock and match by inode --- 225 let fd: i64 = sys_openat_rd(store) 226 if fd < 0 { 227 lw_w("LOCKWATCH: cannot open " as *u8); lw_w(store) 228 lw_w(" -- run from the nishihost CWD\n" as *u8) 229 sys_exit(LW_EXIT_USAGE) 230 return LW_EXIT_USAGE 231 } 232 let dbuf: *u8 = sys_mmap(LW_DIRBUF) 233 let path: *u8 = sys_mmap(LW_PATH) 234 let stbuf: *u8 = sys_mmap(LW_STATBUF) 235 let wbuf: *u8 = sys_mmap(LW_SMALL) 236 let cbuf: *u8 = sys_mmap(LW_SMALL) 237 var files: i64 = 0 238 var held: i64 = 0 239 var wedged: i64 = 0 240 var go: i64 = 1 241 while go == 1 { 242 let dn: i64 = sys_getdents64(fd, dbuf, LW_DIRBUF) 243 if dn <= 0 { go = 0 } else { 244 var off: i64 = 0 245 while off < dn { 246 let rec: *u8 = ((dbuf as i64) + off) as *u8 247 let rl: i64 = dirent_reclen(rec) 248 if rl <= 0 { off = dn } else { 249 let nm: *u8 = dirent_name(rec) 250 var islock: i64 = 0 251 if lw_ends(nm, "slock" as *u8) == 1 { islock = 1 } 252 if lw_ends(nm, "plock" as *u8) == 1 { islock = 1 } 253 if islock == 1 { 254 files = files + 1 255 var o: i64 = 0 256 var t: i64 = 0 257 while store[t] != (0 as u8) { path[o] = store[t]; o = o + 1; t = t + 1 } 258 path[o] = 47 as u8 259 o = o + 1 260 t = 0 261 while nm[t] != (0 as u8) { path[o] = nm[t]; o = o + 1; t = t + 1 } 262 path[o] = 0 as u8 263 let strc: i64 = sys_fstatat(path, stbuf) 264 if strc == 0 { 265 // ⚠NAMED POINTER, NOT AN INLINE CAST-AND-INDEX. `(stbuf as *i64)[n]` types 266 // as a POINTER in this compiler, so the inode silently read as an address and 267 // NEVER matched -- held=0 while /proc/locks plainly showed the lock held. 268 let stq2: *i64 = stbuf as *i64 269 let ino: i64 = stq2[LW_OFF_INO / 8] 270 var m: i64 = 0 271 while m < nlocks { 272 if inos[m] == ino { 273 held = held + 1 274 let hp: i64 = pids[m] 275 // read the holder's wchan + comm; a missing wchan means the pid is GONE 276 var wo: i64 = 0 277 var wt: i64 = 0 278 let pfx: *u8 = "/proc/" as *u8 279 while pfx[wt] != (0 as u8) { path[wo] = pfx[wt]; wo = wo + 1; wt = wt + 1 } 280 // itoa the pid inline 281 var pv: i64 = hp 282 let tmp: *u8 = sys_mmap(32) 283 var ti: i64 = 0 284 while pv > 0 { tmp[ti] = ((pv % LW_B10) + LW_ZERO) as u8; pv = pv / LW_B10; ti = ti + 1 } 285 while ti > 0 { ti = ti - 1; path[wo] = tmp[ti]; wo = wo + 1 } 286 let sfx: *u8 = "/wchan" as *u8 287 wt = 0 288 while sfx[wt] != (0 as u8) { path[wo] = sfx[wt]; wo = wo + 1; wt = wt + 1 } 289 path[wo] = 0 as u8 290 let wn: i64 = lw_slurp(path, wbuf, LW_SMALL) 291 lw_w(" HELD " as *u8); lw_w(nm) 292 lw_w(" pid=" as *u8); lw_n(hp) 293 lw_w(" wchan=" as *u8) 294 if wn > 0 { lw_w(wbuf) } else { lw_w("<gone>" as *u8) } 295 // classify: this is the judgement the manual procedure was for 296 if wn > 0 { 297 if lw_ends(wbuf, "commit" as *u8) == 1 { 298 wedged = wedged + 1 299 lw_w(" <-- WEDGED ON DISK: waiting will not help, every writer behind it is blocked" as *u8) 300 } 301 } 302 if wn <= 0 { 303 lw_w(" <-- STALE HOLDER: flock should release on process death; investigate, this should be impossible" as *u8) 304 } 305 lw_w("\n" as *u8) 306 } 307 m = m + 1 308 } 309 } 310 } 311 off = off + rl 312 } 313 } 314 } 315 } 316 sys_close(fd) 317 318 lw_w("NX-LOCKWATCH lock_files=" as *u8); lw_n(files) 319 lw_w(" held=" as *u8); lw_n(held) 320 lw_w(" wedged=" as *u8); lw_n(wedged) 321 if held > 0 { 322 lw_w(" verdict=HELD (a plane lock is currently held; queued waiters are normal, WEDGED is not)\n" as *u8) 323 sys_exit(LW_EXIT_HELD) 324 return LW_EXIT_HELD 325 } 326 lw_w(" verdict=CLEAR (no plane lock is held right now)\n" as *u8) 327 sys_exit(LW_EXIT_CLEAR) 328 return LW_EXIT_CLEAR 329}