# DF-3118 verdict — full narrative

## The defect (certain)

`hmp->flusher.{done,next}` (sys/vfs/hammer/hammer.h:772-781), flush-group
`seq` (hammer.h:292) and reservation `flg_no` (hammer.h:740) are **signed
32-bit** sequence numbers used as a BSD-style ring: every consumer computes
`a - b > 0` to ask "is a ahead of b":

* `hammer_flusher_wait()`  sys/vfs/hammer/hammer_flusher.c:166
* `hammer_flusher_flush()` KKASSERT + mis-sequencing branch :338-357
* delay-list release check :447-451 (with hammer_blockmap.c:730,736)
* `hammer_flusher_running()` :178-181

The compiled module uses `sub; test; jle` (see disasm.txt) — a wrapping
32-bit subtraction whose result is then read as *signed*. Ring arithmetic
with a signed difference test is only correct while the distance between
allocator and consumer stays below 2^31; past that every comparison flips.

The allocating side is attacker-influenceable without privilege:
`HAMMERIOC_SYNCTID` with `op=HAMMER_SYNCTID_ASYNC` is unprivileged
(hammer_ioctl.c:72 computes `caps_priv_check` into `error`, but the
`case HAMMERIOC_SYNCTID:` handler at hammer_ioctl.c:117-120 **overwrites
`error` unconditionally**), and each call bumps `hmp->flusher.next` once
with no waiting when no flush groups are pending (hammer_flusher.c:114-122).
Measured: ~65k seq/s per process, ~780k/s with 12 processes (run.log).

## What was demonstrated on the guest

### Mechanism (reproduced from an injected wrapped state)

`seqshift.ko` (root, setup-phase; findings/poc/DF-3118/seqshift.c) set
`next = done + 2^31 - 200000`, i.e. `next` negative while `done` positive —
the exact field-value state that a successful 2^31-lap would produce. The
running **unprivileged** burners kept allocating; the grouper's flush group
was created at the wrapped frontier. The master then executed
hammer_flusher.c:338-357 with `flg->seq` negative and `seq = done+1`
positive:

* `seq == flg->seq` fails,
* `KKASSERT(flg->seq - seq > 0 ...)`: the difference underflows INT_MIN and
  wraps **positive**, so the assert passes (no panic text — consistent with
  the silent serial console in wedge_boot.log),
* `flg = NULL`: the pending group is permanently unreachable,
* `hammer_flusher_haswork()` is true forever (group list non-empty), so the
  master executes a **synchronous finalize (volume-header + undo flush +
  disk cache flush) for every dummy sequence number** — a self-sustaining
  fsync storm.

Observed: guest wedged within ~2 minutes; ssh connections timed out during
banner exchange on 25 s and 30 s retries five minutes apart; serial console
silent; QEMU still running; only a hard reset (`vm.sh reset with-src`)
recovered the machine. That is a full system denial of service **from the
wrapped-counter state**, with no kernel memory corruption involved.

### Organic reachability (not demonstrated; refuted on this configuration)

Two organic attempts were run:

1. 12 unpriv burners, 22 minutes, **914,588,507** sequence numbers allocated
   (~780k/s). Probe (wedge_boot.log): `done=914583800 next=914588507
   gap=4707`. The single master flusher thread consumed 99.9995% of the
   allocations.
2. 12 burners + 1 grouper + 4 writers dirtying 11.3 GB in 7 minutes (43k/s
   per burner, ~520k/s aggregate). Gap stayed 5.5k-9k (gapseries.txt), and
   **2 seconds** after the burners were killed the gap had drained to 2.

Conclusion: on this guest (6 vCPU, fast qcow2 disk) the master's per-sequence
consumption capacity exceeds the `fs_token`-serialized unpriv allocation
ceiling, so the ring distance cannot accumulate toward 2^31 — it returns to
~5k after every master I/O stall. Reaching the lap organically would require
~2^31 net allocations during cumulative master stalls (~2000 s at the
measured 430k/s with the master continuously pinned in synchronous
finalizes), plausible only on configurations where the frontend vastly
outpaces the flusher's disk (very slow/network storage with many fast
frontend cores) — not demonstrated here.

### Bottom line

The arithmetic defect is real and its post-condition (wrapped ring) is a
hard, self-sustaining system wedge, demonstrated by driving the kernel
through the exact wrapped state. The unprivileged trigger, while cheap per
operation, could not out-race the master flusher on the test configuration,
so end-to-end unprivileged reproduction is **inconclusive**. If any future
change (or a slow-storage deployment) lets the frontend allocate faster than
the master consumes for a sustained period, this becomes an unprivileged
system DoS plus, at the wrap seam, flush-group mis-sequencing (silent
on non-INVARIANTS builds) and premature delay-list reservation release.

## Fix

`fix.diff` (git-apply-able, verified against the read-only tree): make
`done/next`, `flg->seq`, `resv->flg_no` and the local `seq` variables
`unsigned`. Modular 2^32 arithmetic then keeps every difference comparison
correct for distances < 2^31 and the compiler emits proper ring codegen
(`cmp/je` + unsigned branches — fix_build.log, module compiled clean with
`-Werror` against the patched headers in the guest's /usr/src copy).

Full kernel A/B validation was not performed (no organic reproduction to
A/B against); the fix compile-checks and the codegen change is verified.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
