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)
PoC verification
Evidence pack
findings/poc/DF-2824 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 onlock->lf_blockedof 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 owntsleepβ so a walk from the blocker to its own wait is impossible without new state.struct prochas no blocked-lock list;struct lockfhas 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_testableArchitectural change (owner wait-graph); no minimal safe diff exists -- kernel rebuild/fix validation not applicable for an Info-severity conformance defect.
['VERDICT.md']
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.
No comments yet.