Skip to content

SPEC-73 tiering: bdev_tier composite + relocate primitives + nexus heat + raid5f hardening#13

Open
rducom wants to merge 42 commits into
mainfrom
feat/spec-73-tiering
Open

SPEC-73 tiering: bdev_tier composite + relocate primitives + nexus heat + raid5f hardening#13
rducom wants to merge 42 commits into
mainfrom
feat/spec-73-tiering

Conversation

@rducom

@rducom rducom commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

SPEC-73 data-plane for storage tiering. Adds a bdev_tier composite module + the lvol/blob
relocate primitives + nexus logical heat instrumentation, plus raid5f resilience hardening.
The out-of-tree SPDK changes ship as a 10-patch series (patches/) applied onto the pinned
base at image-build time; scripts/patches.sh makes the round-trip mechanical.

Modules / patches

  • module/bdev/tier/bdev_tier composite (concat fast→slow bands + mirrored md region,
    per-band EIO isolation, on-disk superblock v2 à la bdev_raid_sb). RPCs: create/add_band/
    assemble_band/register/retire_band/get_bands/resync_md/delete + evariops_get_capabilities. (M1)
  • 0003 nvmf subsystem pause/resume RPC (standing-pause registry + TTL self-heal). (barrier)
  • 0004 blob relocate primitives (get_cluster_lba, claim_cluster_in_range, relocate_commit,
    release_cluster_lba) + public blob I/O freeze. (M2)
  • 0005 bdev_lvol_get_cluster_placement + bdev_lvol_relocate_cluster / batch relocate
    (claim → freeze → copy → crash-safe L2P swap → unfreeze). (M2a/M2b)
  • 0006 raid5f degraded-read fallback (reconstruct a present-member read error from parity) +
    double-fault guard
    (bail -EIO if a second member is unavailable). (M3 + hardening)
  • 0007 nexus logical heat instrumentation (dense per-region counters, lossy hot-path). (M5)
  • 0008 bdev_raid_rebuild_ranges read-reconstruct-writeback range repair. (B2/M4)
  • 0009 surface thin-pool ENOSPC as NVMe CAPACITY_EXCEEDED. (C-1)
  • 0010 chmod 0600 the RPC unix socket after bind. (S-2)

Code-review remediation (this round)

A max-effort review surfaced ~31 findings (correctness first). The load-bearing ones are fixed;
architectural/policy items are called out below as deliberate follow-ups.

