code wiki / _hdl_build / nx_dstate.nx

nx_dstate.nx source

↩ module page · 767 lines · 41808 B

1// nx_dstate.nx -- WHO IS BLOCKED. The D-state (uninterruptible I/O) roster, for the exact moment a box 2// is saturated and every other attribution tool has refused. 3// 4// WHY IT EXISTS, MEASURED 2026-08-14: nx_procchurn returned RED sev=2 with `procs_running=2 5// procs_blocked=4` and cpu busy=281 permil -- its own note reads "blocked >> running = I/O or lock bound, 6// NOT compute; more CPU would not help". It names the CONDITION and cannot name the PRODUCER. The tool 7// that names producers, nx_ctxtop, then REFUSED: `load_centi=1342 max=800 -- host already saturated`. 8// -- A DIAGNOSTIC WHOSE ADMISSION CEILING IS THE CONDITION IT DIAGNOSES IS UNAVAILABLE EXACTLY WHEN IT 9// IS NEEDED. The refusal is CORRECT (its lib's law: a diagnostic that cannot refuse to run is a load 10// generator with good intentions) -- the gap is that nothing cheap enough to run under saturation exists. 11// 12// WHY THIS ONE CAN RUN WHERE ctxtop CANNOT -- a COST argument, not a wish: 13// nx_ctxtop measures a RATE, and a rate needs TWO samples separated in time: it walks all of /proc 14// TWICE around a 3000 ms window. D-state is a STATE, not a rate, so ONE sample answers it -- one pass, 15// no window, no fork, and /proc/<pid>/stat is a SINGLE small read per pid (ctxtop reads status twice). 16// Roughly half the syscalls and none of the wait. 17// -- A STATE NEEDS ONE SAMPLE; ONLY A DERIVATIVE NEEDS TWO. Paying for a window to answer an 18// instantaneous question is how a cheap question inherits an expensive tool's admission ceiling. 19// 20// ADMISSION IS KEPT, NOT DROPPED, and its ceiling is DERIVED rather than picked: equal marginal impact 21// at half the cost admits at twice the load, so the default is 2x the incumbent's 800. It is POLICY, so 22// it lives in knowledge/dstate.conf and is PRINTED on every run -- a ceiling nobody can see is a magic 23// number wherever it happens to be stored. 24// 25// nx_dstate [conf-path] 26// 27// -- "NOTHING IS BLOCKED" AND "I REFUSED TO LOOK" MUST NEVER SHARE AN OUTCOME, so refusal has its own 28// exit code and its own line. Codes MATCH the sibling (nx_ctxtop): 3 UNMEASURED, 4 REFUSED. 29// exit 0 sampled | 3 UNMEASURED (cannot read /proc) | 4 ADMISSION-REFUSED (load above the ceiling) 30// license_tier: ORIGINAL. Read-only. No hw writes (Rule 26). 31import "nx_ctxtop_lib.nx" 32import "nx_resmon_lib.nx" // rm_conf: the estate's ONE line-anchored conf reader (the hand parser that lived here was a second copy of it) 33 34const DS_DIRBUF: i64 = 65536 35const DS_STATBUF: i64 = 4096 36const DS_LOADBUF: i64 = 128 37const DS_CONFBUF: i64 = 4096 38const DS_MAXD: i64 = 512 39const DS_NAMEMAX: i64 = 64 40// dirent64: d_reclen is a u16 at byte 16, the NUL-terminated name starts at byte 19. 41const DS_RECLEN_OFF: i64 = 16 42const DS_NAME_OFF: i64 = 19 43const DS_EXIT_UNMEASURED: i64 = 3 44const DS_EXIT_REFUSED: i64 = 4 45// DERIVED, not picked: nx_ctxtop admits at 800 centi-load for a TWO-pass walk around a 3 s window. This 46// organ is one pass with no window (~half the syscalls, none of the wait), so equal marginal impact 47// admits at twice the load. Overridable in knowledge/dstate.conf (or ../knowledge/ from buildroot) as 48// `admit_load_centi=<n>`; the report line prints src=conf|builtin-default and path=<file actually read>. 49const DS_ADMIT_DEFAULT: i64 = 1600 50// ★★TWO-SAMPLE MODE EXISTS BECAUSE THE ONE-SAMPLE CAVEAT IS ADVICE, AND ADVICE GETS SKIPPED -- I skipped 51// my own, twice, and filed a false leak both times. `twice` takes the second sample ITSELF and reports 52// PERSISTENT vs TRANSIENT, so the correct reading stops depending on anyone remembering to do it. 53// ★THE GAP MUST EXCEED THE SLOWEST REAPER'S CYCLE, or a transient zombie appears in BOTH samples and is 54// reported as PERSISTENT. The default is derived from the longest reap tick actually observed in this 55// estate: nx_torrent_get drains on a 2000 ms tick, so 3000 ms clears it with margin. THIS ORGAN CANNOT 56// KNOW EVERY REAPER'S PERIOD, so the gap is an ARGUMENT and is PRINTED -- a PERSISTENT verdict is only 57// as strong as a gap that outlasts the reaper in question. 58const DS_GAP_DEFAULT_MS: i64 = 3000 59const DS_CONF: *u8 = "knowledge/dstate.conf" 60// buildroot-anchored callers (nx_sov_build_run anchors CWD to buildroot/) see the OTHER knowledge tree; the 61// conf lives in nishihost/knowledge/, which is ../knowledge/ from there -- the same two-step nx_build_admit 62// uses for build_admit.conf. One file, two vantages, never two files. 63const DS_CONF_UP: *u8 = "../knowledge/dstate.conf" 64// rm_conf returns its default when a key is absent; a NEGATIVE default is unreachable for a ceiling or a 65// clock rate, so it discriminates "absent" from "present and equal to the built-in" -- which a default equal 66// to DS_ADMIT_DEFAULT could not. 67const DS_CONF_MISS: i64 = 0 - 1 68// ★★AGE IS THE DIFFERENCE BETWEEN A LIVE FAILURE AND A BUSY ONE. "6 blocked" is ambiguous; "6 blocked, 69// oldest 47 minutes" is a diagnosis. Established the hard way: three separate runs were needed to prove 70// the same pids persisted, when the kernel had the answer in ONE read the whole time. 71// starttime is field 22 of /proc/<pid>/stat, in clock ticks since boot. 72const DS_STARTTIME_FIELD: i64 = 22 73// ★★A COUNT WITHOUT A WORKLIST IS NOT ACTIONABLE, AND I SHIPPED ONE. The first cut printed `Z=24` and 74// nothing else -- a number a reader can only worry about. A zombie is a child whose PARENT has not 75// reaped it, so the PARENT is the defect, and the parent pid is field 4 of the SAME stat buffer already 76// in hand: attributing them costs ZERO extra syscalls during the pass. 77const DS_PPID_FIELD: i64 = 4 78const DS_MAXZP: i64 = 64 79// ★REPORT NUMBERS, NOT A VERDICT. A few transient zombies are normal; a sustained pile is a reaping bug. 80// This organ has no calibrated normal FOR THIS BOX, so it names the parents and their counts and lets the 81// reader judge -- an uncalibrated classifier that emits a verdict is inventing a threshold. 82// ⚠USER_HZ IS AN ASSUMPTION, SO IT IS NAMED AND ITS RAW INPUT IS PRINTED BESIDE THE DERIVED SECONDS. 83// Linux x86-64 has used 100 since forever, but a wrong value here would silently scale every age by a 84// constant -- and a fabricated duration gets acted on. Printing start_ticks makes a bad HZ VISIBLE 85// rather than merely wrong. Overridable in the conf as `user_hz <n>`. 86const DS_USER_HZ_DEFAULT: i64 = 100 87 88func ds_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 89func ds_puts(s: *u8) -> i64 { sys_write(1, s, ds_len(s)); return 0 } 90func ds_pn(v: i64) -> i64 { 91 var m: i64 = v 92 if m < 0 { ds_puts("-" as *u8); m = 0 - m } 93 let t: *u8 = sys_mmap(32) 94 var k: i64 = 0 95 if m == 0 { t[0] = 48 as u8; k = 1 } 96 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 97 let o: *u8 = sys_mmap(32) 98 var i: i64 = 0 99 while i < k { o[i] = t[k - 1 - i]; i = i + 1 } 100 sys_write(1, o, k) 101 return 0 102} 103// all-digits name => a pid directory. Anything else in /proc is not a process. 104func ds_isnum(s: *u8) -> i64 { 105 if s[0] == (0 as u8) { return 0 } 106 var i: i64 = 0 107 var ok: i64 = 1 108 while s[i] != (0 as u8) { 109 let c: i64 = s[i] as i64 110 if c < 48 { ok = 0 } 111 if c > 57 { ok = 0 } 112 i = i + 1 113 } 114 return ok 115} 116// Read field N of /proc/<pid>/stat, counting the STATE char as field 3. Fields after comm are single 117// space-separated integers, so this walks tokens from the state char rather than from the start -- comm 118// may contain spaces AND parens, which is exactly what breaks a naive whitespace split of the whole line. 119// Returns -1 if the field is absent (a short/racing stat file), never a fabricated 0. 120func ds_field_from_state(b: *u8, n: i64, stpos: i64, field: i64) -> i64 { 121 if stpos >= n { return 0 - 1 } 122 var want: i64 = field - 3 123 var i: i64 = stpos 124 // advance past `want` space-separated tokens 125 while want > 0 { 126 var go: i64 = 1 127 while go == 1 { 128 if i >= n { go = 0; want = 0 } else { 129 if (b[i] as i64) == 32 { go = 0 } else { i = i + 1 } 130 } 131 } 132 if i >= n { return 0 - 1 } 133 i = i + 1 134 want = want - 1 135 } 136 if i >= n { return 0 - 1 } 137 var v: i64 = 0 138 var got: i64 = 0 139 var g2: i64 = 1 140 while g2 == 1 { 141 if i >= n { g2 = 0 } else { 142 let c: i64 = b[i] as i64 143 if c >= 48 { if c <= 57 { v = v*10 + (c-48); got = 1; i = i + 1 } else { g2 = 0 } } else { g2 = 0 } 144 } 145 } 146 if got == 0 { return 0 - 1 } 147 return v 148} 149func ds_atoi(s: *u8) -> i64 { 150 var v: i64 = 0 151 var i: i64 = 0 152 while s[i] != (0 as u8) { 153 let c: i64 = s[i] as i64 154 if c >= 48 { if c <= 57 { v = v*10 + (c - 48) } } 155 i = i + 1 156 } 157 return v 158} 159 160// Read argv[0] from /proc/<pid>/cmdline. comm is capped at 15 chars AND can be DELIBERATELY MASKED -- 161// measured live 2026-08-14: DSM's synocmstaskd reports comm literally as "_______________" while its 162// cmdline says `synocmstaskd`. A roster that can only print comm cannot name that process at all, and I 163// had to go read /proc by hand to identify it. 164// ★ONLY CALLED FOR ROWS ACTUALLY REPORTED (a handful), never for all ~750 -- the whole admission argument 165// for this organ is that it is one cheap pass, and an extra read per process would spend that. 166// Returns bytes written; 0 = unavailable. ★AN EMPTY CMDLINE IS NOT A MASKED NAME: kernel threads and 167// ZOMBIES have one BY DESIGN (a zombie has already released its memory), so absence here is absence of 168// evidence, and the caller must not present it as a finding. 169func ds_cmd0(pid: i64, out: *u8, cap: i64, path: *u8, reads: *i64) -> i64 { 170 var po: i64 = 0 171 let pre: *u8 = "/proc/" as *u8 172 var pi: i64 = 0 173 while pre[pi] != (0 as u8) { path[po] = pre[pi]; po = po + 1; pi = pi + 1 } 174 let tb: *u8 = sys_mmap(32) 175 var tn: i64 = 0 176 var mv: i64 = pid 177 if mv == 0 { tb[0] = 48 as u8; tn = 1 } 178 while mv > 0 { tb[tn] = (48 + (mv % 10)) as u8; mv = mv / 10; tn = tn + 1 } 179 var tk: i64 = tn 180 while tk > 0 { tk = tk - 1; path[po] = tb[tk]; po = po + 1 } 181 let suf: *u8 = "/cmdline" as *u8 182 var si: i64 = 0 183 while suf[si] != (0 as u8) { path[po] = suf[si]; po = po + 1; si = si + 1 } 184 path[po] = 0 as u8 185 let fd: i64 = sys_openat_rd(path) 186 if fd < 0 { out[0] = 0 as u8; return 0 } 187 let cb: *u8 = sys_mmap(DS_STATBUF) 188 let n: i64 = sys_read(fd, cb, DS_STATBUF) 189 sys_close(fd) 190 reads[0] = reads[0] + 1 191 if n <= 0 { out[0] = 0 as u8; return 0 } 192 var o: i64 = 0 193 var i: i64 = 0 194 var go: i64 = 1 195 while go == 1 { 196 if i >= n { go = 0 } else { 197 let c: i64 = cb[i] as i64 198 if c == 0 { go = 0 } else { 199 if c == 32 { go = 0 } else { 200 if o + 1 < cap { out[o] = cb[i]; o = o + 1 } 201 i = i + 1 202 } 203 } 204 } 205 } 206 out[o] = 0 as u8 207 return o 208} 209func ds_streq(a: *u8, b: *u8) -> i64 { 210 var i: i64 = 0 211 var eq: i64 = 1 212 var go: i64 = 1 213 while go == 1 { 214 let ca: i64 = a[i] as i64 215 let cb: i64 = b[i] as i64 216 if ca != cb { eq = 0; go = 0 } else { if ca == 0 { go = 0 } else { i = i + 1 } } 217 } 218 return eq 219} 220// SECOND-SAMPLE ZOMBIE PID COLLECTOR. Deliberately NARROW: it gathers ONLY the zombie pid set, because 221// only that set decides persistence -- it is not a second copy of the statistical pass, which would be a 222// duplicate ruler. It does re-walk /proc, and that structural overlap is the honest cost of having no 223// struct to return; the rule of three says extract the walk on the THIRD reader, and this is the second. 224// Defined AFTER ds_isnum/ds_atoi on purpose -- a function placed above the helpers it calls does not 225// resolve to them, the same ordering law this file already states for statics. 226// ---- OWNER ATTRIBUTION (2026-09-04). WHOSE I/O IS THE STORM? 227// The estate has carried an open row since 2026-08-06 saying the host is "IO-saturated by synoelasticd 228// + syno-cloud-client + md_raid5 resync, NOT by nishi work" and that there is NO ISOLATION between 229// Synology daemon IO and the sovereign control plane. That claim was true and UNMEASURABLE from any 230// instrument we own: nx_build_admit refuses on procs_blocked >= blocked_max and NEVER SAYS WHOSE 231// PROCESSES THOSE ARE, so a seat facing a refusal cannot tell a storm it caused from one it inherited. 232// A COUNT WITH NO WORKLIST IS NOT ACTIONABLE, and procs_blocked=8 is exactly that count. 233// This classifies the roster this organ ALREADY reads -- no new /proc walk, no new syscall, no new 234// read -- and prints a partition that SUMS to the D-state total, because a partition that does not 235// reconcile is a leak rather than a measurement. 236// DELIBERATELY ADVISORY: nothing here feeds a verdict or an admission decision. Attributing the queue 237// so admission could GRANT more often would be loosening a gate to flatter a number, which this estate 238// forbids by name. The refusal stays exactly as strict; only the operator's ability to READ it improves. 239func ds_has(hay: *u8, ned: *u8) -> i64 { 240 let nl: i64 = ds_len(ned) 241 let hl: i64 = ds_len(hay) 242 if nl == 0 { return 0 } 243 var i: i64 = 0 244 while i + nl <= hl { 245 var k: i64 = 0 246 var hit: i64 = 1 247 while k < nl { if hay[i + k] != ned[k] { hit = 0; k = nl } else { k = k + 1 } } 248 if hit == 1 { return 1 } 249 i = i + 1 250 } 251 return 0 252} 253 254// Returns the owner class of one blocked process. Order matters and is the classification: 255// 0 KERNEL -- a kthread; D is NORMAL for these and counting them as a storm is a false positive 256// 1 NISHI -- a sovereign organ (its own name or its serving root appears in the cmdline) 257// 2 SYNOLOGY -- a vendor daemon 258// 3 OTHER -- anything else, kept as its own bucket so the partition can still sum 259// UNKNOWN IS ITS OWN BUCKET ON PURPOSE: an unrecognised cmdline must never fall into a known class, 260// because the bucket it lands in becomes the number somebody plans against. 261func ds_owner(cmd: *u8, cn: i64, ppid: i64, pid: i64) -> i64 { 262 if ppid == 2 { return 0 } 263 if pid == 2 { return 0 } 264 if cn <= 0 { return 3 } 265 if ds_has(cmd, "syno" as *u8) == 1 { return 2 } 266 if ds_has(cmd, "nx_" as *u8) == 1 { return 1 } 267 if ds_has(cmd, "nishihost" as *u8) == 1 { return 1 } 268 return 3 269} 270 271func ds_zpids(out: *i64, cap: i64, dbuf: *u8, sbuf: *u8, path: *u8, reads: *i64) -> i64 { 272 let fd: i64 = sys_openat_rd("/proc" as *u8) 273 if fd < 0 { return 0 - 1 } 274 var n2: i64 = 0 275 var more: i64 = 1 276 while more == 1 { 277 let n: i64 = sys_getdents64(fd, dbuf, DS_DIRBUF) 278 if n <= 0 { more = 0 } else { 279 var off: i64 = 0 280 while off < n { 281 let base: i64 = dbuf as i64 282 let rec: *u8 = (base + off) as *u8 283 let reclen: i64 = (rec[DS_RECLEN_OFF] as i64) + ((rec[DS_RECLEN_OFF+1] as i64) << 8) 284 let nm: *u8 = (base + off + DS_NAME_OFF) as *u8 285 if ds_isnum(nm) == 1 { 286 var po: i64 = 0 287 let pre: *u8 = "/proc/" as *u8 288 var pi: i64 = 0 289 while pre[pi] != (0 as u8) { path[po] = pre[pi]; po = po + 1; pi = pi + 1 } 290 var ni: i64 = 0 291 while nm[ni] != (0 as u8) { path[po] = nm[ni]; po = po + 1; ni = ni + 1 } 292 let suf: *u8 = "/stat" as *u8 293 var si: i64 = 0 294 while suf[si] != (0 as u8) { path[po] = suf[si]; po = po + 1; si = si + 1 } 295 path[po] = 0 as u8 296 let sfd: i64 = sys_openat_rd(path) 297 if sfd >= 0 { 298 let sn: i64 = sys_read(sfd, sbuf, DS_STATBUF) 299 sys_close(sfd) 300 reads[0] = reads[0] + 1 301 if sn > 0 { 302 var lastp: i64 = 0 - 1 303 var k: i64 = 0 304 while k < sn { if (sbuf[k] as i64) == 41 { lastp = k } k = k + 1 } 305 if lastp >= 0 { 306 let stpos: i64 = lastp + 2 307 var st: i64 = 0 308 if stpos < sn { st = sbuf[stpos] as i64 } 309 if st == 90 { if n2 < cap { out[n2] = ds_atoi(nm); n2 = n2 + 1 } } 310 } 311 } 312 } 313 } 314 if reclen <= 0 { off = n } else { off = off + reclen } 315 } 316 } 317 } 318 sys_close(fd) 319 return n2 320} 321 322func main(argc: i64, argv: *i64) -> i64 { 323 var confp: *u8 = DS_CONF 324 var twice: i64 = 0 325 var gap_ms: i64 = DS_GAP_DEFAULT_MS 326 var conf_given: i64 = 0 327 // `nx_dstate twice [gap_ms] [conf]` -- BACK-COMPATIBLE: argv[1] is still a conf path unless it is 328 // literally the verb, so every existing single-sample call is untouched. 329 if argc >= 2 { 330 if ds_streq(argv[1] as *u8, "twice" as *u8) == 1 { 331 twice = 1 332 if argc >= 3 { gap_ms = ds_atoi(argv[2] as *u8) } 333 if argc >= 4 { confp = argv[3] as *u8; conf_given = 1 } 334 if gap_ms <= 0 { gap_ms = DS_GAP_DEFAULT_MS } 335 } else { confp = argv[1] as *u8; conf_given = 1 } 336 } 337 338 ds_puts("=== nx_dstate -- WHO is blocked (D = uninterruptible I/O), one sample, no window ===\n" as *u8) 339 340 // ---- ADMISSION. Load comes from the sibling's ct_load_centi; the conf is read by the estate's ONE 341 // line-anchored conf reader, rm_conf (nx_resmon_lib) -- the hand-rolled key parser that lived here until 342 // 2026-08-22 was a second copy of it, and a duplicate ruler is how two organs come to disagree about one 343 // file. Resolution mirrors nx_build_admit: argv path > knowledge/dstate.conf > ../knowledge/dstate.conf 344 // (buildroot-anchored callers) > built-in default -- and WHICH ONE WON IS PRINTED EVERY RUN as src=/path=. 345 // THE FILE ACTUALLY READ IS THE ONE NAMED. A neg-control run once reported a ceiling loaded from 346 // /tmp/dstate_low.conf as coming from knowledge/dstate.conf; a report that names a different subject than 347 // the one it measured is the defect class this estate spent a week removing. 348 // A MISSING CONF IS ANNOUNCED, NEVER SILENT: knowledge/dstate.conf did not exist in either knowledge tree 349 // from 2026-08-14 until 2026-08-22, so every run in between executed on the built-in while the header 350 // promised a conf. The conf now exists and nx_dstate_gate keeps it load-bearing. 351 var admit: i64 = DS_ADMIT_DEFAULT 352 var user_hz: i64 = DS_USER_HZ_DEFAULT 353 var conf_src: *u8 = "builtin-default" as *u8 354 var hz_src: *u8 = "default" as *u8 355 var conf_path: *u8 = confp 356 let cl: *i64 = sys_mmap(16) as *i64 357 var cbuf: *u8 = sys_read_file(confp, cl) 358 if (cbuf as i64) == 0 { if conf_given == 0 { 359 conf_path = DS_CONF_UP 360 cbuf = sys_read_file(DS_CONF_UP, cl) 361 } } 362 if (cbuf as i64) != 0 { 363 let a: i64 = rm_conf(cbuf, cl[0], "admit_load_centi" as *u8, DS_CONF_MISS) 364 if a != DS_CONF_MISS { admit = a; conf_src = "conf" as *u8 } 365 let h: i64 = rm_conf(cbuf, cl[0], "user_hz" as *u8, DS_CONF_MISS) 366 if h > 0 { user_hz = h; hz_src = "conf" as *u8 } 367 } 368 let lbuf: *u8 = sys_mmap(DS_LOADBUF) 369 let lfd: i64 = sys_openat_rd("/proc/loadavg" as *u8) 370 var ln: i64 = 0 371 if lfd >= 0 { ln = sys_read(lfd, lbuf, DS_LOADBUF); sys_close(lfd) } 372 let load: i64 = ct_load_centi(lbuf, ln) 373 ds_puts(" load_centi=" as *u8); ds_pn(load) 374 ds_puts(" admit_ceiling=" as *u8); ds_pn(admit) 375 ds_puts(" src=" as *u8); ds_puts(conf_src) 376 ds_puts(" path=" as *u8); ds_puts(conf_path); ds_puts("\n" as *u8) 377 ds_puts(" user_hz=" as *u8); ds_pn(user_hz) 378 ds_puts(" hz_from=" as *u8); ds_puts(hz_src); ds_puts("\n" as *u8) 379 if load < 0 { 380 ds_puts(" UNMEASURED: /proc/loadavg unreadable -- refusing to walk /proc without an admission check\n" as *u8) 381 ds_puts("verdict=UNMEASURED\n" as *u8) 382 sys_exit(DS_EXIT_UNMEASURED) 383 return DS_EXIT_UNMEASURED 384 } 385 if load > admit { 386 ds_puts(" ADMISSION-REFUSED: the host is above this organ's ceiling, and a /proc walk must not become\n" as *u8) 387 ds_puts(" the load it measures. This is NOT a report that nothing is blocked -- it is a refusal to look.\n" as *u8) 388 ds_puts("verdict=REFUSED\n" as *u8) 389 sys_exit(DS_EXIT_REFUSED) 390 return DS_EXIT_REFUSED 391 } 392 393 // ---- ONE PASS over /proc. State comes from /proc/<pid>/stat: `pid (comm) STATE ...` ---- 394 let fd: i64 = sys_openat_rd("/proc" as *u8) 395 if fd < 0 { 396 ds_puts(" UNMEASURED: cannot open /proc\n" as *u8) 397 ds_puts("verdict=UNMEASURED\n" as *u8) 398 sys_exit(DS_EXIT_UNMEASURED) 399 return DS_EXIT_UNMEASURED 400 } 401 let dbuf: *u8 = sys_mmap(DS_DIRBUF) 402 let sbuf: *u8 = sys_mmap(DS_STATBUF) 403 let path: *u8 = sys_mmap(256) 404 let dpid: *i64 = sys_mmap(DS_MAXD*8) as *i64 405 let dnam: *i64 = sys_mmap(DS_MAXD*8) as *i64 406 let dst: *i64 = sys_mmap(DS_MAXD*8) as *i64 407 let dpp: *i64 = sys_mmap(DS_MAXD*8) as *i64 408 // distinct zombie PARENTS and how many each has failed to reap 409 let zpp: *i64 = sys_mmap(DS_MAXZP*8) as *i64 410 let zpc: *i64 = sys_mmap(DS_MAXZP*8) as *i64 411 var nzp: i64 = 0 412 var zpover: i64 = 0 413 // ★★THE ZOMBIE'S OWN NAME IS THE MOST DIRECT EVIDENCE THERE IS: it names WHAT was forked, which points 414 // at the fork SITE. Grouping only by parent says 17 daemons leak one child each; adding the child's 415 // comm says WHICH child, and if that name is the SAME across daemons the shared call site is proven 416 // rather than inferred. Captured from the stat buffer already read -- no extra syscall. 417 let zfp: *i64 = sys_mmap(DS_MAXZP*8) as *i64 418 let zfn: *i64 = sys_mmap(DS_MAXZP*8) as *i64 419 let zfpp: *i64 = sys_mmap(DS_MAXZP*8) as *i64 420 var nzf: i64 = 0 421 // ★AN UNREADABLE UPTIME MAKES AGES UNAVAILABLE, NOT ZERO. A zero age reads as "just started", which is 422 // the exact OPPOSITE of the finding this column exists to surface. 423 let ubuf: *u8 = sys_mmap(DS_LOADBUF) 424 let ufd: i64 = sys_openat_rd("/proc/uptime" as *u8) 425 var un: i64 = 0 426 if ufd >= 0 { un = sys_read(ufd, ubuf, DS_LOADBUF); sys_close(ufd) } 427 var uptime_s: i64 = 0 - 1 428 if un > 0 { 429 var uv: i64 = 0 430 var ugot: i64 = 0 431 var ui: i64 = 0 432 var ugo: i64 = 1 433 while ugo == 1 { 434 if ui >= un { ugo = 0 } else { 435 let c: i64 = ubuf[ui] as i64 436 if c >= 48 { if c <= 57 { uv = uv*10 + (c-48); ugot = 1; ui = ui + 1 } else { ugo = 0 } } else { ugo = 0 } 437 } 438 } 439 if ugot == 1 { uptime_s = uv } 440 } 441 var nd: i64 = 0 442 var dover: i64 = 0 443 var nproc: i64 = 0 444 var reads: i64 = 0 445 var cR: i64 = 0 446 var cS: i64 = 0 447 var cD: i64 = 0 448 var cZ: i64 = 0 449 var cT: i64 = 0 450 var cO: i64 = 0 451 var unread: i64 = 0 452 453 // LOOP UNTIL getdents64 RETURNS 0 -- one call is a PREFIX of /proc, not a listing, and publishing a 454 // prefix count as a process total is how a census silently becomes a sample. 455 var more: i64 = 1 456 while more == 1 { 457 let n: i64 = sys_getdents64(fd, dbuf, DS_DIRBUF) 458 if n <= 0 { more = 0 } else { 459 var off: i64 = 0 460 while off < n { 461 let base: i64 = dbuf as i64 462 let rec: *u8 = (base + off) as *u8 463 let reclen: i64 = (rec[DS_RECLEN_OFF] as i64) + ((rec[DS_RECLEN_OFF+1] as i64) << 8) 464 let nm: *u8 = (base + off + DS_NAME_OFF) as *u8 465 if ds_isnum(nm) == 1 { 466 nproc = nproc + 1 467 var po: i64 = 0 468 let pre: *u8 = "/proc/" as *u8 469 var pi: i64 = 0 470 while pre[pi] != (0 as u8) { path[po] = pre[pi]; po = po + 1; pi = pi + 1 } 471 var ni: i64 = 0 472 while nm[ni] != (0 as u8) { path[po] = nm[ni]; po = po + 1; ni = ni + 1 } 473 let suf: *u8 = "/stat" as *u8 474 var si: i64 = 0 475 while suf[si] != (0 as u8) { path[po] = suf[si]; po = po + 1; si = si + 1 } 476 path[po] = 0 as u8 477 let sfd: i64 = sys_openat_rd(path) 478 if sfd < 0 { unread = unread + 1 } else { 479 let sn: i64 = sys_read(sfd, sbuf, DS_STATBUF) 480 sys_close(sfd) 481 reads = reads + 1 482 if sn <= 0 { unread = unread + 1 } else { 483 // comm may itself contain ')' -- the robust parse takes the LAST ')', which is 484 // why a naive scan-to-first-paren misreads any process named with one. 485 var lastp: i64 = 0 - 1 486 var k: i64 = 0 487 while k < sn { if (sbuf[k] as i64) == 41 { lastp = k } k = k + 1 } 488 if lastp < 0 { unread = unread + 1 } else { 489 let stpos: i64 = lastp + 2 490 var st: i64 = 0 491 if stpos < sn { st = sbuf[stpos] as i64 } 492 if st == 82 { cR = cR + 1 } 493 if st == 83 { cS = cS + 1 } 494 if st == 90 { 495 cZ = cZ + 1 496 // attribute to the parent that has not reaped it 497 let pp: i64 = ds_field_from_state(sbuf, sn, stpos, DS_PPID_FIELD) 498 if nzf < DS_MAXZP { 499 var zop: i64 = 0 500 var zf2: i64 = 0 501 while zf2 < sn { if (sbuf[zf2] as i64) == 40 { if zop == 0 { zop = zf2 } } zf2 = zf2 + 1 } 502 let zn: *u8 = sys_mmap(DS_NAMEMAX) 503 var zci: i64 = 0 504 var zq: i64 = zop + 1 505 while zq < lastp { if zci + 1 < DS_NAMEMAX { zn[zci] = sbuf[zq]; zci = zci + 1 } zq = zq + 1 } 506 zn[zci] = 0 as u8 507 zfp[nzf] = ds_atoi(nm) 508 zfn[nzf] = zn as i64 509 zfpp[nzf] = pp 510 nzf = nzf + 1 511 } 512 if pp >= 0 { 513 var zi: i64 = 0 514 var zf: i64 = 0 - 1 515 while zi < nzp { if zpp[zi] == pp { zf = zi } zi = zi + 1 } 516 if zf >= 0 { zpc[zf] = zpc[zf] + 1 } else { 517 if nzp < DS_MAXZP { zpp[nzp] = pp; zpc[nzp] = 1; nzp = nzp + 1 } else { zpover = 1 } 518 } 519 } 520 } 521 if st == 84 { cT = cT + 1 } 522 if st == 68 { 523 cD = cD + 1 524 if nd < DS_MAXD { 525 // comm is between the FIRST '(' and that last ')' 526 var op: i64 = 0 527 var f: i64 = 0 528 while f < sn { if (sbuf[f] as i64) == 40 { if op == 0 { op = f } } f = f + 1 } 529 let cn: *u8 = sys_mmap(DS_NAMEMAX) 530 var ci: i64 = 0 531 var q: i64 = op + 1 532 while q < lastp { if ci + 1 < DS_NAMEMAX { cn[ci] = sbuf[q]; ci = ci + 1 } q = q + 1 } 533 cn[ci] = 0 as u8 534 dpid[nd] = ds_atoi(nm) 535 dnam[nd] = cn as i64 536 dst[nd] = ds_field_from_state(sbuf, sn, stpos, DS_STARTTIME_FIELD) 537 dpp[nd] = ds_field_from_state(sbuf, sn, stpos, DS_PPID_FIELD) 538 nd = nd + 1 539 } else { dover = 1 } 540 } 541 if st != 82 { if st != 83 { if st != 90 { if st != 84 { if st != 68 { cO = cO + 1 } } } } } 542 } 543 } 544 } 545 } 546 if reclen <= 0 { off = n } else { off = off + reclen } 547 } 548 } 549 } 550 sys_close(fd) 551 552 // THE WORKLIST. A count with no names is a symptom; the names are the diagnosis. 553 ds_puts("\n-- D-STATE (uninterruptible I/O -- these are what loadavg is counting) --\n" as *u8) 554 // ★★SAY WHICH DURATION THIS IS. proc_age_s is how long the PROCESS has existed, NOT how long it has 555 // been blocked -- a long-lived process that blocked one second ago is indistinguishable from one 556 // wedged for days. Linux exposes no time-in-state, so D-duration needs repeated samples of the SAME 557 // pid, which is a RATE and deliberately outside this one-sample organ. Printed here because the 558 // number invites exactly the inference it does not support. 559 ds_puts(" (proc_age_s = PROCESS age, NOT time-in-D; a long-lived process that just blocked looks\n" as *u8) 560 ds_puts(" identical. D-duration needs repeated samples of the same pid.)\n" as *u8) 561 if nd == 0 { ds_puts(" (none at this instant)\n" as *u8) } 562 // OWNER TALLY. Four buckets, and every blocked process lands in exactly one, so they must sum to nd. 563 var own_kern: i64 = 0 564 var own_nishi: i64 = 0 565 var own_syno: i64 = 0 566 var own_other: i64 = 0 567 var j: i64 = 0 568 while j < nd { 569 ds_puts(" pid=" as *u8); ds_pn(dpid[j]) 570 ds_puts(" " as *u8); ds_puts(dnam[j] as *u8) 571 // ★★MARK KERNEL THREADS. kthreads (children of kthreadd, pid 2) sit in D BY DESIGN -- md4_defer* 572 // has been there since boot and always will be. Listing them beside a genuinely blocked userspace 573 // daemon sends an operator chasing a non-defect, and A DETECTOR WITH FALSE POSITIVES IS WORSE THAN 574 // NONE because it teaches everyone to ignore the true positives next to them. 575 if dpp[j] == 2 { ds_puts(" [kthread -- D is normal for these]" as *u8) } 576 if dpid[j] == 2 { ds_puts(" [kthread -- D is normal for these]" as *u8) } 577 // a blocked process is LIVE, so its cmdline is readable and names it even when comm is masked 578 var owner: i64 = 0 579 if dpp[j] != 2 { 580 let cmdd: *u8 = sys_mmap(256) 581 let rp4: *i64 = sys_mmap(16) as *i64 582 rp4[0] = reads 583 let cn4: i64 = ds_cmd0(dpid[j], cmdd, 256, path, rp4) 584 reads = rp4[0] 585 if cn4 > 0 { ds_puts(" cmd=" as *u8); ds_puts(cmdd) } 586 // Classified from the cmdline THIS LOOP ALREADY READ -- no extra walk, no extra syscall. 587 owner = ds_owner(cmdd, cn4, dpp[j], dpid[j]) 588 } 589 if owner == 0 { own_kern = own_kern + 1; ds_puts(" owner=KERNEL" as *u8) } 590 if owner == 1 { own_nishi = own_nishi + 1; ds_puts(" owner=NISHI" as *u8) } 591 if owner == 2 { own_syno = own_syno + 1; ds_puts(" owner=SYNOLOGY" as *u8) } 592 if owner == 3 { own_other = own_other + 1; ds_puts(" owner=OTHER" as *u8) } 593 // age, with its RAW INPUT printed beside it so a wrong USER_HZ is visible rather than silent 594 if dst[j] < 0 { ds_puts(" age=UNAVAILABLE (stat field absent)" as *u8) } else { 595 if uptime_s < 0 { ds_puts(" age=UNAVAILABLE (/proc/uptime unreadable)" as *u8) } else { 596 let age: i64 = uptime_s - (dst[j] / user_hz) 597 ds_puts(" proc_age_s=" as *u8); ds_pn(age) 598 ds_puts(" (start_ticks=" as *u8); ds_pn(dst[j]) 599 ds_puts(" user_hz=" as *u8); ds_pn(user_hz); ds_puts(")" as *u8) 600 } 601 } 602 ds_puts("\n" as *u8) 603 j = j + 1 604 } 605 if dover == 1 { ds_puts(" ROSTER TRUNCATED at the table bound -- the count above is complete, the NAMES are not.\n" as *u8) } 606 607 // ---- WHOSE STORM IS IT? The partition, and it must RECONCILE or it is a leak rather than a measure. 608 ds_puts("\n-- D-STATE BY OWNER (the question every build refusal raises and none of them answered) --\n" as *u8) 609 ds_puts(" kernel=" as *u8); ds_pn(own_kern) 610 ds_puts(" nishi=" as *u8); ds_pn(own_nishi) 611 ds_puts(" synology=" as *u8); ds_pn(own_syno) 612 ds_puts(" other=" as *u8); ds_pn(own_other) 613 let own_sum: i64 = own_kern + own_nishi + own_syno + own_other 614 ds_puts(" sum=" as *u8); ds_pn(own_sum) 615 ds_puts(" of nd=" as *u8); ds_pn(nd) 616 if own_sum == nd { ds_puts(" PARTITION-RECONCILES\n" as *u8) } 617 if own_sum != nd { ds_puts(" PARTITION-LEAK -- a process fell into no bucket; the split above is NOT a population\n" as *u8) } 618 // ★KERNEL THREADS ARE COUNTED AND THEN SET ASIDE, NEVER SILENTLY DROPPED. D is normal for them, so a 619 // storm figure that includes them overstates; a figure that omits them without saying so is a 620 // different population wearing the same name. Both numbers are printed. 621 ds_puts(" userspace_blocked=" as *u8); ds_pn(own_nishi + own_syno + own_other) 622 ds_puts(" (nd minus kernel threads -- THIS is the number a build refusal is really about)\n" as *u8) 623 // ★THE ATTRIBUTION IS ADVISORY AND SAYS SO. It changes no verdict here and feeds no admission 624 // decision. Its whole job is to let an operator answer, in one call, the question the estate has 625 // carried unanswered since 2026-08-06: is this storm OURS or the vendor's? A refusal that names the 626 // conjunct that fired but not the owner of the queue sends every reader at the wrong subject. 627 ds_puts(" ADVISORY: this partition drives NO verdict and NO admission decision -- read it beside\n" as *u8) 628 ds_puts(" a BUILD-ADMIT refusal to learn whose I/O the refusal is protecting you from.\n" as *u8) 629 630 // ---- ZOMBIE PARENTS. A zombie is a child its PARENT never reaped, so naming the parent names the 631 // defect. The parent's comm needs one extra read PER DISTINCT PARENT -- O(k) with k tiny (usually 1-3), 632 // not O(processes) -- and those reads are counted into stat_reads so the cost stays honest. 633 // The zombie's OWN name first -- it is the fork TARGET, and a name repeated across unrelated parents 634 // turns "a shared idiom" from an inference into a demonstration. 635 // ★★★ONE ROSTER CANNOT TELL TRANSIENT FROM PERSISTENT, AND READING IT AS A LEAK IS THE DEFAULT ERROR. 636 // Measured 2026-08-14: I filed an "active unbounded zombie leak" TWICE off a single sample. A second 637 // sample minutes later showed the SAME parent set with EVERY CHILD PID CHANGED -- i.e. children being 638 // forked, exited and reaped continuously, with the snapshot landing inside the reap window. Only the 639 // pids that REPEAT across samples are real. This is the same level-vs-derivative law as leak 640 // detection: a COUNT is a level, and only two samples in time can express accumulation. 641 ds_puts("\n-- ZOMBIES (what was forked and never reaped) --\n" as *u8) 642 // ---- SECOND SAMPLE (opt-in). Only pids present in BOTH samples are persistent. ---- 643 let zs2: *i64 = sys_mmap(DS_MAXZP*8) as *i64 644 var nz2: i64 = 0 - 1 645 if twice == 1 { 646 // ★ANNOUNCE THE WAIT BEFORE TAKING IT. Output is written as it goes, so a silent multi-second sleep 647 // mid-section reads as a HANG -- I misread my own organ that way and started diagnosing a defect 648 // in sys_sleep_ms that did not exist. A long pause must say it is a pause. 649 ds_puts(" ... second sample in " as *u8); ds_pn(gap_ms); ds_puts(" ms ...\n" as *u8) 650 sys_sleep_ms(gap_ms) 651 let rp: *i64 = sys_mmap(16) as *i64 652 rp[0] = reads 653 nz2 = ds_zpids(zs2, DS_MAXZP, dbuf, sbuf, path, rp) 654 reads = rp[0] 655 } 656 if twice == 0 { 657 ds_puts(" (ONE SAMPLE CANNOT DISTINGUISH TRANSIENT FROM PERSISTENT: a child caught between exit and\n" as *u8) 658 ds_puts(" reap looks identical to a leaked one. Pass `twice` -- only pids in BOTH samples are real.)\n" as *u8) 659 } 660 if twice == 1 { 661 ds_puts(" (TWO SAMPLES, gap_ms=" as *u8); ds_pn(gap_ms) 662 ds_puts(" -- PERSISTENT = present in both. A gap SHORTER than a reaper's cycle would report a\n" as *u8) 663 ds_puts(" transient child as PERSISTENT, so this verdict is only as strong as that gap.)\n" as *u8) 664 } 665 if nzf == 0 { ds_puts(" (none at this instant)\n" as *u8) } 666 var zk: i64 = 0 667 var npers: i64 = 0 668 while zk < nzf { 669 ds_puts(" pid=" as *u8); ds_pn(zfp[zk]) 670 ds_puts(" " as *u8); ds_puts(zfn[zk] as *u8) 671 ds_puts(" <- ppid=" as *u8); ds_pn(zfpp[zk]) 672 if nz2 >= 0 { 673 var seen2: i64 = 0 674 var q2: i64 = 0 675 while q2 < nz2 { if zs2[q2] == zfp[zk] { seen2 = 1 } q2 = q2 + 1 } 676 if seen2 == 1 { ds_puts(" PERSISTENT" as *u8); npers = npers + 1 } else { ds_puts(" transient (reaped between samples)" as *u8) } 677 } 678 ds_puts("\n" as *u8) 679 zk = zk + 1 680 } 681 682 ds_puts("\n-- ZOMBIE PARENTS (a zombie is a child this process has not reaped) --\n" as *u8) 683 if nzp == 0 { ds_puts(" (no zombies at this instant)\n" as *u8) } 684 var zj: i64 = 0 685 while zj < nzp { 686 ds_puts(" ppid=" as *u8); ds_pn(zpp[zj]) 687 var po2: i64 = 0 688 let pre2: *u8 = "/proc/" as *u8 689 var pi2: i64 = 0 690 while pre2[pi2] != (0 as u8) { path[po2] = pre2[pi2]; po2 = po2 + 1; pi2 = pi2 + 1 } 691 let tb: *u8 = sys_mmap(32) 692 var tn: i64 = 0 693 var mv: i64 = zpp[zj] 694 if mv == 0 { tb[0] = 48 as u8; tn = 1 } 695 while mv > 0 { tb[tn] = (48 + (mv % 10)) as u8; mv = mv / 10; tn = tn + 1 } 696 var tk: i64 = tn 697 while tk > 0 { tk = tk - 1; path[po2] = tb[tk]; po2 = po2 + 1 } 698 let suf2: *u8 = "/stat" as *u8 699 var si2: i64 = 0 700 while suf2[si2] != (0 as u8) { path[po2] = suf2[si2]; po2 = po2 + 1; si2 = si2 + 1 } 701 path[po2] = 0 as u8 702 let pfd: i64 = sys_openat_rd(path) 703 var named: i64 = 0 704 if pfd >= 0 { 705 let pn2: i64 = sys_read(pfd, sbuf, DS_STATBUF) 706 sys_close(pfd) 707 reads = reads + 1 708 if pn2 > 0 { 709 var lp2: i64 = 0 - 1 710 var kk: i64 = 0 711 while kk < pn2 { if (sbuf[kk] as i64) == 41 { lp2 = kk } kk = kk + 1 } 712 var op2: i64 = 0 713 var ff: i64 = 0 714 while ff < pn2 { if (sbuf[ff] as i64) == 40 { if op2 == 0 { op2 = ff } } ff = ff + 1 } 715 if lp2 > op2 { 716 ds_puts(" " as *u8) 717 sys_write(1, (((sbuf as i64) + op2 + 1) as *u8), lp2 - op2 - 1) 718 named = 1 719 } 720 } 721 } 722 // ★A PARENT THAT EXITED BETWEEN THE WALK AND THIS READ IS NOT AN UNNAMED PARENT -- it is a parent 723 // that is GONE, which is a different fact and is said differently. 724 if named == 0 { ds_puts(" (parent no longer present -- these zombies are now orphans, reparented to init)" as *u8) } 725 // name it properly when comm is masked or truncated -- the parent is LIVE, so its cmdline is readable 726 if named == 1 { 727 let cmdb: *u8 = sys_mmap(256) 728 let rp3: *i64 = sys_mmap(16) as *i64 729 rp3[0] = reads 730 let cn3: i64 = ds_cmd0(zpp[zj], cmdb, 256, path, rp3) 731 reads = rp3[0] 732 if cn3 > 0 { ds_puts(" cmd=" as *u8); ds_puts(cmdb) } 733 } 734 ds_puts(" unreaped=" as *u8); ds_pn(zpc[zj]); ds_puts("\n" as *u8) 735 zj = zj + 1 736 } 737 if zpover == 1 { ds_puts(" PARENT TABLE TRUNCATED -- the zombie COUNT is complete, the parent list is not.\n" as *u8) } 738 // ★THE HEADLINE OF TWO-SAMPLE MODE IS THE PERSISTENT COUNT, NOT THE RAW ONE. Reporting only the raw 739 // count is what made me file a false leak twice off a roster that was mostly reap-window noise. 740 if nz2 >= 0 { 741 ds_puts("\nzombies_sample1=" as *u8); ds_pn(nzf) 742 ds_puts(" zombies_sample2=" as *u8); ds_pn(nz2) 743 ds_puts(" PERSISTENT=" as *u8); ds_pn(npers) 744 ds_puts(" transient=" as *u8); ds_pn(nzf - npers) 745 ds_puts("\n" as *u8) 746 } 747 748 let sum: i64 = cR + cS + cD + cZ + cT + cO + unread 749 ds_puts("\nprocs=" as *u8); ds_pn(nproc) 750 ds_puts(" R=" as *u8); ds_pn(cR) 751 ds_puts(" S=" as *u8); ds_pn(cS) 752 ds_puts(" D=" as *u8); ds_pn(cD) 753 ds_puts(" Z=" as *u8); ds_pn(cZ) 754 ds_puts(" T=" as *u8); ds_pn(cT) 755 ds_puts(" other=" as *u8); ds_pn(cO) 756 ds_puts(" unreadable=" as *u8); ds_pn(unread) 757 ds_puts(" sum=" as *u8); ds_pn(sum) 758 if sum == nproc { ds_puts(" partition=RECONCILES\n" as *u8) } else { ds_puts(" partition=LEAK\n" as *u8) } 759 // COST, MEASURED NOT CLAIMED: the whole justification for admitting at a higher ceiling than the 760 // sibling is that this pass is cheap, so the pass reports its own syscall count and lets the reader 761 // check the claim instead of trusting the header. 762 ds_puts("stat_reads=" as *u8); ds_pn(reads) 763 ds_puts(" passes=1 window_ms=0 (a STATE needs one sample; only a RATE needs two)\n" as *u8) 764 ds_puts("verdict=SAMPLED\n" as *u8) 765 sys_exit(0) 766 return 0 767}