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).
Recommended fix
--- 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)
PoC verification
Evidence pack
findings/poc/DF-2738 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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)
sysctl_rootwrite gate β sys/kern/kern_sysctl.c:1438-1450: requires CTLFLAG_WR, honors CTLFLAG_SECURE vs securelevel, thencaps_priv_check(td->td_ucred, SYSCAP_NOSYSCTL_WR). No prison check.caps_priv_checkβ sys/kern/kern_caps.c:311-341 β passes uid-0 through, delegates jail policy toprison_priv_check.prison_priv_checkβ sys/kern/kern_jail.c:839-905 β for jailed creds,SYSCAP_NOSYSCTL_WRfalls in the "group 2 allowed" list (kern_jail.c:883-885) β returns 0 (permitted).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
inconclusivefix.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)']
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.
No comments yet.