In-repo (module/bdev/tier, module/bdev/cbt, scripts/):

  • tier: fix leg_io double-free on the md-read mirror-failover retry path.
  • tier: add_band gains a TIER_MAX_BANDS bound + registered guard (closes a base_ch[]
    out-of-bounds and a post-register geometry-divergence path).
  • tier: fail channel-create on a NULL base io_channel instead of leaving an ACTIVE band with no leg.
  • tier: re-validate dst_band->desc across the relocate copy chain (hot-remove mid-copy → clean EIO,
    not a NULL deref).
  • tier: io_type_supported now AND-consults the base bands for UNMAP/FLUSH/WRITE_ZEROES.
  • tier: overflow-safe range/capacity checks in assemble_band.
  • tier: multi-band FLUSH/UNMAP/WRITE_ZEROES fan-out — fixes the whole-device NVMe Flush (was
    100% FAILED) and any op crossing the md/data boundary; RESET now propagates to all base bands.
  • tier(rpc): drop the stale u32 cluster_blocks cap (v2 SB is u64); drop the dead
    capacity_blocks/used_blocks fields.
  • cbt: clear out_rebuild_id on start_rebuild error; the epoch-takeover (c5) guard now keys on an
    actually-running rebuild — fixes a permanent -EBUSY after a completed rebuild.
  • patches.sh: check is now non-destructive (validates against a scratch index, never touches the
    caller's tree); NUL-safe patch enumeration.

Patch series (regenerated via patches.sh regen; only the 4 touched patches changed):

  • 0003: re-resolve the subsystem by NQN before the TTL resume dereferences it (use-after-free if a
    paused subsystem is destroyed).
  • 0004: roll back the in-memory L2P swap on a persist failure (prevents two blobs sharing one cluster).
  • 0007: guard blocklen == 0 before dividing (SIGFPE on a not-yet-online nexus); hot-path
    divide→shift + 64-byte-padded region struct; reuse raid_bdev_find_by_name.
  • 0008: drop the lock_range_cb function-pointer cast (a future internal-typedef change now breaks the
    build instead of corrupting the stack at runtime).

Deferred (design/policy, tracked for follow-up): compose the md mirror over raid1 instead of the
hand-rolled RAID1; heat as a passthru vbdev / per-channel counters; capabilities via the live RPC
registry; blob claim-scan word-wise + resume cursor; move relocate guards into blobstore; raid1
ENOSPC reconvergence policy.

Validation

  • CI greenlint (incl. T1 tier host unit test, ASAN+UBSAN+LSAN) ✓ and Build ✓ on debug+release
    × amd64+arm64
    (real Linux SPDK build applying all 10 patches). Trivy fails as expected on feature
    branches (image not pushed).
  • Fixed a LeakSanitizer leak in the T1 test harness (bands allocated by make_composite were never
    freed — masked locally because macOS ASan has no LSan).
  • patches.sh check confirms the 10-patch series applies cleanly in order against a fresh pinned base.
  • Live validation on the turing cluster still pending for the end-to-end tiering/relocate flows.

Label build-images applied to publish a prerelease image for downstream (spdk-csi) testing.

rducom added 13 commits June 21, 2026 00:47
- vbdev_tier.h: band-table data model (band=disk, tier class, state),
  composite layout (mirrored md region [0,md_end) + concat data bands),
  per-band failure isolation, lifecycle + relocate-quiesce API.
- Makefile (overlay template).
- Dockerfile: COPY + register tier in DIRS-y / BLOCKDEV_MODULES_LIST / DEPDIRS.
WIP: vbdev_tier.c (io path/translate/copy/md-mirror/superblock) + RPC next.
- vbdev_tier.c: concat translate, mirrored md region (RAID1 across 2 bands),
  per-band failure isolation (-EIO on degraded band, never global is_degraded),
  channels (per-band base_ch), lifecycle create/add_band/retire/register/delete,
  relocate-quiesce co-design hook for M2b.
- vbdev_tier_rpc.c: bdev_tier_create/add_band/register/retire_band/get_bands/delete.
…acro fix (SPEC-73 M1)

- vbdev_tier.h: on-disk tier_superblock + tier_sb_band structs (à la bdev_raid_sb),
  TIER_SB_* constants, per-band phys_offset, sb_blocks/seq/registered fields,
  fix SPDK_BDEV_MAX_NAME_LENGTH -> TIER_BDEV_NAME_LEN (CI build error).
- vbdev_tier.c: reserve sb_blocks at start of each base bdev; geometry computes
  phys_offset for md-mirror bands (A/B) vs plain concat; io path offsets every
  physical access by phys_offset (data) / sb_blocks (md region).
WIP next: async superblock write/read + examine-based self-assembly (wwn validation).
- vbdev_tier_sb.c: serialize + CRC32c, async write-all-bands (seq bump),
  async read+validate from a base desc (building block for examine assembly).
- persist superblock to every band on register + retire_band.
- Makefile: add vbdev_tier_sb.c.
WIP next: examine-based self-assembly (read sb on boot, wwn-validate, auto-create).
Physical LBA of a cluster on the bs_dev (bdev_tier composite), so the
control-plane can map clusters to bands/tiers. New public blob API
(patch 0004), conflict-free with 0001-0003 (lib/blob only).
Single lib/blob patch (conflict-free with 0001-0003):
- spdk_blob_get_cluster_lba (M2a): cluster -> physical LBA.
- spdk_blob_claim_cluster_in_range (M2b): claim free cluster in a band's LBA range.
- spdk_blob_relocate_commit (M2b): md-thread swap-in-place + persist extent +
  release old after durability (invariants A/B; native replay reclaims orphans).
Per allocated cluster -> physical LBA (via spdk_blob_get_cluster_lba, patch 0004).
Control-plane maps LBA->band/tier from its band table. Stacked on 0002 (lvol).
- vbdev_tier_relocate_copy: direct base-bdev copy between bands (bypasses
  composite/quiesce).
- blob patch 0004: + spdk_blob_release_cluster_lba (abort path).
- lvol patch 0005: bdev_lvol_relocate_cluster RPC = claim -> quiesce(old) ->
  copy(base-direct) -> commit(md-thread swap) -> unquiesce, with abort cleanup.
- Dockerfile: DEPDIRS-bdev_lvol += bdev_tier ; lvol CFLAGS -I tier.
…EC-73 M3)

raid5f_chunk_read_complete: on !success, recompute geometry and reconstruct the
chunk from survivors+parity instead of failing. Enables degraded reads during a
bdev_tier band failure. (NULL-peer double-fault guard = follow-up.)
Patch 0007 (stacked on 0001): hot-path hook in raid_bdev_submit_request +
dense per-region read/write counters (lossy, INV-52) + RPCs
vbdev_nexus_enable_heat/get_heat/disable_heat. Read async by the brain; never
on the I/O path (INV-T2).
@rducom
rducom requested a review from a team as a code owner June 21, 2026 14:35
…t free-at-destruct

