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

CTLFLAG_PRISON never enforced: jailed uid-0 can write host-global sysctls (kern.maxfiles, kern.securelevel, debug.*, net.*, vm.*)

Field Value
ID DF-2738
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:L/I:H/A:H
CWE CWE-863 Incorrect Authorization
File sys/kern/kern_sysctl.c
Lines 1438-1450 (with kern_jail.c:883-885)
Area kern
Confidence certain
Discovered 2026-08-30
Pass 2 (GLM 5.3 second pass)
Bucket privesc
Reported pending
Known CVE none
CVE match novel

Summary

sysctl_root's write gate honors CTLFLAG_WR/CTLFLAG_SECURE and caps_priv_check(SYSCAP_NOSYSCTL_WR), but prison_priv_check deliberately allows SYSCAP_NOSYSCTL_WR inside jails and CTLFLAG_PRISON ("prisoned roots can fiddle") is consulted nowhere in the kernel. A uid-0 process inside a jail therefore passes every write gate for plain RW variables and mutates HOST-global kernel state. Only kern.hostname is safe because its handler implements jail semantics itself.

Threat model & preconditions

Multi-tenant hosting model (root inside a customer jail): jailed root reconfigures the shared host kernel β€” host-wide DoS (kern.maxfiles/maxproc), shared network stack (net.*), kern.sugid_coredump=1 (host setuid core dumps, credential-disclosure assist), kern.securelevel raise while host securelevel ≀ 0 (host admin brick). Integrity+availability break across the jail boundary; not a direct host uid=0.

Proof of concept

VERIFIED twice on the stock INVARIANTS guest (findings/poc/DF-2738/): static jailwrite.c executed via jail(8) as real root; inside the jail uid=0 writes kern.sugid_coredump (host observed 0β†’1), kern.maxfiles, debug.sysctl, kern.securelevel β€” 4/4 "WROTE-HOST-GLOBAL". Unprivileged non-root writes remain EPERM (control). Fix (deny jailed-credential writes to nodes lacking CTLFLAG_PRISON + flag kern.proc.args) compile- verified (not boot-validated).

--- a/sys/kern/kern_sysctl.c
+++ b/sys/kern/kern_sysctl.c
@@ -1449,6 +1449,16 @@ sysctl_root(SYSCTL_HANDLER_ARGS)
    {
        return (error);
    }
+
+   /*
+    * A confined (jailed) root may only write nodes explicitly
+    * marked CTLFLAG_PRISON.  prison_priv_check() deliberately
+    * allows SYSCAP_NOSYSCTL_WR inside jails, so without this check
+    * a jailed uid-0 can mutate host-global state even though
+    * CTLFLAG_PRISON exists for exactly this purpose and is honored
+    * nowhere.
+    */
+   if (req->newptr && td->td_ucred && jailed(td->td_ucred) &&
+       (oid->oid_kind & CTLFLAG_PRISON) == 0) {
+       return (EPERM);
+   }

plus CTLFLAG_PRISON on the kern.proc.args node (kern_proc.c:2205) β€” full diff in findings/poc/DF-2738/fix.diff.

Timeline

  • 2026-08-30 Discovered during pass-2 audit of kern_sysctl.c (GLM 5.3); jailed-root writes reproduced on stock same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2738 Β· 10 files
FileTypeDescriptionSize
jailwrite.c β€” 2.0 KB view raw
build.sh β€” 178 B view raw
run.sh β€” 872 B view raw
run.log β€” 431 B view raw
run.2.log β€” 431 B view raw
build_fix.log β€” 1.1 KB view raw
fix.diff β€” 1.2 KB view raw
VERDICT.md β€” 3.2 KB ↓ raw
manifest.json β€” 867 B view raw
verdict.json β€” 4.9 KB view raw
VERDICT.md
↓ download raw

DF-2738 verdict

status: reproduced (2 clean runs, run.log / run.2.log) impact: dos (host-global kernel-state rewrite from inside a jail; arbitrary host availability/integrity impact, e.g. kern.maxfiles; closest enum is dos β€” the ceiling includes host-wide integrity changes such as kern.sugid_coredump=1 and kern.securelevel raising) confidence: certain

Root cause chain (path:line)

  1. sysctl_root write gate β€” sys/kern/kern_sysctl.c:1438-1450: requires CTLFLAG_WR, honors CTLFLAG_SECURE vs securelevel, then caps_priv_check(td->td_ucred, SYSCAP_NOSYSCTL_WR). No prison check.
  2. caps_priv_check β€” sys/kern/kern_caps.c:311-341 β€” passes uid-0 through, delegates jail policy to prison_priv_check.
  3. prison_priv_check β€” sys/kern/kern_jail.c:839-905 β€” for jailed creds, SYSCAP_NOSYSCTL_WR falls in the "group 2 allowed" list (kern_jail.c:883-885) β†’ returns 0 (permitted).
  4. CTLFLAG_PRISON (sys/sys/sysctl.h:97, comment "Prisoned roots can fiddle") β€” tree-wide grep finds zero enforcement; only 3 nodes set it (kern_mib.c:242, if_fxp.c:507,511) and one handler (kern_hostname, kern_mib.c:214-235) implements jail semantics itself.

β‡’ jailed uid-0 passes every write gate for plain RW variables.

Reproduction (INVARIANTS guest, DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC)

Static writer jailwrite.c (no jail filesystem dependencies) executed via legacy jail(8) as uid 0 inside jtest-DF2738:

write kern.sugid_coredump=1   -> r=0 WROTE-HOST-GLOBAL
write kern.maxfiles=130112    -> r=0 WROTE-HOST-GLOBAL
write debug.sysctl=0          -> r=0 WROTE-HOST-GLOBAL
write kern.securelevel=-1     -> r=0 WROTE-HOST-GLOBAL
HOST AFTER: kern.sugid_coredump = 1   (was 0; mutated from inside the jail)

