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

AB-BA kernel deadlock: jail_lock vs per-CPU gd_sysctllock β€” unprivileged `sysctl jail.list` readers vs root jail(2) permanently wedge the kernel

Field Value
ID DF-2799
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:C/C:N/I:N/A:H
CWE CWE-667 Improper Locking
File sys/kern/kern_jail.c
Lines 275 (outer jail_lock), 223/235 (sysctl create under it), 696 (handler takes jail_lock)
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

sys_jail holds the global jail_lock across the entire syscall, including prison_sysctl_create(), whose SYSCTL_ADD_* macros take the all-CPU sysctl xlock (every CPU's gd_sysctllock exclusively). Meanwhile any unprivileged user reading the world-readable sysctl jail.list holds its own CPU's gd_sysctllock shared across the whole handler dispatch and then blocks acquiring jail_lock inside sysctl_jail_list. Two CPUs, each holding one lock and sleeping on the other; lockmgr never times out and there is no cycle detection. Cascade: jail_lock is global (all jailed_ip() network paths, jail(2)/jail_attach(2), last-ref prison_free) and the stuck xlock walk already holds CPU locks 0..k exclusively β€” even echo dies (new process startup blocks on rtld boot sysctls).

Threat model & preconditions

Unprivileged local user (jail.list needs no privileges) permanently hangs the entire machine β€” host and all jails β€” whenever an administrator or automation runs jail(2) (routine on jail-hosting/CI hosts). Reboot required; no recovery path. No uid0 route (pure deadlock).

Proof of contest

VERIFIED on the stock guest (findings/poc/DF-2799/): 4 unpriv readers + 3000-iteration jail churn wedged the guest in seconds β€” markers 124 (timeout), ssh dead, maxproc flood from the never-reaping deadlocked parent. Fixed kernel (create the prison's sysctl tree BEFORE publishing and outside jail_lock; drop the outer jail_lock before kern_jail): the same storm completed with 1,146,880 reads / 0 errors.

Full git-apply-able diff in findings/poc/DF-2799/fix.diff (validated).

Timeline

  • 2026-08-31 Discovered during pass-2 audit of kern_jail.c (GLM 5.3); unpriv-triggered whole-system wedge reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2799 Β· 11 files
FileTypeDescriptionSize
jailchurn.c β€” 2.5 KB view raw
jailistreader.c β€” 986 B view raw
listonce.c β€” 928 B view raw
jailonce.c β€” 759 B view raw
run.sh β€” 1.7 KB view raw
run.log β€” 10.5 KB view raw
run.patched.log β€” 469 B view raw
serial_wrong_oid_run.log β€” 2.4 KB view raw
env.txt β€” 373 B view raw
fix.diff β€” 5.1 KB view raw
verdict.json β€” 4.8 KB view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Same storm on the patched kernel ran to completion: 4 readers performed 1,146,880 jail.list reads with 0 errors while the 3000-jail churn ran; listonce returned live data (rc=0) DURING the storm; jailonce rc=0; guest fully responsive after. Baseline wedge (markers 124, echo dead, maxproc flood) is gone.

findings/poc/DF-2799/run.patched.log; fix.diff (hunks: kern_jail.c kern_jail/sys_jail restructure)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 1 13:39:40 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

unprivileged user: while(1) sysctlbyname("jail.list") β€” each read parks holding a per-CPU gd_sysctllock; concurrent root jail(2) reaches SYSCTL_XLOCK under jail_lock; cycle closes; whole-system wedge (all jails, all sysctl activity, new process exec). DoS ceiling; no memory-safety primitive.

Evidence (decisive lines)

['findings/poc/DF-2799/run.log β€” attempt-2 evidence: PRE markers rc=0, post-storm PROBE/TRIVIAL/MARKERS all 124 with no output, maxproc console flood, flood-lines=0 note', 'findings/poc/DF-2799/run.patched.log β€” fixed kernel: storm survives, POST-LISTONCE:0 (335 bytes listed mid-storm), 1.15M reads 0 errors', 'findings/poc/DF-2799/serial_wrong_oid_run.log β€” attempt-1 context (wrong oid; wedge that run was DF-2802-family)']

PoC changes

Reader oid corrected from kern.jail.list to jail.list (the SYSCTL_NODE parent is the root, not kern β€” attempt 1 used the wrong oid and its readers no-op'd); run_user reader launch fixed (cd must precede each nohup; 'cd X && nohup A &' backgrounds the compound and later nohups run in the wrong cwd); run.sh rewritten as a host-side vm.sh driver with every guest command timeout-wrapped because the guest wedges by design.

Verified recommended fix

Never call sysctl add/remove while holding jail_lock: create the prison sysctl tree before publishing the prison (and outside jail_lock) in kern_jail(), and release sys_jail's outer jail_lock before calling kern_jail(); prison_free already drops the lock before prison_sysctl_done.

Verdict

REPRODUCED: permanent kernel-wide AB-BA deadlock between the global jail_lock and the per-CPU gd_sysctllock locks. Root side: sys_jail holds jail_lock (kern_jail.c:275) across prison_sysctl_create (:223) whose SYSCTL_ADD_* macros take the all-CPU sysctl xlock (_sysctl_xlock, kern_sysctl.c:1641-51). Unprivileged side: userland_sysctl holds its CPU's gd_sysctllock SHARED across the whole handler dispatch (kern_sysctl.c:1571-73) and sysctl_jail_list blocks acquiring jail_lock (kern_jail.c:696) inside it. On the stock kernel, 4 unprivileged jail.list readers plus a 3000-iteration jail(2) churn wedged the guest in seconds: markers listonce/jailonce returned rc 124 under timeout, even echo stopped returning (new process startup blocks on the exclusively-held per-CPU locks via rtld boot sysctls), and the serial console flooded 'maxproc limit exceeded by jailchurn' because the deadlocked parent never reaps. Zero id-sharing flood lines at 3000 iterations, isolating this wedge from DF-2802. On the patched kernel (prison_sysctl_create moved before publication and out of jail_lock; sys_jail drops the lock before kern_jail) the identical storm completed: 1,146,880 jail.list reads with 0 errors, markers rc=0, guest healthy. Permanent unrecoverable hang requiring reboot; unprivileged-triggerable whenever jail(2) runs on the host.