- vbdev_tier add_band: reject a wwn already present in the composite (duplicate disk →
  double-counted capacity / corrupt concat geometry).
- raid5f reconstruct read: bail out -EIO if a SECOND member is unavailable (NULL base
  channel) — raid5 tolerates one fault; avoids submitting to a missing base bdev.
- raid_bdev_free: raid_tier_heat_destroy() at nexus teardown (reclaim the heat sketch).
@rducom
rducom force-pushed the feat/spec-73-tiering branch from ae265c3 to d85cce3 Compare June 21, 2026 14:43
rducom added 13 commits June 21, 2026 17:18
Live turing deploy surfaced this: on a 1-disk node the md region had no second mirror band,
so tier_route_rw returned -EIO for the md range → GPT/raid examine failed → lvolstore init
failed (could not initialize blobstore). Now the md region is RAID1-mirrored only when a second
band exists; a single-band composite serves md single-copy on its one band (no redundancy is
possible with one disk anyway). The geometry already places md at [sb_blocks, sb_blocks+md) on band A.
Data-safety guard (G-M2b-3): bdev_lvol_relocate_cluster now rejects a cluster of a
snapshot blob (read-through-shared by clones via back_bs_dev). Moving it without
clone back-dev re-resolution (§7.3, out of v1) would corrupt the clones' view.
After the destination write, read it back and CRC32c-compare against the source (snapshotted under
quiesce, so stable). A silent media/write corruption is caught at relocate time with -EIO rather than
surfacing later through the upper-layer redundancy. Disk-to-disk copy ⇒ no accel memory-copy offload
applies; the integrity check does.
…y guard)

Relocating a snapshot blob's cluster is now permitted (§7.3). Read-through coherence for clones holds:
the copy+swap run under quiesce of the composite OLD range (a clone's read-through resolves to the
snapshot's bs_dev = this composite at old_lba, held until the atomic swap, then resumes onto new_lba),
and relocate_commit runs md-thread-serialized with snapshot create/delete/decouple. ⚠️ UNVALIDATED until
the S-D-snap spike runs on multi-tier hardware.
…eback range repair

Repairs lost ranges of a degraded member without raid internals: per stripe-aligned range, read from
the raid bdev (M3 reconstructs the degraded chunk from survivors+parity for raid5f; raid1 reads a
healthy mirror), then write the buffer back (recomputes parity / rewrites mirrors), repairing the
relocated/replacement band. Public bdev read/write API only; QD=1 background; INV-T2 (data stays in
SPDK). Works for both replica and EC. ⚠️ UNVALIDATED until exercised on EC + a real disk failure.
…band, swap detection)

vbdev_tier_assemble_band places a band at EXPLICIT stored geometry (band_id/lba_start/num_blocks/state/
is_md) so the CSI agent reassembles a composite identically across reboots and can detect a physically
swapped disk (live wwn != slot's stored wwn). bdev_tier_read_sb returns the on-disk superblock (the full
band table incl. wwn per slot). Completes A2 beyond the wwn-sort (which already fixes reorder).
… guard, F4 per-blob serialize, F6 remap-nocopy, F8 flush-before-verify, F10 atomic SB seq
…nc, SB durability

- C3: hot-remove drains per-reactor channels then closes the desc (SPDK
  REMOVE contract), persists DEGRADED; new bdev_tier_resync_md RPC rebuilds
  a replacement md leg under an md-range quiesce with channel propagation
  before activation.
- M1/T-2: md fan-out never completes the orig_io while a submitted leg is
  in flight. M2/T-6: async read failover to the mirror leg. M3: a failed md
  write leg is degraded+persisted; write succeeds off the surviving leg.
- M4/W9: -ENOMEM completes NOMEM everywhere (broken queue_io path removed).
- M5: SB generation reserved at write_all entry, fan-outs serialized and
  coalesced, DEGRADED bands excluded (unfreezes the seq); F-6 FLUSH per
  copy; F-4 LE-only guard; F-1 on-disk ABI locked by static asserts.
- M6/T-3/W7: assemble_band validates band_id/state bounds, overlap,
  capacity, duplicate wwn. W1: double-register guard. T-4/T-7/MJ6:
  retire_band is async (ack after persistence), drains before close,
  refuses md-mirror bands. m1/m2/m6/m7 minors + header comments fixed.
- rpc: md_num_blocks>0 and cluster_blocks<=u32 validation.
- CBT-1/CBT-2: epoch_freeze/epoch_close refuse -EBUSY while a rebuild is
  RUNNING on the epoch (freeing bitmap_frozen under the scanner was an UAF).
- c5: higher-generation epoch_open cannot rip a REBUILDING epoch.
- CBT-4: the rebuild FLUSHes the target before COMPLETED (a lacunary member
  believed synced after a power cut was silent corruption).
