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 | β | 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 |
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 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_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.