# 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:

```c
	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.