- New: anti-double-rebuild guard in the shared engine (covers the legacy
  bdev_cbt_partial_rebuild path the CSI actually calls); rebuild_id stamped
  before the first I/O (sync completion left get_rebuild_status -ENOENT);
  find_active_for_epoch discriminates by cbt node; dead override_ranges
  decoder removed; c4: no silent recreation with a virgin bitmap on
  base-bdev reappearance.
- D3: dead healthy-clear poller and backends_healthy machinery removed;
  bitmap clearing is reset-driven (documented).
…ENOSPC, S-2 socket — patches 0001-0010

- 0001: CBT-6 channel-promotion failure propagates + full unwind (never
  ONLINE with a NULL channel); P-5 runtime slot check; membership rollback.
- 0003: registry entry preallocated before pause dispatch (no orphan pause
  on OOM); UUID nonce (P-6); loud TTL clamp.
- 0004: expose spdk_blob_freeze_io/unfreeze_io (C1 foundation); m8 assert.
- 0005: C1 — relocate runs under a BLOB freeze (composite quiesce replayed
  held writes to the pre-swap LBA = lost write); N-2 blob pinned by an own
  open-ref; P-2 lvol-on-composite check; N-7/W6 remap requires DEGRADED
  source + ACTIVE destination; m9 placement scan budget.
- 0007: M7 release/acquire publish of the heat sketch (arm64), per-region
  TSC recency (no shared tick cacheline), N-8 grace-period free on disable.
- 0008: C2 — every repair chunk runs under a channel-owned LBA range lock
  (exported from lib/bdev; host writes held, replayed after write-back);
  P-3 stripe alignment validated, heap ranges; N-6 REMOVE aborts.
- NEW 0009: thin-pool ENOSPC surfaces as NVMe CAPACITY_EXCEEDED end-to-end;
  raid1 keeps the member and fails the write to the consumer (C-1).
