code wiki / _hdl_build / nx_raidwatch.nx

nx_raidwatch.nx source

↩ module page · 457 lines · 22670 B

1// nx_raidwatch.nx -- ARRAY + KERNEL-IO HEALTH BEAT. The estate had NONE. 2// 3// WHY THIS EXISTS (2026-08-07). A grep across every organ for mdstat, smartctl or raid returned only 4// substring false positives (a bip39 wordlist, a chemistry table). Nothing in a 140 TB estate was 5// watching whether its arrays were intact. Two live faults were sitting there unwatched: 6// md3 (raid1, the NVMe mirror) DEGRADED [2/1] [_U] -- one member gone, /dev/nvme0n1p1 has no 7// device node at all, so it cannot even be re-added without physical intervention. It was 8// recorded in a memory file and NOTHING CHECKED IT, which is the difference between a note and 9// a monitor. 10// sata8 throwing repeated UNC (uncorrectable) read errors on ONE sector, 840 kernel records over 11// four hours, while the array it backs still reports [8/8] because raid5 keeps reconstructing 12// the block from parity. A disk can be failing loudly while every array metric reads healthy. 13// 14// ROOT-FREE BY CONSTRUCTION, because the beat runs as the estate user: 15// /sys/block/<md>/md/degraded, raid_disks, level, mismatch_cnt -- structured integers, no text 16// parsing of /proc/mdstat, so this cannot be broken by a format change. 17// /dev/kmsg -- readable here, opened O_NONBLOCK so the final read returns EAGAIN instead of 18// BLOCKING FOREVER at the end of the buffer. A monitor that can hang is not a monitor. 19// smartctl is present but needs root (open of /dev/sata8 = Permission denied, measured), so 20// per-disk SMART is deliberately OUT OF SCOPE here rather than silently half-working. 21// 22// REFUSES if it can read neither surface: ignorance is not health, and a GREEN produced by an 23// unreadable sysfs would be worse than no beat at all. 24import "nx_syscalls.nx" 25 26const RW_MAXMD: i64 = 16 27const RW_MAXSECT: i64 = 256 28const RW_KMSG_RECS: i64 = 8192 29const RW_RECBUF: i64 = 8192 30const RW_SMALL: i64 = 64 31const RW_PATH: i64 = 256 32const RW_O_NONBLOCK: i64 = 2048 33const RW_AT_FDCWD: i64 = 0 - 100 34const RW_SYS_OPENAT: i64 = 257 35const RW_EXIT_RED: i64 = 6 36const RW_EXIT_AMBER: i64 = 4 37const RW_EXIT_NODATA: i64 = 5 38const RW_DIRBUF: i64 = 65536 // getdents64 batch for the per-member walk 39const RW_RECLEN_OFF: i64 = 16 // linux_dirent64.d_reclen 40const RW_NAME_OFF: i64 = 19 // linux_dirent64.d_name 41 42// ---- DURABLE VERDICT (2026-09-03). THIS MONITOR HAS BEEN RIGHT AND UNHEARD FOR 25 DAYS. 43// It sits on an hourly clock row with NO output redirection and wrote no artifact, so a RED verdict -- 44// md3 running with zero redundancy since 2026-08-09 -- was computed and thrown away every hour, and 45// nothing anywhere consumed it. That is the estate's own law firing on the one monitor that matters 46// most: A CRON ROW THAT DISCARDS ITS OUTPUT MAKES ITS OWN FAILURE UNOBSERVABLE. The console line is 47// unchanged byte for byte -- anything already parsing stdout keeps working -- and the same numbers now 48// also land in a TRUNCATE-written status file with the verdict LAST, which is the shape a watch row and 49// gv_last_line can consume. 50const RW_STATUS: *u8 = "knowledge/status/raidwatch.status" 51const RW_STATBUF: i64 = 1024 52// THE VERDICT LADDER, COMPUTED ONCE. The status file reads THIS; the printed branches below keep their 53// own prose because each names a DIFFERENT cause, but the machine-readable answer has exactly one 54// definition. Two copies of a decision agree the day they are written and drift on the next edit. 55func rw_verdict_code(degraded: i64, ioerr: i64, memberbad: i64, mismatched: i64) -> i64 { 56 if degraded > 0 { return RW_EXIT_RED } 57 if ioerr > 0 { return RW_EXIT_RED } 58 if memberbad > 0 { return RW_EXIT_AMBER } 59 if mismatched > 0 { return RW_EXIT_AMBER } 60 return 0 61} 62func rw(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 63func rwe(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(2, s, n); return 0 } 64func rwn(v: i64) -> i64 { 65 var m: i64 = v 66 if m < 0 { rw("-" as *u8); m = 0 - m } 67 let t: *u8 = sys_mmap(28) 68 var k: i64 = 0 69 if m == 0 { t[0] = 48 as u8; k = 1 } 70 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 71 let o: *u8 = sys_mmap(28) 72 var i: i64 = 0 73 while i < k { o[i] = t[k - 1 - i]; i = i + 1 } 74 sys_write(1, o, k) 75 sys_munmap(t, 28) 76 sys_munmap(o, 28) 77 return 0 78} 79 80// append a NUL-terminated literal at offset o 81func rw_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o + i] = s[i]; i = i + 1 } return o + i } 82// append a small non-negative integer 83func rw_catn(d: *u8, o: i64, v: i64) -> i64 { 84 if v < 0 { d[o] = 45 as u8; return rw_catn(d, o + 1, 0 - v) } 85 // NEGATIVES (2026-08-07). Without this the `while m > 0` loop below never runs for a 86 // negative value and this function emits ZERO CHARACTERS, silently corrupting whatever 87 // format it is writing into. Handled AT THE SIGNATURE so it is independent of which 88 // cursor variable the body happens to use. Non-negative input is byte-identical (rule 19). 89 if v < 0 { d[o] = 45 as u8; return rw_catn(d, o + 1, 0 - v) } 90 if v == 0 { d[o] = 48 as u8; return o + 1 } 91 let t: *u8 = sys_mmap(28) 92 var m: i64 = v 93 var k: i64 = 0 94 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 95 var i: i64 = 0 96 while i < k { d[o + i] = t[k - 1 - i]; i = i + 1 } 97 sys_munmap(t, 28) 98 return o + k 99} 100 101// read a small sysfs file as an integer. -1 = unreadable, which is NOT the same as 0. 102func rw_sysint(path: *u8) -> i64 { 103 let fd: i64 = sys_openat_rd(path) 104 if fd < 0 { return 0 - 1 } 105 let b: *u8 = sys_mmap(RW_SMALL) 106 let n: i64 = sys_read(fd, b, RW_SMALL - 1) 107 sys_close(fd) 108 if n <= 0 { sys_munmap(b, RW_SMALL); return 0 - 1 } 109 var v: i64 = 0 110 var any: i64 = 0 111 var i: i64 = 0 112 while i < n { 113 let c: i64 = b[i] as i64 114 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); any = 1 } } 115 i = i + 1 116 } 117 sys_munmap(b, RW_SMALL) 118 if any == 0 { return 0 - 1 } 119 return v 120} 121 122// copy a small sysfs string (level name) into dst, NUL-terminated; 0 on success, -1 unreadable 123func rw_sysstr(path: *u8, dst: *u8, cap: i64) -> i64 { 124 let fd: i64 = sys_openat_rd(path) 125 if fd < 0 { return 0 - 1 } 126 var n: i64 = sys_read(fd, dst, cap - 1) 127 sys_close(fd) 128 if n <= 0 { return 0 - 1 } 129 while n > 0 { if dst[n - 1] == (10 as u8) { n = n - 1 } else { dst[n] = 0 as u8; return 0 } } 130 dst[0] = 0 as u8 131 return 0 132} 133 134// first integer following the NUL-terminated literal key within buf[0,n). -1 = key absent. 135func rw_after(buf: *u8, n: i64, key: *u8) -> i64 { 136 var kl: i64 = 0 137 while key[kl] != (0 as u8) { kl = kl + 1 } 138 var i: i64 = 0 139 while i + kl <= n { 140 var k: i64 = 0 141 var ok: i64 = 1 142 while k < kl { if buf[i + k] != key[k] { ok = 0; k = kl } else { k = k + 1 } } 143 if ok == 1 { 144 var j: i64 = i + kl 145 var v: i64 = 0 146 var any: i64 = 0 147 var go: i64 = 1 148 while go == 1 { 149 go = 0 150 if j < n { 151 let c: i64 = buf[j] as i64 152 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); any = 1; j = j + 1; go = 1 } } 153 } 154 } 155 if any == 0 { return 0 - 1 } 156 return v 157 } 158 i = i + 1 159 } 160 return 0 - 1 161} 162 163// does [s,e) of buf contain the NUL-terminated literal lit? 164func rw_has(buf: *u8, n: i64, lit: *u8) -> i64 { 165 var ll: i64 = 0 166 while lit[ll] != (0 as u8) { ll = ll + 1 } 167 if ll == 0 { return 0 } 168 var i: i64 = 0 169 while i + ll <= n { 170 var k: i64 = 0 171 var ok: i64 = 1 172 while k < ll { if buf[i + k] != lit[k] { ok = 0; k = ll } else { k = k + 1 } } 173 if ok == 1 { return 1 } 174 i = i + 1 175 } 176 return 0 177} 178 179// ---- PER-MEMBER HEALTH: the gauge the array-level numbers structurally cannot show ----------------- 180// THE BLIND SPOT THIS CLOSES, and it is the one this organ own header describes: md2 reports 181// degraded=0 raid_disks=8 mismatch_cnt=0 array_state=clean -- PERFECT -- while a member disk is visibly 182// failing, because raid5 reconstructs every failed read from parity. So an array-level monitor reports 183// health during exactly the failure it exists to catch. mdraid exposes the missing number, root-free, as 184// a structured integer: /sys/block/<md>/md/dev-<name>/errors = read errors detected on THAT member and 185// corrected without evicting it. 186// MEASURED 2026-08-07 the moment this was read: sata6p5=808 sata8p5=56 every other member 0. 187// The kmsg investigation that opened this had named sata8 as the single failing device -- true of the 188// RING-BUFFER WINDOW it could see, but sata6 carries 14x the cumulative count and nothing had ever read 189// it. ★A RING BUFFER REPORTS THE RECENT, NOT THE WORST -- AND A CUMULATIVE COUNTER IS THE ONLY THING 190// THAT CAN NAME THE WORST. 191// ★AN ARRAY-LEVEL METRIC IS A CLAIM ABOUT REDUNDANCY, NOT ABOUT DISK HEALTH; RAID EXISTS PRECISELY TO 192// MAKE THE SECOND INVISIBLE IN THE FIRST. 193// FULL SCOPE, NO SAMPLING: every md, every member, EVERY nonzero member NAMED -- no cap, no "shown=N of 194// M". A capped list is a sample, and a sample must never be published as a population fact. 195// out[0]=members seen out[1]=sum errors out[2]=members with errors>0 out[3]=max errors 196func rw_member_scan(mdnum: i64, out: *i64) -> i64 { 197 out[0] = 0 198 out[1] = 0 199 out[2] = 0 200 out[3] = 0 201 let dp: *u8 = sys_mmap(RW_PATH) 202 var od: i64 = rw_cat(dp, 0, "/sys/block/md" as *u8) 203 od = rw_catn(dp, od, mdnum) 204 od = rw_cat(dp, od, "/md" as *u8) 205 dp[od] = 0 as u8 206 let fd: i64 = sys_openat_rd(dp) 207 if fd < 0 { return 0 - 1 } 208 let db: *u8 = sys_mmap(RW_DIRBUF) 209 let ep: *u8 = sys_mmap(RW_PATH) 210 var n: i64 = sys_getdents64(fd, db, RW_DIRBUF) 211 while n > 0 { 212 var q: i64 = 0 213 while q < n { 214 let rl: i64 = (db[q + RW_RECLEN_OFF] as i64) + ((db[q + RW_RECLEN_OFF + 1] as i64) * 256) 215 if rl <= 0 { q = n } 216 else { 217 let nm: *u8 = ((db as i64) + q + RW_NAME_OFF) as *u8 218 if rw_has(nm, 4, "dev-" as *u8) == 1 { 219 out[0] = out[0] + 1 220 var oe: i64 = rw_cat(ep, 0, dp) 221 oe = rw_cat(ep, oe, "/" as *u8) 222 oe = rw_cat(ep, oe, nm) 223 oe = rw_cat(ep, oe, "/errors" as *u8) 224 ep[oe] = 0 as u8 225 let ec: i64 = rw_sysint(ep) 226 if ec > 0 { 227 out[1] = out[1] + ec 228 out[2] = out[2] + 1 229 if ec > out[3] { out[3] = ec } 230 rw(" MEMBER " as *u8); rw(nm) 231 rw(" errors=" as *u8); rwn(ec) 232 rw(" (corrected reads -- this member is damaged; the ARRAY still reads healthy)" as *u8) 233 rw("\n" as *u8) 234 } 235 } 236 q = q + rl 237 } 238 } 239 n = sys_getdents64(fd, db, RW_DIRBUF) 240 } 241 sys_close(fd) 242 return 0 243} 244 245func main(argc: i64, argv: *i64) -> i64 { 246 let p: *u8 = sys_mmap(RW_PATH) 247 let lvl: *u8 = sys_mmap(RW_SMALL) 248 var seen: i64 = 0 249 var degraded: i64 = 0 250 var mismatched: i64 = 0 251 var memberbad: i64 = 0 // members with errors>0, across EVERY array 252 var membersum: i64 = 0 253 var membermax: i64 = 0 254 255 rw("=== nx_raidwatch -- md array integrity + kernel IO errors (root-free) ===\n" as *u8) 256 257 var m: i64 = 0 258 while m < RW_MAXMD { 259 var o: i64 = rw_cat(p, 0, "/sys/block/md" as *u8) 260 o = rw_catn(p, o, m) 261 o = rw_cat(p, o, "/md/raid_disks" as *u8) 262 p[o] = 0 as u8 263 let rd: i64 = rw_sysint(p) 264 if rd > 0 { 265 seen = seen + 1 266 var o2: i64 = rw_cat(p, 0, "/sys/block/md" as *u8) 267 o2 = rw_catn(p, o2, m) 268 o2 = rw_cat(p, o2, "/md/degraded" as *u8) 269 p[o2] = 0 as u8 270 let dg: i64 = rw_sysint(p) 271 var o3: i64 = rw_cat(p, 0, "/sys/block/md" as *u8) 272 o3 = rw_catn(p, o3, m) 273 o3 = rw_cat(p, o3, "/md/mismatch_cnt" as *u8) 274 p[o3] = 0 as u8 275 let mc: i64 = rw_sysint(p) 276 var o4: i64 = rw_cat(p, 0, "/sys/block/md" as *u8) 277 o4 = rw_catn(p, o4, m) 278 o4 = rw_cat(p, o4, "/md/level" as *u8) 279 p[o4] = 0 as u8 280 lvl[0] = 0 as u8 281 rw_sysstr(p, lvl, RW_SMALL) 282 283 rw(" md" as *u8); rwn(m) 284 rw(" level=" as *u8); rw(lvl) 285 rw(" members=" as *u8); rwn(rd) 286 rw(" degraded=" as *u8); rwn(dg) 287 rw(" mismatch=" as *u8); rwn(mc) 288 if dg > 0 { 289 degraded = degraded + 1 290 rw(" *** DEGRADED: " as *u8); rwn(dg) 291 rw(" of " as *u8); rwn(rd) 292 rw(" members missing -- NO REDUNDANCY, a second failure loses the array ***" as *u8) 293 } 294 if mc > 0 { mismatched = mismatched + 1; rw(" *** MISMATCH: parity disagrees with data ***" as *u8) } 295 rw("\n" as *u8) 296 // PER-MEMBER immediately AFTER the array line, so the contrast sits in one screen: the array 297 // says clean, the member says 808. Reading them apart is how the fault stayed invisible. 298 let mout: *i64 = sys_mmap(64) as *i64 299 rw_member_scan(m, mout) 300 if mout[2] > 0 { 301 memberbad = memberbad + mout[2] 302 membersum = membersum + mout[1] 303 if mout[3] > membermax { membermax = mout[3] } 304 } 305 } 306 m = m + 1 307 } 308 309 // ---- kernel IO errors. O_NONBLOCK so the last read returns EAGAIN instead of blocking. ---- 310 // COUNTED SEPARATELY, because one physical failure emits a PAIR of records: an ATA line carrying 311 // { UNC } and a block layer line carrying I/O error. Summing them reported 168 for 84 real events 312 // -- twice the apparent severity. Cross-checked against dmesg: 84 and 84, union 168, zero false 313 // positives on the loose UNC substring (every match was the ATA form). 314 // DISTINCT SECTORS is the discriminator that actually decides what to do: 84 failures on ONE 315 // sector is a single bad block, recoverable and remappable; 84 failures across 84 sectors is a 316 // disk coming apart. The same error count means opposite things. 317 var ioerr: i64 = 0 318 var unc: i64 = 0 319 var recs: i64 = 0 320 var nsect: i64 = 0 321 var corrected: i64 = 0 322 var slo: i64 = 0 - 1 323 var shi: i64 = 0 - 1 324 let sect: *i64 = sys_mmap(8 * RW_MAXSECT + 16) as *i64 325 let kfd: i64 = __syscall(RW_SYS_OPENAT, RW_AT_FDCWD, "/dev/kmsg" as i64, RW_O_NONBLOCK, 0, 0, 0) 326 if kfd >= 0 { 327 let rb: *u8 = sys_mmap(RW_RECBUF) 328 var go: i64 = 1 329 while go == 1 { 330 let r: i64 = sys_read(kfd, rb, RW_RECBUF - 1) 331 if r <= 0 { go = 0 } else { 332 recs = recs + 1 333 rb[r] = 0 as u8 334 var hit: i64 = 0 335 if rw_has(rb, r, "I/O error" as *u8) == 1 { ioerr = ioerr + 1; hit = 1 } 336 if rw_has(rb, r, "{ UNC }" as *u8) == 1 { unc = unc + 1; hit = 1 } 337 if rw_has(rb, r, "medium error" as *u8) == 1 { hit = 1 } 338 // "read error corrected" is the ARRAY COMPENSATING: raid5 rebuilt the block from 339 // parity and the caller never saw a failure. It is the difference between "a disk is 340 // damaged" and "data was lost", and it is why every array metric can read healthy 341 // while a member is visibly failing. Counted, never treated as an error. 342 if rw_has(rb, r, "read error corrected" as *u8) == 1 { corrected = corrected + 1 } 343 if hit == 1 { 344 var s: i64 = rw_after(rb, r, "sector in range " as *u8) 345 if s < 0 { s = rw_after(rb, r, "sector " as *u8) } 346 if s >= 0 { 347 var q: i64 = 0 348 var known: i64 = 0 349 while q < nsect { if sect[q] == s { known = 1; q = nsect } else { q = q + 1 } } 350 if known == 0 { if nsect < RW_MAXSECT { sect[nsect] = s; nsect = nsect + 1 } } 351 if slo < 0 { slo = s; shi = s } 352 if s < slo { slo = s } 353 if s > shi { shi = s } 354 } 355 } 356 if recs >= RW_KMSG_RECS { go = 0 } 357 } 358 } 359 sys_close(kfd) 360 sys_munmap(rb, RW_RECBUF) 361 } 362 363 rw(" kernel: records_scanned=" as *u8); rwn(recs) 364 rw(" io_error=" as *u8); rwn(ioerr) 365 rw(" unc=" as *u8); rwn(unc) 366 rw(" distinct_sectors=" as *u8); rwn(nsect) 367 rw(" corrected_by_parity=" as *u8); rwn(corrected) 368 if nsect > 1 { 369 rw(" span=" as *u8); rwn(shi - slo) 370 rw(" sectors" as *u8) 371 } 372 // Deliberately NOT calling a verdict on the disk from a sector count. Three sectors inside a 373 // 400 KB span is a LOCALISED defect; the same count spread over a whole platter is a different 374 // failure. Report the count AND the span and let the reader see which one this is -- an earlier 375 // draft of this line said "SPREAD across sectors -- this is a failing disk" for any count above 376 // one, which would have overstated a 400 KB region as a disintegrating drive. 377 if nsect == 1 { rw(" (ONE bad block -- remappable)" as *u8) } 378 if nsect > 1 { rw(" (MULTIPLE bad blocks -- a growing defect; judge by the span, not the count)" as *u8) } 379 if kfd < 0 { rw(" (kmsg unreadable -- IO errors NOT checked, this is ignorance not health)" as *u8) } 380 rw("\n" as *u8) 381 382 if seen == 0 { 383 rwe("NX-RAIDWATCH REFUSED: no md array was readable under /sys/block -- refusing to report health it never measured\n" as *u8) 384 sys_exit(RW_EXIT_NODATA) 385 return RW_EXIT_NODATA 386 } 387 388 // PERSIST BEFORE PRINTING. Every verdict branch below sys_exit()s, so a write placed after them 389 // would run on exactly ONE path -- and the path it would miss is RED. 390 let rvc: i64 = rw_verdict_code(degraded, ioerr, memberbad, mismatched) 391 let sb: *u8 = sys_mmap(RW_STATBUF) 392 var so: i64 = 0 393 so = rw_cat(sb, so, "arrays " as *u8); so = rw_catn(sb, so, seen) 394 so = rw_cat(sb, so, "\ndegraded " as *u8); so = rw_catn(sb, so, degraded) 395 so = rw_cat(sb, so, "\nmismatched " as *u8); so = rw_catn(sb, so, mismatched) 396 so = rw_cat(sb, so, "\nio_error " as *u8); so = rw_catn(sb, so, ioerr) 397 so = rw_cat(sb, so, "\ndistinct_sectors " as *u8); so = rw_catn(sb, so, nsect) 398 so = rw_cat(sb, so, "\nmember_errors_sum " as *u8); so = rw_catn(sb, so, membersum) 399 so = rw_cat(sb, so, "\nmembers_damaged " as *u8); so = rw_catn(sb, so, memberbad) 400 so = rw_cat(sb, so, "\nworst_member_errors " as *u8); so = rw_catn(sb, so, membermax) 401 so = rw_cat(sb, so, "\nexit_code " as *u8); so = rw_catn(sb, so, rvc) 402 // THE ENVELOPE TRAVELS WITH THE NUMBERS. io_error, distinct_sectors and the sector span are read 403 // from /dev/kmsg, a RING BUFFER: two readings days apart are two WINDOWS, not two points on a 404 // trend. A seat compared them across 25 days in this very session and had to retract the 405 // progression it published. member_errors_* are CUMULATIVE since array start and ARE comparable. 406 so = rw_cat(sb, so, "\nkmsg_fields_are_ring_buffer_not_trend 1" as *u8) 407 if rvc == RW_EXIT_RED { so = rw_cat(sb, so, "\nverdict=RED\n" as *u8) } else { 408 if rvc == RW_EXIT_AMBER { so = rw_cat(sb, so, "\nverdict=AMBER\n" as *u8) } else { 409 so = rw_cat(sb, so, "\nverdict=GREEN\n" as *u8) 410 } } 411 let sfd: i64 = sys_openat_wr(RW_STATUS, MODE_0644) 412 if sfd >= 0 { sys_write(sfd, sb, so); sys_close(sfd) } else { 413 rwe("NX-RAIDWATCH: status write FAILED -- this beat measured the arrays and published nothing durable, which from outside is indistinguishable from never running\n" as *u8) 414 } 415 rw("NX-RAIDWATCH arrays=" as *u8); rwn(seen) 416 rw(" degraded=" as *u8); rwn(degraded) 417 rw(" mismatched=" as *u8); rwn(mismatched) 418 rw(" io_error=" as *u8); rwn(ioerr) 419 rw(" distinct_sectors=" as *u8); rwn(nsect) 420 rw(" member_errors_sum=" as *u8); rwn(membersum) 421 rw(" members_damaged=" as *u8); rwn(memberbad) 422 rw(" worst_member_errors=" as *u8); rwn(membermax) 423 if degraded > 0 { 424 rw(" verdict=RED (an array is running without redundancy; a second member failure is data loss)\n" as *u8) 425 sys_exit(RW_EXIT_RED) 426 return RW_EXIT_RED 427 } 428 if ioerr > 0 { 429 rw(" verdict=RED (kernel is reporting uncorrectable IO; a disk can be failing while every array still reads healthy)\n" as *u8) 430 sys_exit(RW_EXIT_RED) 431 return RW_EXIT_RED 432 } 433 // MEMBER DAMAGE OUTLIVES THE RING BUFFER. This is the branch that makes the monitor survive its own 434 // evidence expiring: io_error above is read from /dev/kmsg, so once the buffer rolls past the errors it 435 // reports 0 and the organ would print GREEN on a fleet with a visibly damaged disk. The per-member 436 // counter is CUMULATIVE since array start, so it cannot roll over and cannot be out-waited. 437 // MEASURED 2026-08-07: sata6p5=808 sata8p5=56 while md2 read degraded=0 mismatch=0 array_state=clean. 438 // AMBER not RED, deliberately: raid5 corrected every one of these reads, so nothing is lost YET -- but 439 // md3 is already degraded, so the estate has less margin than 8/8 suggests, and a damaged member is a 440 // REPLACEMENT DECISION, not an outage. Naming it AMBER keeps RED meaning "redundancy is gone". 441 // ★A MONITOR WHOSE EVIDENCE EXPIRES REPORTS RECOVERY WHEN IT SHOULD REPORT AMNESIA. 442 if memberbad > 0 { 443 rw(" verdict=AMBER (a member disk is DAMAGED: " as *u8); rwn(membersum) 444 rw(" corrected read errors across " as *u8); rwn(memberbad) 445 rw(" member(s), worst " as *u8); rwn(membermax) 446 rw(" -- every array still reads healthy because parity is masking it; plan replacement)\n" as *u8) 447 sys_exit(RW_EXIT_AMBER) 448 return RW_EXIT_AMBER 449 } 450 if mismatched > 0 { 451 rw(" verdict=AMBER (parity mismatch recorded -- schedule a scrub)\n" as *u8) 452 sys_exit(RW_EXIT_AMBER) 453 return RW_EXIT_AMBER 454 } 455 rw(" verdict=GREEN (every array full, no parity mismatch, no uncorrectable IO in the kernel buffer, and ZERO cumulative per-member read errors -- the last clause is what makes this GREEN survive a rolled kmsg)\n" as *u8) 456 return 0 457}