# DF-2645 — flush-driven indirect-block collapse racing a concurrent child modification

**Class:** lock/refcount race in the flusher's indirect-maintenance path,
reached from `hammer2_flush_core()` (sys/vfs/hammer2/hammer2_flush.c:1039-1042).

**Reach:** unprivileged local user (file create/delete + append + sync on
a mounted hammer2 PFS).

**Status:** not_reproduced after 2 stress attempts (~35 min, >130M
appends + continuous churn + 20 Hz sync loop); race is code-proven
(confidence: likely).  See VERDICT.md for the full analysis and run.log
for the experiment transcripts.

## Reproduce

```
cc -O2 -o stress2645 stress2645.c
cc -O2 -o mknames   mknames.c
cc -O2 -o mkfiles   mkfiles.c
cc -O2 -o delsome   delsome.c
sh run_df2645.sh 12 1        # 512MB vn image, 6000-file dir, 70% delete,
                             # 4 append writers + create/delete churn +
                             # sync loop
```

Success criterion (not achieved): kernel panic with the
`hammer2_chain_repchange` KKASSERT (hammer2_chain.c:2314) or
`hammer2: debug repchange` console prints; alternatively any
LOST CHILD/overlap marker from the maintenance path.

## Mechanism (why it should be possible)

1. flush_core(B) [B = sparse INDIRECT with UPDATE set] scans+flushes
   B's children bottom-up, updating B's media blocktable per child
   (hammer2_flush.c:1116-1134).
2. A frontend write modifies a file-inode chain F under B AFTER F's
   flush completes but BEFORE flush_core(B) reaches
   `hammer2_chain_indirect_maintenance(parent, B)` (:1040) —
   `hammer2_chain_modify()` COWs F to a fresh data_off
   (hammer2_chain.c:1437+), so F's in-memory bref diverges from B's
   media entry.
3. maintenance's collapse loop hits the skip guard
   (`bcmp(&bsave, &sub->bref)` mismatch, chain.c:4202-4209) and leaves
   F in B's rbtree.
4. `hammer2_chain_repchange(parent, B)` (chain.c:4228 → :2314)
   unconditionally KKASSERTs B is empty → panic (DoS); on
   non-INVARIANTS semantics the reptracks migrate while F is still
   parented to the already-deleted B → F's media entry becomes
   unreachable (silent data loss).

## Suggested fix

Defer the destructive `hammer2_chain_delete(parent, chain)`
(chain.c:4160) until after a clean full sweep of the collapse loop;
abort the collapse (return 0, flush retries later) when any child was
skipped, keeping repchange's invariant true.
