raid56 stripe_alloc — 6.18 octopus validation test plan (rev 5)

Status: the specification run-6.18-validation.sh implements, rewritten 2026-09-17 against the suite as it runs today. Rev 5 moved the plan from ~/share/btrfs-forensics/ into this repository (the rev 3 + rev 4 text is kept at forensics/history/6.18-octopus-test-plan-rev4.md), folded the §11 decisions and the rev 4 addenda into the sections they govern, and added what the earlier revisions did not describe: the profile-parametric geometry and the T6 sizing rule, the metadata headroom chunks, the declared expected outcomes as the spec, the driver stages around the octopus, and where raid6 and raid56-metadata coverage actually live (they are not octopus passes).

1. Objective

Validate that the raid56 stripe-exclusive-allocation write-hole series (the 6.18 backport octopus) meets the exit criteria below with the fault present for the whole run, under a total disk failure and under ongoing silent corruption of one drive, with no data loss and no crashes.

The octopus is the umbrella acceptance suite (T1–T13, three passes). It is one stage of the rebase revalidation driver (§11); crash consistency, fsync forensics, the directed regression tests, raid6 (§12) and raid56 metadata (§13) are covered by the other stages and drivers named there.

2. System under test

item value
kernel the 6.18 lane: zygo-6.18.x-zb64 = 6.18/testing (octopus of the 6.18/topics/* series over stable/linux-6.18.y) + the zb64 config tail. Identity is uname -r (6.18.NN+) plus the per-VM build number (#NNN); the report records the tree sha and progs version at run start. Lanes, refs and how a kernel gets onto a VM: REFERENCE.md §2–§3
btrfs-progs v6.14 (+ZSTD +LZO)
allocator mount -o stripe_alloc (P1/P2); stock allocator in P3 on the same kernel (§7)
profile mkfs.btrfs -K -d $PROFILE_D -m $PROFILE_M (defaults raid5 / raid1), mount -o nodiscard[,stripe_alloc]
harness ~/share/raid56-wh-harness (canonical git repo) rsynced to /root/harness on both VMs (REFERENCE.md §4; bhive is also a git remote). rolling-oracle.py generator, fsstress at /root/xfstests/ltp/fsstress, compsize

3. VM assignment and fault models

Both VMs run the same suite; only the injector and nodatacow-inclusion differ. The fault is present for every test; it is removed only by the replace step (T12), after which the offline check (T13) runs on a healed array.

VM fault (ROLE, auto-detected from the hostname) nodatacow tests rationale
bhive = FAIL one data member returns EIO on every read and write included a dead disk must not affect nodatacow correctness — data is still raid5-redundant
dhive = CORRUPT one member silently drops writes and returns random garbage on read, its garbage rotating so a block never reads the same twice excluded (T9 SKIP) nodatacow has no checksum; silent corruption is undetectable there by design

4. Filesystem geometry

5. Fault injectors

5.1 FAIL — dead disk via the dm error target

Each member is loop → dm-linear at mkfs. After mount and the baseline writes, one data member is killed live: dmsetup suspend / reload --table "0 <sectors> error" / resume. All subsequent I/O to it returns EIO — a dead disk, not a -o degraded remount.

5.2 CORRUPT — worst-case silent device via dm-flakey drop_writes

dm-flakey in permanent drop_writes mode over a /dev/urandom backing: a write returns success but is discarded, and every read returns the random backing — there is no window where a recently written block is readable. Every read of an affected stripe csum-fails on that column and rebuilds from parity; metadata falls back to its other raid1 mirror.

Rotation (ON by default, CORRUPT_ROTATE=1, ROTATE_SECS=1). A dm-linear in front of the random backing whose start offset moves every second makes the same block return different garbage over time: no lucky-consistent read, no stable content for reconstruction to accidentally match. If the worst case passes, the easier cases (static garbage, dd overwrite) pass too. The one risk it adds — a low-memory stall while a member is dm-suspended for the reload — is what T8's watchdog looks for.

(Periodic dd if=/dev/urandom over the raw member was rejected: slower, and it leaves a window where the just-written block reads back correct, so reconstruction is exercised only sometimes.)

6. Test suite

Execution order within a pass: baseline tree dc (the T12 verify set) → inject the fault → T1 T3 T4 T5 T6 T7 T9 T10 T11, then T2 (it scans every writelog, so it runs once they all exist), T8, then T12 (removes the fault) and T13. T8 is re-checked at the end of the pass so a WARN/abort during T12–T13 cannot slip through (it did once, 2026-09-13).

Oracle classes: correct / eio / missing / zero / wrong / short. wrong and zero are always fatal. A short/missing file is fatal unless its writer got a write(2)/fsync EIO or ENOSPC for that region — otherwise it is a zero/missing in disguise; the write phase records per-file outcomes so verify can tell. Within single-fault tolerance no write error is expected, so short/missing must be 0 except at the ENOSPC edge in T6.

# Criterion Method PASS
T1 read-after-write w/ flush oracle write (FILES=4000) → sync; drop_caches → verify --expect all-correct wrong=zero=eio=0; short=missing=0
T2 no IO errors visible to apps app return codes + oracle eio across every tree's writelog (dc/en/z/nc/log/fs) apps succeed; eio=0 (internal csum/EIO must be recovered, not surfaced)
T3 scrub btrfs scrub start -Bd; per-device status; re-verify completes on every device, no abort; post-scrub all-correct. Errors on the faulted device are expected — the target is completion + app-visible correctness
T4 balance btrfs balance start -d; re-verify completes with all-correct, or stops with a reported error and data intact. FAIL = relocates corrupted data
T5 fsstress fsstress -n 200000 -p 8 for SOAK_SECS under the fault no crash/hang; rc 0 or timeout; no app-visible EIO
T6 ENOSPC, no data loss drop the fsstress tree (keep dc); shrink each live member to the computed target (§4); consume free space to a ~256 MiB margin with one sequential filler; oracle fill (100 000 small files) to ENOSPC under the fault; verify; drop en + filler; resize :max every acked write verifies; ENOSPC only at write(2); short/missing only at the fill edge where the writer got ENOSPC; the shrink/grow succeed (write_errors=0 means the fill never reached the edge — noted, not failed)
T7 compressed (zstd) subvol compression=zstd; oracle --compressible (4-bit alphabet, ~54 %) write/verify all-correct and compression engaged (compsize ≈ 50 %)
T8 kernel health (continuous) dmesg + D-state watchdog for the whole pass, re-checked at the end zero oops/BUG/RIP/hung task/soft lockup/rcu stall, KASAN/UBSAN/refcount_t/list corruption, write-hole-invariant WARNs, unexpected dropping unwritten extent/csum escalation; no task stuck in D (incl. no stall with a member dm-suspended during rotation). Known-benign signatures (TEST-CATALOG.md §8) are filtered
T9 nodatacow under disk failure (FAIL only) chattr +C subvol; write → drop_caches → verify all-correct; SKIP under CORRUPT
T10 fsync durability across the fault write + fsync under the fault → drop_caches → verify fsync'd bytes reconstruct; wrong=zero=short=0
T11 fsync-heavy log-tree stress parallel tight fsync loops plus rename A over B for SOAK_SECS no crash/hang; committed state consistent on readback; no lost/torn fsync'd data
T12 device replace / rebuild meta_buffer +1; btrfs replace start the faulted member with the spare → wait → verify replace completes; reconstructed data all-correct. Removes the fault
T13 offline consistency, post-replace unmount → btrfs check --readonly no errors — "the disks are healthy again, did the metadata survive?"

7. Execution passes

PASSES_RUN="P1_online P2_degraded P3_stock", in that order, every loop:

  1. P1 online failure, stripe_alloc (primary) — the live injectors of §5.
  2. P2 degraded mount, stripe_alloc — the fault member is downed to error before the first mount and the filesystem mounted -o degraded (P2_STOCK=1 runs it with the stock allocator too, as an A/B).
  3. P3 online failure, stock allocator — P1 with stripe_alloc off on the same kernel (which carries topics/raid56-fixes). It isolates stripe_alloc-specific regressions, and its passing shows the crash and corruption fixes carry the stock allocator through the same faults; a genuine stock FAIL is marked FAIL(exp).

P3b, positive control (done 2026-08-13, forensics/p3b/): the same suite on a pristine stable/linux-6.18.y build (v6.18.42, no topics). It proved the suite is sensitive to the bugs the topics fix: FAIL model — T6 and T12 fail; CORRUPT — T2 and T10 fail. Re-run only if the suite's discriminating power is in doubt. Pass 4 (stock + degraded) is skipped — no information over P3.

8. Execution model

  1. Boot the SUT; record uname -r, tree sha, progs version in the report.
  2. Detect-not-panic sysctls: hung_task_panic=0 softlockup_panic=0 panic_on_oops=0 panic_on_warn=0.
  3. Arm the T8 watchdog; kernel messages between loops (the previous loop's unmount, this one's setup) are reported but charged to no test.
  4. Per pass: build the filesystem, meta_buffer +2, baseline dc, inject the fault (or mount degraded), run the tests in §6 order, replace, offline check, tear down (dm devices, loops).
  5. On any failure: record and CONTINUE to the next test.
  6. Loop the whole suite for BUDGET_SECS — 24 h standalone, 4 h when the rebase driver calls itor stop on the first unexpected failure (§10). CONTINUE_ON_FAIL=1 keeps looping for the budget when a known, separate WARN would otherwise cut an A/B soak short; the final status still reflects the failure.
  7. Diagnosis afterwards: reproduce each recorded failure in isolation (minimal geometry, deterministic fault); the record goes under forensics/<what>-<date>/.

9. Report (regenerated live)

The table is regenerated after every test, so /root/6.18-validation-report.md is a live status map while the run proceeds (the driver copies it to its run directory as octopus-report.md):

# 6.18 octopus validation — <VM> (<fault>) — <date>  [loop N, elapsed HH:MM]
kernel: <uname>  branch: zygo-6.18.x-zb64 @ <sha>  progs: <ver>

| test | criterion                 | P1 online | P2 degraded | P3 stock | notes |
| T1   | read-after-write          | PASS      | PASS        | FAIL(exp)|       |
| T6   | ENOSPC no data loss       | PASS      | RUNNING     | pending  | enospc edge write_errors=.. shrunk=5/5 to 2524M |
...
Loop N: pass=.. fail=.. skip=..
RESULT_6.18[bhive/FAIL/P1]: pass=.. fail=.. skip=..

PASS / FAIL / FAIL(exp) / SKIP / RUNNING / pending per cell, with a note.

10. Declared expected outcomes — the spec

11. Where the octopus sits: the rebase revalidation driver

run-rebase-validation.sh (run on each VM after a rebased kernel boots) sequences the regression-relevant subset of TEST-CATALOG.md and writes a live report under /root/rebase-validation-<date>/:

stage what verdict
sanity kernel/tree identity, progs, ≥ 20 GiB free on /mnt/wh (clean leftover loop images first), lockdep on preflight; a FATAL here should stop the run (today it does not — clean /mnt/wh before launching)
stage0 the dm-log-writes crash-replay matrix, both arms: stock must reproduce the write hole RED, stripe_alloc must be GREEN per-arm expectation
fsync fsync-window forensics (torn-log replay, degraded endpoints) oracle
directed the regression reproducers (DIRECTED= list: write-hole/parity, corrupt-churn, fill-edge, ENOSPC data loss, balance-reclaim, bg-gate incl. the raid56-metadata and property cases, nocow strand/RO drain, legacy conversion, punch-hole, balance-resume) exit 77 = SKIPPED (feature absent on this kernel); a kernel WARN/BUG during a script fails the stage unless declared in directed-expected-warns.txt
octopus this plan, BUDGET_SECS=4 h, OCTOPUS_SOAK_SECS=1 h (scoped explicitly: the driver's own SOAK_SECS is the 12 h soak below and once leaked into T11) §10
soak the 12 h dm-crypt deadlock soak from ~/share/raid56-hang-debug/, with credit that resumes across reboots for the same kernel build and settings hang trip or unknown WARN = FAIL

Two VMs run the same driver under their two fault models; the host-side heartbeat monitor (scripts/vm-heartbeat-monitor.sh) reports loss of signal, taint changes, D-state hangs, stage changes and hourly progress, and reads the serial console captures in ~/share/btrfs-forensics/console-logs/ for the crash signature when a VM goes away.

12. raid6 coverage

The octopus acceptance runs raid5. raid6 is covered by run-r6c3-validation.sh — the recommended production pairing **raid6 data

stage axis
matrix-stock, matrix-sa crash safety: the log-writes replay matrix, both arms (stock RED, stripe_alloc GREEN), plus the 2-device drop axis and degraded read-write continuation and degraded log replay inside it (R6_NDEV=5: 3 data + 2 parity, R6_TAILS/R6_PHASES 3/3 = 72 cases per arm)
fsync-sa the strict fsync-window matrix under the flush-consistent (barrier) crash model
recovery corrupted devices within tolerance: detect, recover, repair
cdev, cstripe corruption beyond tolerance: stock drops writes, stripe_alloc must not

raid1c3 is what makes the raid6 drop axis honest: under raid1 metadata a two-device drop can take both copies of a metadata chunk and the case dies as "unmountable" instead of testing data recovery (the 2026-08-01 geometry sweep's raid6-over-raid1 rows are weaker than they look for this reason).

Results: all six stages PASS on both VMs 2026-08-28/29; 7/7 on both 2026-09-15 (matrix stock RED 72 rows with 27/27 controls, stripe_alloc GREEN 43/43 continuations). That run also found the lib/raid6 x1 bug: the sse2x1/avx2x1/avx512x1 gen_syndrome variants read pointers[-1] at three disks (the 3-wide raid6 chunks the cdev stage and uneven devices produce) — fixed as topics/raid6-fixes on both lanes, with a test-only raid6_pq.algo= knob and a grub entry that pins the x1 variant so the coverage runs on the code path that had the bug (REFERENCE.md §3), and the 3-device probes raid6-3dev-test.sh / raid6-3dev-parity.py as the positive control. bg-gate-test.sh also asserts the claim rule at raid6 (data + raid1, data + raid6 metadata).

An octopus pass at raid6 (PROFILE_D=raid6 PROFILE_M=raid1c3, or raid1) has never been run; the driver supports it (§4). It is listed as an optional addition in §17, not as acceptance.

13. raid56 metadata coverage

stripe_alloc applies to raid56 metadata automatically — there is no separate option or property (Zygo, 2026-08-27: if the metadata is raid56 the policy covers it; if it is raid1 it has no effect there). Tree blocks are allocated by whole stripes, metadata runs retire per transaction (the adjacent-generation canary went 45 → 0), and the trapped/claimable accounting, the reserve that keeps tree blocks landing, and reclaim of trapped groups all apply to metadata groups. SYSTEM chunks are the one raid56 shape still uncovered — the mount warns "does not cover raid56 system chunks", and the RMW audit shows they take real write holes (forensics/, 2026-08-27). Mixed block groups are refused, permanently (mount option and property alike).

Coverage lives in the directed stage and the round-1 review tests, not in an octopus pass:

test what it holds
bg-gate-test.sh raid5/raid6 data + raid5/raid6 metadata: data and metadata claimed; the metadata RMW counter moves; any uncovered-RMW log line names a stripe in a SYSTEM chunk, never a METADATA chunk; mixed refused; v1 cache and property/option agreement cases
stripe-meta-toggle-test.sh runtime enable/disable of the property on a live filesystem with raid56 metadata (MPROF=raid5/raid6, ssd/ssd_spread/nossd, legacy data before the flip): cached metadata groups are armed/disarmed, the open-run remainder is synced, no stale bytes_stripe_open after disable, clean unmount
run-meta-fsync-tear.sh + metamix-scan.py the fsync-window hazard: log-tree blocks of one fsync share a metadata stripe with blocks a later log commit or the transaction commit writes. Two committed phases, a torn phase with half the files fsynced, a cut inside the logged window, then the replay matrix with degraded replay, ro,degraded,rescue=nologreplay fallback, continuation, offline check and a per-stripe tree-block scan that classifies every torn block (log/other, new/old generation, parity consistency)
prealloc-race-test.sh fallocate storms against manifest-verified buffered writers at the fill edge

Measured (2026-09-16, four passes: raid5/raid6 × uniform/late cuts, 162 armed cases): 0 committed-data damage, 0 torn stripe that held a committed tree block, 0 stripes mixing log and non-log tree blocks; 2 raid5 log-tear refusals, both recovered intact by mount -o ro,degraded,rescue=nologreplay (the degraded recovery — btrfs rescue zero-log needs a writable, all-devices mount and cannot open a degraded raid56). Conclusion: the window is a detectable, log-only loss; committed metadata is never harmed. Settling metadata runs at log commit, which would close the window, is queued after the other open issues (Zygo, 2026-09-17).

An octopus pass with raid56 metadata (PROFILE_M=raid5 or raid6) has never been run — optional addition, §17.

14. Instrumented modes

15. Prerequisites / checklist (per VM, before every launch)

16. Results to date

The evidence table, by run, is REFERENCE.md §6. In brief: FAIL model GREEN 13/13 (first 2026-08); CORRUPT 24 h soak 2026-08-13 (forensics/); P3b positive control 2026-08-13; the ENOSPC chain candidates cand48/49 FAIL 3 × 39/39 and CORRUPT 12/12/12 (2026-09-13/14); cand53 (the landed chain + seven round-1 fixes) CORRUPT all-green and FAIL at the known T6 geometry abort before the sizing fix (2026-09-16); the rebased 6.18 lane (v6.18.52) under validation on both VMs 2026-09-16/17, interrupted on dhive by the parked-rbio UAF (§14) — not a rebase regression.

17. Decisions and changelog