- NEW 0010: JSON-RPC unix socket chmod 0600 after bind (S-2).
Patches regenerated from a clean per-patch commit series (format-patch
--zero-commit); all 10 apply cleanly on vanilla v26.01.
- module/bdev/tier/test: host-compiled unit tests of the PRODUCTION code
  (#include vbdev_tier_sb.c) under ASAN+UBSAN — on-disk ABI, serialize/
  validate roundtrip, corruption/version/byte-swapped-magic rejection,
  geometry inlines, seq semantics. Mock SPDK headers, no SPDK build needed.
- workflow: tests run as a lint-job step on every PR (types now include
  'opened'); compute-tag/build depend on it — a red test blocks the image.
- Dockerfile: upstream tag pinned to commit 2ef883e and verified at build
  (a moved tag fails the build). Verified end-to-end in podman: 10/10
  patches apply, zero-warning build, spdk_tgt links.
rducom added 15 commits July 4, 2026 15:37
…, patch tooling, RPC contract

On-disk superblock v2 (clean break — v1 is NOT readable, no migration: the
format never shipped publicly, every redeploy is wipe+reinstall):
- magic TIERSB02, header 256 B, band 192 B; cluster_blocks widened to u64 (F-3).
- F-5: the 256 KiB reserve is two 128 KiB A/B slots; generation seq N is written
  to slot N%2 and flushed alone, so a torn write damages at most one slot;
  readers take the highest-seq valid slot (tier_sb_select).
- F-2: generation_uuid (composite-instance fencing, minted at create, exposed by
  read_sb), created_epoch_sec, 104 B header + 32 B/band reserve. Static asserts
  192/256/12544. docs/FORMAT-tier-superblock.md.

evariops_get_capabilities (PR2): boot_id (CSI detects a target restart →
volatile state lost), tier_sb_version, method list, single_reactor_assumed (D5).

bdev_lvol_relocate_clusters (PF3): batch relocate under ONE amortized blob
freeze over N clusters (≤4096); partial success is a 200 with {relocated,
requested, error}. verify flag (PF4) forwards to each copy — skips the C5
read-back on trusted media. Regenerated patch 0005.

Tooling: scripts/patches.sh (check/apply/regen/verify, reads the pinned SHA from
the Dockerfile) + patches/README.md (order, sed wiring, upstreaming) — closes
the manual-hunk-editing risk (U-3/U-5/U-6). docs/RPC-CONTRACT.md (PR1): per-RPC
preconditions/idempotence/crash behavior + invariants P1-P5, D5, CBT-7.

Host tests extended (A/B slot selection, torn-slot fallback, binary golden
header, u64 cluster_blocks). Validated end-to-end in podman: 10/10 patches
apply, zero-warning build, spdk_tgt links.
… patches

In-repo module/script fixes:
- tier: fix leg_io double-free on md-read failover retry failure
- tier: add_band TIER_MAX_BANDS bound + registered guard (base_ch OOB)
- tier: fail channel-create on NULL base io_channel
- tier: re-validate dst_band->desc across the relocate copy chain (hot-remove)
- tier: io_type_supported consults base bands for UNMAP/FLUSH/WRITE_ZEROES
- tier: overflow-safe range/capacity checks in assemble_band
- tier: multi-band FLUSH/UNMAP/WRITE_ZEROES fan-out (fixes whole-device flush)
  and RESET propagation to all base bands
- tier(rpc): drop stale u32 cluster_blocks cap (v2 SB is u64); drop dead
  capacity_blocks/used_blocks fields
- cbt: clear out_rebuild_id on start_rebuild error; c5 takeover guard keys on an
  actually-running rebuild (fixes permanent -EBUSY after a completed rebuild)
- patches.sh: check is non-destructive (scratch index), NUL-safe patch list

Out-of-tree patch fixes (regenerated via patches.sh regen):
- 0003 nvmf-pause: re-resolve subsystem by NQN before TTL resume (TTL UAF)
- 0004 blob-relocate: roll back in-memory L2P swap on sync failure (corruption)
- 0007 raid-heat: guard blocklen==0 (SIGFPE); divide->shift + 64B pad on the hot
  path; reuse raid_bdev_find_by_name
- 0008 raid-rebuild: drop lock_range_cb function-pointer cast (compile-time safety)

Verified: series applies cleanly in order; only the 4 intended patches changed;
tier/cbt/rpc + bdev_raid_heat type-check against SPDK headers; host SB test passes.
Each test calls make_composite() which heap-allocates 3 tier_band nodes into
t->bands but never freed them — 5 tests × 3 = 15 leaked nodes (3960 bytes).
macOS ASan has no LeakSanitizer so it passed locally; the Linux CI (ASAN+LSAN)
caught it. Add a free_composite() teardown and call it at the end of each test.
…cycle UAF hardening

Implements the fork-owned items of docs/audits/2026-07-04_plan-action-besoins-fork.md
(the confrontation of spdk-csi's activation needs against the fork). csi benches
(V-EC, substrate) and the semver release tag remain out of this repo.

P1.1 — EC targeted review (raid5f rebuild_ranges + degraded-read):
  Verdict: the EC path is sound, not "written blind" — all parity math flows
  through upstream read/write (0006 reuses raid5f geometry verbatim; 0008 reads
  full stripes through the raid and writes them back, no hand-rolled parity).
  Hardening (0008): raid_repair_read_done now names the unrecoverable chunk
  (LBA/range) on a >1-fault stripe instead of an opaque -EIO.
  Load-bearing finding: reconstruct-on-read only fires for a DEGRADED/absent
  member, NOT a fresh zeroed replacement (reads its zeros successfully) — so the
  control-plane must drive rebuild_ranges in remap→rebuild order, documented in
  RPC-CONTRACT.md and docs/audits/2026-07-04_revue-ec-rebuild-ranges.md.

P1.2 — C3 geometry (0008, bdev_raid.c):
  raid_bdev_write_info_json emits full_stripe_blocks = strip_size ×
  min_base_bdevs_operational (the exact alignment rebuild_ranges validates),
  removing the num_base_bdevs_operational(=n) vs min(=k) trap for csi.

P3.5 — SEC1 audit trail (new patch 0011 + wiring):
  jsonrpc captures SO_PEERCRED at accept and exposes
  spdk_jsonrpc_request_get_peer_ucred() + spdk_jsonrpc_request_audit(). Every
  destructive handler logs "audit rpc=… peer=pid,uid,gid …":
  tier delete/retire/resync (in-repo), lvol relocate/remap/batch (0005), nvmf
  pause (0003), raid rebuild_ranges (0008). 0011 is a shared substrate applied
  with the series (README documents the forward reference).

P3.7 — lifecycle UAF/race hardening (in-repo, deferred review items):
  - tier SB fan-out: a bdev_tier_delete arriving mid-fan-out is deferred behind
    it (delete_pending) — no more freeing `t`/closing base descs under in-flight
    SB writes; tier_sb_fanout_complete reordered + vbdev_tier_sb_fanout_idle().
  - tier hot-remove: a degraded band's channel-drain+close is deferred behind an
    in-flight fan-out (close_pending) — honors the channel-before-desc contract.
  - cbt rebuild: pin the cbt vbdev's lifetime for the whole rebuild (extra desc
    when a source_bdev_name override means src_desc doesn't already pin it) +
    abort on REMOVE — fixes the UAF where a base hot-remove freed the cbt node
    (and its frozen bitmap/epoch) under a running rebuild's bare ctx->cbt.
  These are async paths macOS can't run — validate on Linux ASAN / csi bench.

P3.8 — contract + decisions:
  RPC-CONTRACT.md gains C3 (full_stripe_blocks + EC reconstruct precondition),
  G3 (ENOSPC CAPACITY_EXCEEDED divergence: legs stay ONLINE, no auto-reconverge),
  and the SEC1 audit note. D4/D5/D6 recorded in
  docs/audits/2026-07-04_decisions-d4-d5-d6.md (D5: stay -m 0x1).

Patch series 10 → 11; patches.sh check: all 11 apply cleanly on pinned 2ef883e.
All edited in-repo + vendor files pass -fsyntax-only against the patched SPDK
headers (SO_PEERCRED capture is #ifdef __linux__).
tier_sb_fanout_complete now calls vbdev_tier_sb_fanout_idle (defined in
vbdev_tier.c, not host-compilable). The T1 harness #includes vbdev_tier_sb.c but
does not link vbdev_tier.c → undefined reference at link. Add a no-op stub
(returns false = nothing deferred), matching the existing bdev-I/O stub pattern;
the SB serialize/valid/select tests never drive a fan-out to completion.
Remediates the code-review findings (docs/audits/2026-07-04_plan-remediation-
revue-code-pr13.md) and treats the deferred items (…_fork-deferred-items…).

Tier module (module/bdev/tier):
- R2  register rehydrates t->seq from on-disk SBs → seq monotone across restart
      (fork-owned; the control-plane never threads seq — contract already assumed it)
- R3  relocate flushes the destination unconditionally (durability), verify opt-in
- R4+R5 md-mirror divergence: per-leg md-ness from band + base offset (correct for
      whole-device ops); degrade md legs on submission failure, not just completion
- R6  retired band = hole: FLUSH/UNMAP/WRITE_ZEROES skip it instead of -EIO
- R7  validate blocklen↔SB-slot early (reject T10-DIF 520/4160 at add/assemble)
- R8  assemble_band -EBUSY after register (band table frozen at register)
- R9  unified async_inflight quiesce-before-teardown (relocate/resync/rehydrate);
      fanout_idle no longer drops a queued follow-up on delete_pending (would strand
      a coalesced resync cb → leaked ref → hung delete; found in adversarial review)
- R13 reject cluster_blocks==1 (keep 0=legacy, ≥2=grain)
- R15 free req.name on decode error in 7 RPC handlers
- R17 reject md_num_blocks near UINT64_MAX (align overflow)
- #3  evariops_get_capabilities methods[] filtered through the live RPC registry

CBT module (module/bdev/cbt):
- R1  distinct CBT_REBUILD_ABORTED state; an aborted rebuild is not COMPLETED
- R10 throttle poller returns BUSY without dereferencing ctx after submit_next
- R12 documented non-bug (SPDK defers inline completions via in_submit_request)

Patches:
- R11 (0004/0005) remap quarantines the old cluster (release_old=false) instead of
      freeing it to the thin pool → no re-serve into the dead band
- #5  (0004) F2 snapshot guard made intrinsic to spdk_blob_relocate_commit
- R16 (0005) decode_batch_items re-entry guard (duplicate JSON key leak)
- R14 (0010) umask(077) around bind() → atomic owner-only socket (closes TOCTOU)
Series regenerated via scripts/patches.sh; 11/11 apply cleanly on 2ef883e.

Deferred #1 (md-on-raid1) and #2 (heat passthru): documented decisions to keep the
current design (contract-stable, CSI-coupled; refactors not justified now). #4/#6
documented deferrals. L1/L2 left latent (neutralized by D5 -m 0x1).

Docs: RPC-CONTRACT.md + FORMAT-tier-superblock.md updated for the changed invariants.
Batch analogue of bdev_lvol_remap_cluster (patch 0005): collapse N
per-cluster remap round-trips into one RPC for a DEGRADED-band drain,
before a single bdev_raid_rebuild_ranges. No freeze / no copy / no
verify (the source band is dead — nothing to drain or read back).

- remap_one_precheck(): factor the per-item guards (cluster allocated ->
  source DEGRADED -> claim dst -> dst ACTIVE); on success a claim is held,
  on failure none. Single rpc_bdev_lvol_remap_cluster refactored to call
  it, so single and batch apply provably identical guards (basis of the
  idempotent-reissue property).
- remap_batch_ctx + async chain (open blob -> step -> commit(release_old=
  false, R11 quarantine) -> next); sequential, first per-item error stops
  the batch releasing its un-committed claim; returns {remapped, requested,
  error}; partial success is a 200; bounded to RPC_BATCH_MAX (4096).
  Dedicated decode_remap_batch_items (distinct container type, not a
  CONTAINEROF type-pun onto the relocate struct). SEC1 audit hook.
- capabilities: add "bdev_lvol_remap_clusters" to g_evariops_methods[]
  (runtime-filtered via spdk_rpc_get_method_state_mask).
- RPC-CONTRACT.md: contract entry with R11 quarantine + idempotency notes.

patches.sh verify: 11/11 apply cleanly (only 0005 changed). TU compiles
clean under SPDK's warning posture (mk/spdk.common.mk).
spdk_blob_claim_cluster_in_range probed the used_clusters bit-pool one
BIT at a time from c_start on every call, so a campaign filling one band
re-scanned its allocated prefix each time -> O(N^2) on the reactor (under
the batch relocate's held freeze). Perf only; claim semantics unchanged.

patch 0004 (the claim primitive):
- spdk_bit_pool_find_first_free_from(pool, start): read-only accessor that
  delegates to the word-wise spdk_bit_array_find_first_clear, keeping the
  pool abstraction (bit_pool.h, bit_array.c, spdk_util.map).
- blob_claim_cluster_in_range(..., cursor, ...) static core: word-wise
  scan (skip fully-allocated 64-bit words) + optional in/out cursor.
  spdk_blob_claim_cluster_in_range is now a cursor=NULL wrapper (existing
  callers unchanged, still get word-wise); new
  spdk_blob_claim_cluster_in_range_from exposes the cursor
  (blobstore.c, blob.h, spdk_blob.map).

patch 0005 (thread the cursor through the batches):
- struct claim_cursor + claim_cursor_for(): advance across consecutive
  same-window items, reset on window change so a stale hint can't
  spuriously -ENOSPC. Embedded in batch_ctx (relocate) and
  remap_batch_ctx (remap); remap_one_precheck gained a cursor param
  (NULL for the single form).

Correctness: cursor==NULL is bit-identical to the old scan; the cursor is
a pure hint (start=max(c_start,*cursor), results bounded to [c_start,
c_end), set_bit_allocated re-checks), so a stale/over-shot cursor never
yields an allocated or out-of-window cluster. Turns an N-claim campaign
filling one band from O(N^2) into O(bandsize + N).

patches.sh verify: 11/11 (only 0004+0005 changed). TUs compile clean
under SPDK's warning posture. No new RPC (C-API only).
…antics)

The live bitmap was never cleared during an epoch's lifetime: freeze
memcpy'd it and rebuild finalize recomputed residual from the same
accumulating bitmap. Iteration N therefore recopied the union of
everything dirtied since epoch open — residual was monotonically
non-decreasing and the control plane's iterate-until-converge loop
could not terminate by design (observed on turing: 61-89% dirty at
every freeze, full-size copies every iteration).

Freeze now snapshots via atomic per-byte EXCHANGE and leaves the live
bitmap empty, so each freeze yields the delta since the previous one
and iterative partial rebuilds converge geometrically once copy rate
exceeds dirty rate. Exchange (not memcpy+memset): a bit OR'd by an IO
thread between copy and clear would be lost — a missed chunk under
skip_rebuild is silent data divergence.

Epochs share the single live bitmap, so the clear only happens when no
OTHER epoch is active; otherwise the old snapshot-only behavior is kept
(degraded convergence, still correct).

Test mirrors updated to the new contract; new properties: sole-epoch
freeze clears live / multi-epoch freeze preserves it / 200 re-freezes
under a concurrent marker thread lose and invent zero bits (ASAN+UBSAN).
The post-write durability flush (F-6) EIO'd on every base bdev without
FLUSH support — bdev_uring in particular — so NO tier superblock was
ever persisted on such nodes: 'tier: superblock persist failed rc=-5'
at every register/retire/relocate persist, and seq monotonicity never
had an on-disk anchor.

Same contract as the CBT rebuild path: a base without FLUSH has no
volatile cache to drain — treat the completed write as durable and
skip the flush. Mock header/harness extended accordingly.
Rolled nodes intermittently boot with 'Performing recovery on
blobstore' (~70-95s md replay on SATA vs ~6s clean load) — the
shutdown unload outcome decides it, and upstream only logs it at
INFOLOG (invisible in production). Controlled experiments on turing:
graceful pod delete unloads clean both idle and under active fio;
only rollout-restarted nodes end up dirty, and not all of them — a
shutdown race we cannot attribute without the errno.

Patch 0012 logs all three outcomes (clean via fini_start, clean via
unregister path, FAILED with errno) plus the fini_start skip at
NOTICE. git-apply validated against v26.01 (2ef883e) after the
existing series.
… lifecycle guards

- H1: an exchanged-out frozen delta is merged back into the live bitmap
  before its buffer is discarded (re-freeze, close, evict) unless a rebuild
  COMPLETED — a failed/aborted iteration no longer silently loses chunks
  under skip_rebuild. Freeze now allocates before freeing (ENOMEM leaves the
  epoch intact). New cbt_epoch.frozen_live_consumed ownership flag.
- H2: dirty bits are RE-marked at I/O completion (before the host ack), so a
  snapshot-and-clear freeze consuming the submit-time bit of an in-flight
  write is harmless — no drain around freeze required. Lock-free.
- C3: epoch_invalidate refused (-EBUSY) while a rebuild is RUNNING, and the
  max-epochs eviction refuses epochs a rebuild ctx still points at (UAF).
- M1: rebuild finish gate adds !aborted — a hot-remove abort classifies
  ABORTED (resumable), no longer FAILED via a doomed flush.
- tests: resilience model aligned with production semantics (guards CBT-1/2,
  safe eviction) + 7 framing tests (H1 x4, H2, C3, M1). 60/60 green under
  ASan/UBSan. README rewritten to the reset-driven model (M4).
…semantics

- C2: band drain no longer puts a reactor's base channel with legs in flight
  (per-reactor inflight[] counters, no atomics — completions land on the
  submitting thread; deferred put at the last completion via drain_refs) and
  no longer closes the desc under a relocate/resync engine (desc_pins; a
  close landing while pinned defers to the engine's terminal unpin). The
  final close runs on the app thread (spdk_bdev_close thread contract).
- C4: md-resync re-validates src/dst liveness before every chunk, write and
  final flush (-ENODEV early abort); the C2 pins make a close under the
  engine structurally impossible.
- H3: composite-global SB state (seq, pending cbs, inflight/queued flags) is
  owned by t->thread; the reactor-side md-leg degrade funnels its persist
  through spdk_thread_send_msg and re-resolves the composite via g_tier_nodes.
- H5: shared tier_flush_or_durable() — no-FLUSH bases (bdev_uring) treat the
  completed write as durable instead of failing every relocate/md-resync
  with -ENOTSUP (same contract as the SB persist fix 54d02b2).
- H6: a whole-device FLUSH no longer fails on a DEGRADED data band — the
  segment is a logged no-op (range already unreachable via -EIO on R/W);
  mutations and md legs keep failing honestly. Single-disk loss no longer
  turns every FS barrier into a full-volume read-only outage.
- M2: the SB fan-out ctx-calloc ENOMEM path now runs
  vbdev_tier_sb_fanout_idle() — a deferred delete behind a queued follow-up
  is no longer stranded (composite/claims leak, -EEXIST on re-create).
- M3: a fan-out that launched ZERO band writes completes -ENODEV, never
  rc==0 — the MJ6 durability contract is honored (a retire on a composite
  with all SB-carrying bands degraded is no longer acked as durable).
- tests: test_tier_sb gains calloc fault injection + M2/M3 framing tests
  against the production code.
…004/0005)

Regenerated mechanically via scripts/patches.sh regen from a 2ef883e clone
with the series applied as commits (git am + rebase --autosquash) — the
first fully mechanical round trip; patches.sh check passes 12/12 on a
pristine pinned tree.

- C1-DRAIN (0005): the relocate flows (single + batch) now take an LVOL-BDEV
  spdk_bdev_quiesce before the blob freeze: outstanding host I/O is DRAINED
  (LBA-range lock semantics) and new I/O is held ABOVE the L2P translation
  (re-translates on release). The blob freeze alone only gated new
  submissions — a write already in flight to the bs_dev could land on
  old_lba AFTER the copy read it: ACKed write lost on commit, cross-blob
  overwrite once old_lba was reallocated, undetectable by the C5 CRC.
  Release order: unfreeze → unquiesce (also on the leaked-freeze path).
  remap is unaffected (source band DEGRADED, no live-source copy).
- H4 (0004+0005): an ambiguous commit failure (extent write dispatched,
  error completion — it may still have reached media) QUARANTINES the
  destination cluster instead of releasing it: a durable extent may
  reference it, and re-serving it would create two durable owners after
  restart. Bounded single-boot in-RAM leak; replay reconciles either way
  (contract documented in blob_relocate_extent_written).
- Side effect of the mechanical regen: subjects normalized x/11 → x/12 and
  0012's hand-edited hunk stats corrected; README table gains the missing
  0012 row and the CFLAGS provenance fix (patch Makefile hunk, not
  Dockerfile sed) — audit A2.
- Breaking (D3/L1): bdev_cbt_epoch_list no longer emits
  healthy_clear_suspended / backends_healthy (dead state removed).
- epoch_invalidate -EBUSY guard (C3) and the H1 delta-preservation
  invariant.
- relocate section updated to the C1-DRAIN barrier (lvol-bdev quiesce +
  inner blob freeze) and the H4 quarantine contract.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant