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

POSIX EDEADLK not detected for F_SETLKW deadlock cycles spanning two (or more) files

Field Value
ID DF-2824
Status new
Severity Info
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L
CWE CWE-667 (incomplete deadlock detection)
File sys/kern/kern_lockf.c
Lines 406-413 (waits :423-426)
Area kern
Confidence certain
Discovered 2026-08-31
Pass 2 (GLM 5.3 second pass)
Bucket base:kern
Reported pending
Known CVE none
CVE match novel

Summary

lf_setlock()'s deadlock check scans only the blocked list of the same lockf for the conflicting owner, so any lock cycle spanning two files closes without EDEADLK and all members hang in F_SETLKW. POSIX.1-2008 2.9.7 requires [EDEADLK]. flock-style locks get no deadlock check at all. Impact bounded: the sleep is PCATCH (killable β€” unlike DF-2758's unkillable FOFFSETLOCK sleep); an attacker gains nothing beyond what holding an advisory lock forever already buys.

Proof of contest

VERIFIED deterministically on the guest (findings/poc/DF-2824/): two children, two files, two-phase gate β€” child1 locks A then waits on B, child2 locks B then waits on A; both remain blocked after 4s (BUG-REPRODUCED) instead of one exiting with EDEADLK; 3/3 runs. Fix: architectural (cross-file owner wait-graph, FreeBSD's lf_owner_t model) β€” no inline diff shipped (an untested invasive diff would be worse than none; VERDICT.md has the design).

Timeline

  • 2026-08-31 Discovered during pass-2 audit of kern_lockf.c (GLM 5.3); deterministic cross-file deadlock reproduced same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2824 Β· 10 files
FileTypeDescriptionSize
df2824_deadlock.c β€” 4.1 KB view raw
build.sh β€” 93 B view raw
run.sh β€” 92 B view raw
build.log β€” 11 B view raw
run.log β€” 243 B view raw
run.1.log β€” 289 B view raw
run.2.log β€” 243 B view raw
run.3.log β€” 243 B view raw
env.txt β€” 287 B view raw
VERDICT.md β€” 3.8 KB ↓ raw
VERDICT.md
↓ download raw

DF-2824 VERDICT β€” REPRODUCED (bounded impact)

Bottom line

lf_setlock()'s POSIX deadlock detection (sys/kern/kern_lockf.c:406–413) is single-level and single-file: when about to block, it scans only the blocked list of the same lockf for the conflicting owner:

    if (flags & F_POSIX) {
        TAILQ_FOREACH(brange, &lock->lf_blocked, lf_link) {
            if (brange->lf_owner == range->lf_owner) {
                error = EDEADLK;
                goto do_cleanup;
            }
        }
    }

Any cycle that spans two files therefore closes without EDEADLK and every member hangs in F_SETLKW. Reproduced deterministically on the stock guest: two children, two files, two-phase sync gate; both still blocked after 4 s; POSIX requires EDEADLK for the cycle closer. The PoC then SIGKILLs both β€” the sleep is PCATCH (kern_lockf.c:427), so this is a killable hang, which is why this is filed Info and not as a hard DoS: the only victims are processes that voluntarily entered F_SETLKW against attacker-held locks (the attacker could equally just hold the lock forever β€” the missing EDEADLK adds spec non-compliance, not a new privilege-relevant primitive).

Why the check cannot see two-file cycles (path:line)

  • The blocker identity is range->lf_owner β€” the conflicting range's owner (kern_lockf.c:363–377 scan, deadlock check at 406–413).
  • Wait records (brange) are enqueued on lock->lf_blocked of the file being waited on (kern_lockf.c:423–426). There is no cross-file registry of "who is currently waiting on what" β€” the only per-owner wait state lives in the thread's own tsleep β€” so a walk from the blocker to its own wait is impossible without new state. struct proc has no blocked-lock list; struct lockf has no owner index. Hence single-hop, single-file.
  • flock-style locks get no deadlock check at all (if (flags & F_POSIX), kern_lockf.c:406; the in-code XXX at 396–405 acknowledges this).

Reproduction notes

  • First harness attempt raced: child1 acquired B before child2 locked it (and an ignored first-lock failure printed a stale errno), yielding a false NOT-REPRODUCED. Fixed with a two-phase gate (both children verify their first lock, signal the parent, and only proceed when the parent opens the gate) and checked return codes. Final harness reproduces 3/3 runs (run.1.log, run.2.log, run.3.log).
  • Guest left clean: children reaped, temp files unlinked, locks released on child exit (closef per-close POSIX unlock, kern_descrip.c:3051–3062).

Exploit chain

None β€” no memory corruption, no info leak. Impact ceiling: unkillable-by- default it is not (PCATCH); an attacker can wedge only processes it can already stall by holding advisory locks. POSIX-conformance defect with a denial-of-service flavor for cooperative lock users (e.g., mail spool / DB lock files shared across users where a malicious co-user holds locks and forces a well-behaved process into an undetected cross-file deadlock instead of the mandated EDEADLK error path).

Fix validation

Not performed (architectural change; no minimal safe diff). Recommended direction: FreeBSD-style lf_owner_t graph (owner objects with blocking links + transitive iterator with visited set), or minimally a per-proc list of active wait records maintained at brange enqueue/dequeue under p_token, walked transitively on the block path. No fix.diff is shipped for this finding because an untested invasive diff would be worse than none.

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

Architectural change (owner wait-graph); no minimal safe diff exists -- kernel rebuild/fix validation not applicable for an Info-severity conformance defect.

['VERDICT.md']
per-fix-DF-2824

Confirmed kernel references

Detail

Exploit chain

two processes (or one attacker + one victim using F_SETLKW on attacker-lockable files) -> cross-file WRLK cycle -> missing EDEADLK -> both sleep in tsleep(PCATCH) until signaled; no memory-safety primitive

Evidence (decisive lines)

['run.log line 2: T2_RESULT: BUG-REPRODUCED both processes still blocked in F_SETLKW after 4s', 'run.1/2/3.log: 3/3 deterministic reproductions with the two-phase gate harness', "VERDICT.md 'Why the check cannot see two-file cycles': no cross-file owner-wait registry exists (branges live only on the waited file's lf_blocked, kern_lockf.c:423-426)"]

PoC changes

First harness raced (child1 grabbed B before child2 locked it; an ignored first-lock failure printed a stale errno) giving a false NOT-REPRODUCED; rewrote with a two-phase parent gate and checked return codes, then reproduced 3/3.

Verified recommended fix

Adopt a cross-file owner wait-graph (FreeBSD lf_owner_t + transitive iterator with visited set) or maintain a per-proc active-wait-record list under p_token walked transitively on the block path; too invasive for an untested inline diff, none shipped

Verdict

lf_setlock()'s EDEADLK check (kern_lockf.c:406-413) scans only the same file's lf_blocked list for the conflicting owner, so a two-file F_SETLKW deadlock closes without EDEADLK and both processes hang (deterministically reproduced: both children still blocked after 4s; POSIX.1-2008 2.9.7 requires EDEADLK). Impact is bounded: the sleep is PCATCH (killable), and an attacker gains nothing beyond what holding an advisory lock forever already buys; filed Info for the spec violation with a DoS flavor for cooperative lock users.