hammer2_sync_insert() TOCTOU re-lookup races concurrent same-key creates on mounted rw clusters: KKASSERT(chain == NULL) panics INVARIANTS kernels (duplicate-key chain_create on production)
| Field | Value |
|---|---|
| ID | DF-2660 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-367 TOCTOU (CWE-672 operation after validation) |
| File | sys/vfs/hammer2/hammer2_synchro.c |
| Lines | 458 (inode unlocked), 741-758 (relock + assert) |
| Area | vfs |
| Confidence | likely |
| Discovered | 2026-08-29 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | hammer2 |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
The sync scan runs with the inode unlocked (synchro.c:458) and the
parent locked shared, so VOP create/rename xops on a mounted rw
multi-master cluster execute concurrently against it. When the scan
finds a key missing locally, hammer2_sync_insert() drops the locks,
relocks the parent exclusively, and re-runs the lookup as a TOCTOU
re-check β then asserts the result is still missing:
KKASSERT(chain == NULL) (synchro.c:758). A user doing rm X; echo > X
(same name β same dirent key) that lands in the relock window inserts
the key on ALL columns (including the sync thread's own idx), the
re-lookup finds it, and the INVARIANTS kernel panics; production kernels
instead drive hammer2_chain_create() into a duplicate-key insert
against an RB tree whose comparator treats overlap as match
(chain.c:113-117). Live on stock for β₯3-master clusters (quorum
reachable among 2 participants) and on DF-2657-fixed kernels for
2-master clusters; on stock 2-master clusters the DF-2657 h2coll park
hides it.
Threat model & preconditions
Local unprivileged user on a mounted rw multi-master hammer2 cluster racing file recreation against the 5-second sync passes' insert path.
Proof of concept
Structural proof at findings/poc/DF-2660/README.md; the live hammer
(tight rm -f X; echo data > X loop) could not complete honestly β
every multi-session cluster run on this guest was terminated early by
the pre-existing DF-2620-family teardown UAF (see DF-2657's
panic_serial.log).
Recommended fix
Treat the raced chain as adopt-and-retry instead of a panic:
--- a/sys/vfs/hammer2/hammer2_synchro.c
+++ b/sys/vfs/hammer2/hammer2_synchro.c
@@ -755,7 +755,14 @@
chain = hammer2_chain_lookup(parentp, &dummy,
focus->bref.key, focus->bref.key,
&error,
HAMMER2_LOOKUP_NODIRECT |
HAMMER2_LOOKUP_ALWAYS);
- KKASSERT(chain == NULL);
+ if (chain != NULL) {
+ /* raced a concurrent modify on our column: adopt it */
+ hammer2_chain_unlock(chain);
+ hammer2_chain_drop(chain);
+ return (HAMMER2_ERROR_EAGAIN);
+ }
References
- DF-2657 (whose fix exposes this on 2-master clusters), DF-2618 (the RB overlap-as-match comparator)
Timeline
- 2026-08-29 Discovered during pass-2 audit of hammer2_synchro.c (GLM 5.3).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2660 Β· 2 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | file | 2.9 KB | β raw | |
| verdict.json | file | 2.2 KB | view raw |
DF-2660 β hammer2_sync_insert() re-lookup TOCTOU: concurrent same-key
create on a mounted rw cluster trips KKASSERT(chain == NULL) (panic on
INVARIANTS) or duplicate-key chain_create on production kernels
sys/vfs/hammer2/hammer2_synchro.c:736-758.
Medium / hammer2 bucket. Code defect: certain (the race window is structural). Reproduction: not attempted beyond bounded opportunity (status: untested) β see below.
Root cause (path:line)
The sync thread scans with the parent locked SHARED and the inode UNLOCKED (hammer2_inode_unlock at synchro.c:458), so VOP writers on a mounted rw cluster run concurrently against the scan. When the scan finds a key missing locally (n > 0 path, synchro.c:555-577), hammer2_sync_insert() relocks the parent exclusively and re-runs the lookup as a TOCTOU re-check:
sys/vfs/hammer2/hammer2_synchro.c:741-758: ...unlock child/parent... hammer2_chain_unlock(*parentp); hammer2_chain_lock(*parentp, HAMMER2_RESOLVE_ALWAYS); chain = hammer2_chain_lookup(parentp, &dummy, focus->bref.key, focus->bref.key, &error, ...); KKASSERT(chain == NULL); /* fires on the race */ chain = NULL; error = hammer2_chain_create(parentp, &chain, ...);
If a concurrent create/rename xop (which writes ALL columns, including
the sync thread's own idx) inserts the same key between the shared
iteration and the exclusive re-lock β e.g. a user does
rm X; echo > X (same dirent key = same name hash) while the sync
thread is inserting an older X from the focus β the KKASSERT panics the
INVARIANTS kernel. On production kernels chain_create would encounter
an existing overlapping chain (hammer2_chain_cmp treats overlap as
match), with duplicate-key handling by the RB tree left to chance.
Note: on STOCK 2-master clusters this window is unreachable because the sync threads park forever in the DF-2657 quorum wedge before the scan; it is live on stock for >=3-master clusters (quorum reachable among 2 participants) and on any kernel carrying the DF-2657 fix.
Fix
if (chain != NULL) {
/* raced a concurrent modify on our column: adopt it */
hammer2_chain_unlock(chain);
hammer2_chain_drop(chain);
return (HAMMER2_ERROR_EAGAIN);
}
(replace the KKASSERT; the caller already handles EAGAIN by
re-running the pass β synchro.c:190-191.)
PoC status
untested β a deliberate hammer (tight rm X; echo > X loop against a
diverged merged cluster across several 5 s sync passes) was planned for
the fix kernel, but every multi-session cluster run on this guest was
terminated early by the pre-existing DF-2620-family teardown UAF
(see DF-2657 panic_serial.log), so no honest reproduction attempt
completed. The window and the panic assertion are structural,
line-cited above.
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-2660/README.md (race window path:line, panic assertion, fix)
PoC changes
n/a (no seed PoC existed; audit-time discovery)
Verified recommended fix
Replace KKASSERT(chain == NULL) in hammer2_sync_insert() with an EAGAIN return when the re-lookup finds a raced chain (the caller already retries on EAGAIN).
Verdict
Not verified on the guest (status untested): the planned hammer (tight rm/recreate loop of the same filename against a diverged merged rw cluster, racing the 5s sync passes' insert path) could not be run to completion because every multi-session cluster run on this guest was killed by the pre-existing DF-2620-family teardown UAF (DF-2657 panic_serial.log). The defect is structural and line-certain: the sync scan runs with the inode unlocked (synchro.c:458) and the parent shared, hammer2_sync_insert() relocks exclusively and re-looks-up the key as a TOCTOU check, then KKASSERT(chain == NULL) (synchro.c:758) panics INVARIANTS kernels when a concurrent VOP create/rename xop (writing all columns incl. idx) inserted the same key in the window; production kernels fall into duplicate-key chain_create against the RB tree (hammer2_chain_cmp counts overlap as match, chain.c:113-117). Reachable on stock for >=3-master clusters and on DF-2657-fixed kernels for 2-master clusters.
No comments yet.