Two consecutive runs identical (4/4 writes). Host state restored each time. Unprivileged non-root writes remain EPERM (control test in the DF-2737 prober: write debug.sysctl -> EPERM for uid 1001).

Setup note: the setup (creating the jail) needs real root β€” that is the deployment model under test (hosting provider grants customers root inside jails); the attacker position is "uid 0 confined in a jail", which the kernel promises to contain.

Exploit ceiling

Arbitrary host-global kernel variable writes from a jailed root: host-wide DoS (kern.maxfiles/maxproc knobs), shared network-stack reconfiguration (net.*), kern.sugid_coredump=1 (host setuid core dumps β†’ host credential disclosure assist), kern.securelevel manipulation while host securelevel <= 0. Not a direct host uid=0 primitive; classed as a jail-confinement / integrity+availability break.

Fix validation

fix.diff: dispatcher jail gate + CTLFLAG_PRISON for kern.proc.args. - git apply --check: clean. - Applied inside the guest's /usr/src; both touched objects rebuilt with the stock kernel CFLAGS incl. -Werror: OK (build_fix.log); guest source then reverted to pristine (md5 verified equal to the audit tree). - Full kernel build + boot + in-jail re-test NOT performed (Medium severity, non-corruption; compile-verified 5-line gate whose shape mirrors the already-runtime-proven ANYBODY/caps gate) β†’ fix_status: inconclusive (compiled, not boot-validated).

Fix verification

inconclusive
baseline reproduced→ patch + rebuild →patched clean

fix.diff is git-apply-clean and both touched objects (kern_sysctl.o, kern_proc.o) compile with the stock kernel CFLAGS incl. -Werror inside the guest build env (build_fix.log). Full kernel build + boot + in-jail re-test was not run (Medium severity, non-corruption; the gate mirrors the already-runtime-proven caps/ANYBODY gate shape, so behavior post-patch is EPERM for jailed writes to non-PRISON nodes). Compile-verified only => inconclusive rather than fixed.

['findings/poc/DF-2738/build_fix.log (OBJ-BUILD-OK, -Werror, guest source reverted md5-verified)']
↓ fix.diffper-fix-DF-2738

Confirmed kernel references

Detail

Exploit chain

hosting scenario: attacker holds uid 0 inside a jail -> sysctlbyname("kern.sugid_coredump", NULL,NULL,&one,4) (or any plain CTLFLAG_RW variable) passes sysctl_root's write gate because prison_priv_check returns 0 for SYSCAP_NOSYSCTL_WR -> handler writes the host-global variable -> host state mutated across the jail boundary; e.g. kern.maxfiles/* denial-of-service for the whole host and other jails, or kern.sugid_coredump=1 to enable host-wide setuid core dumps.

Evidence (decisive lines)

["findings/poc/DF-2738/run.log and run.2.log: 'write kern.sugid_coredump=1 -> r=0 WROTE-HOST-GLOBAL' x4 inside jail; '=== HOST AFTER ===' shows kern.sugid_coredump = 1 (was 0)", "sys/kern/kern_jail.c:883-885: prison_priv_check returns 0 (allowed) for SYSCAP_NOSYSCTL_WR in jails ('group 2 allowed')", "sys/sys/sysctl.h:97: CTLFLAG_PRISON defined 'Prisoned roots can fiddle' but tree-wide grep shows zero enforcement (only kern_mib.c:242 and if_fxp.c:507/511 set it)", 'sys/kern/kern_mib.c:214-235: kern.hostname handler does its own jailed() redirect to prison->pr_host - the only node with real jail write semantics', 'findings/poc/DF-2737/run_user_prober.log control: unprivileged uid!=0 write to debug.sysctl -> EPERM (the caps gate itself works; it is jail policy that is open)']

PoC changes

authored fresh (no seed): static cc -static writer so the jail needs no dynamic loader or filesystem beyond the binary itself; initial dynamic-chroot attempt segfaulted (guest ld-elf.so.2 in minimal tree), replaced with -static.

Verified recommended fix

Deny jailed-credential writes to nodes lacking CTLFLAG_PRISON in sysctl_root (kern_sysctl.c:1446-1450) and flag kern.proc.args CTLFLAG_PRISON (it is the one legitimate jailed write, already curproc-checked) - see fix.diff

Verdict

Reproduced twice on the stock INVARIANTS guest (DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC): a statically linked writer executed as uid 0 inside a jail(8) successfully wrote four host-global sysctls (kern.sugid_coredump, kern.maxfiles, debug.sysctl, kern.securelevel) and the host observed kern.sugid_coredump flip 0->1. Root cause is purely in the write gate chain: sysctl_root (sys/kern/kern_sysctl.c:1438-1450) never consults CTLFLAG_PRISON, whose header comment (sys/sys/sysctl.h:97 'Prisoned roots can fiddle') documents exactly the missing semantics; caps_priv_check delegates to prison_priv_check which deliberately allows SYSCAP_NOSYSCTL_WR inside jails (sys/kern/kern_jail.c:883-885). Only kern.hostname is safe because its handler implements jail semantics itself (kern_mib.c:214-235). Impact ceiling is arbitrary host-global kernel-state mutation from a confined root: host-wide DoS (kern.maxfiles et al.), shared network-stack reconfiguration, kern.sugid_coredump=1 (host credential-disclosure assist), kern.securelevel raising while host securelevel <= 0. Classed dos (closest enum); it is a jail-confinement integrity/availability break, not a direct host uid=0.