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}