β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2660

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).

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2660 Β· 2 files
FileTypeDescriptionSize
README.md file 2.9 KB ↓ raw
verdict.json file 2.2 KB view raw
README.md file
↓ download 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_testable
per-fix-DF-2660

Confirmed 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.