β¬’ 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 β€” 128 B view raw
run.sh β€” 75 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.4 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 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.