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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2799 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
fixedSame 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)
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.
No comments yet.