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

devfs rule ioctls have no privilege/jail check: jailed root rewrites host-wide devfs visibility and permissions (jail escape)

Field Value
ID DF-NNNN
Status new
Severity
CVSS 3.1
CWE
File
Lines -
Area
Confidence
Discovered
Pass <1 = GLM 5.2 first pass / 2 = GLM 5.3 second pass>
Bucket
Reported
Known CVE
CVE match

Summary

<2-4 sentences. What is broken, where, and the security consequence.>

Root cause

file:line citations. Name the exact variable, length, check, or lock that is wrong.>

Threat model & preconditions

  • Attacker position:
  • Privileges gained or impact:
  • Required config or capabilities: vfs.mount.<fs> allowed, needs a specific device node>
  • Reachability:

Proof of concept

PoC source: findings/poc/DF-NNNN/<file>

Build & run

<exact commands to compile and run the PoC on a DragonFlyBSD host/VM>

Expected output

<what a successful trigger looks like β€” panic dump, leaked bytes, uid=0, etc.>

Impact

--- a/sys/<path>
+++ b/sys/<path>
@@ -<l>,<n> +<l>,<n> @@
 <context>
-<vulnerable line>
+<fixed line>
 <context>

References

Timeline

  • Discovered during automated audit.
  • Reported to DragonFlyBSD security contact.

Verification (latest)

status: reproduced | impact: uid0_privesc | confidence: certain

Fix status: fixed β€” validated fix.diff lives at findings/poc/DF-3197/fix.diff

devfs_dev_ioctl() (sys/vfs/devfs/devfs_rules.c:419) has no priv_check() and no jail check; the only gate is the 0600 root:wheel /dev/devfs node, which jailed uid 0 opens. Rule state is the global devfs_rule_list and APPLY/RESET walk the global devfs_mnt_list, so jailed root unhid host-hidden devices in its own jail devfs (opened /dev/zero from inside the jail) and rewrote the HOST's /dev/console from 0600 to 0666 via a PERM rule with mntpoint '/dev'. Impact is a jail escape (jailed root -> host device access / host permission weakening) whenever the jail's devfs shows the devfs control node (default on this guest: no /etc/devfs.rules shipped). Impact: jail escape (jailed root defeats the host's devfs ruleset and rewrites host device permissions; full host uid=0 follows standardly from the unhide+perm primitives, e.g. raw-disk access, but was not executed to avoid destroying the guest disk).

Evidence pack: findings/poc/DF-3197/ (README.md, VERDICT.md, sources, full logs, fix.diff, manifest.json).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3197 Β· 10 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 2.0 KB ↓ raw
build.sh β€” 93 B view raw
env.txt β€” 415 B view raw
fix.diff β€” 8.6 KB view raw
jail_show.c β€” 8.6 KB view raw
run.fixed.log β€” 539 B view raw
run.log β€” 684 B view raw
run.sh β€” 92 B view raw
verdict.json β€” 3.1 KB view raw

DF-3197 β€” devfs rule ioctls reachable from jail: host-wide devfs ruleset manipulation (jail escape)

Where

  • sys/vfs/devfs/devfs_rules.c:419 devfs_dev_ioctl() β€” no priv_check(), no jail check; only gate is the 0600 root:wheel /dev/devfs node (devfs_rules.c:470), which jailed uid 0 passes.
  • Rules are stored in the GLOBAL devfs_rule_list (devfs_rules.c:67); DEVFS_RULE_APPLY/DEVFS_RULE_RESET walk the GLOBAL devfs_mnt_list (devfs_core.c:1912-1931) β€” every devfs mount on the system, including the host's /dev and every other jail's.

What a jailed root can do (all demonstrated on the guest)

  1. Open /dev/devfs inside the jail (control node visible β‡’ any jail devfs not explicitly hiding it β€” this guest ships no /etc/devfs.rules, so a plain jail devfs exposes it; the same holds for custom rulesets that forget to hide devfs).
  2. DEVFS_RULE_ADD a SHOW rule β†’ DEVFS_RULE_APPLY "*" β†’ un-hides nodes the HOST hid in the jail's devfs (defeats the jail's devfs ruleset).
  3. DEVFS_RULE_ADD a PERM rule with mntpoint "/dev" β†’ DEVFS_RULE_APPLY "/dev" β†’ rewrites permissions on the HOST's /dev (demonstrated: /dev/console 0600 β†’ 0666).
  4. Same primitive on any other jail's mounts.

Files

  • jail_show.c β€” PoC (host phase mounts jail devfs + hides mem/zero; jail phase un-hides + opens)
  • build.sh, run.sh
  • run.log β€” decisive output (JAIL-ESCAPE: unhidden + OPENED /dev/zero from inside jail, host /dev/console 0666)
  • fix.diff β€” consolidated fix (jailed() gate + the other five findings)
  • verdict.json, manifest.json

Build / run

cc -static -O -o jail_show jail_show.c
./jail_show        # as root on the host; expect JAIL-ESCAPE lines

Notes

  • /dev/mem was un-hidden and stat-able from the jail; opening it returns EPERM only because memio(4) itself checks jailed() β€” a driver-level defense devfs does not provide. Any device whose driver lacks that check is fully reachable once shown (demonstrated with /dev/zero).
  • The rule engine also never bumps mnt_namecache_gen on SHOW/HIDE (devfs_core.c:277 bumps only on node link); stale namecache entries mask flag changes until the generation is bumped β€” the PoC links one pid-unique alias to flush it.
VERDICT.md
↓ download raw

DF-3197 β€” verdict

REPRODUCED on a pristine guest (see README.md and run.log): 1. Host mounted devfs at /jail/dev and hid mem + zero via the rule ioctl (the standard way jail devfs rulesets are enforced). 2. Jailed uid-0 process (jail(2) v1, path=/jail) OPENED /dev/devfs β€” the only gate is 0600 root:wheel, which jailed uid 0 passes. devfs_dev_ioctl() (devfs_rules.c:419) has no priv_check()/jail check whatsoever. 3. A jailed-issued SHOW rule + APPLY "*" un-hid /dev/mem inside the jail. 4. JAIL-ESCAPE marker: un-hid and OPENED /dev/zero from inside the jail β€” the host's devfs ruleset was fully defeated from within the jail. 5. Cross-mount write: a jailed-issued PERM rule (mntpoint "/dev") changed the HOST's /dev/console from 0600 to 0666. 6. /dev/mem un-hid + stat-able from the jail; only memio(4)'s own jailed() check (EPERM) stopped the read β€” a driver-level defense devfs lacks.

Impact: jail escape. Any jail whose devfs does not hide the devfs control node (this guest ships no /etc/devfs.rules, so a plain jail devfs exposes it; same for custom rulesets that forget it) lets jailed root un-hide arbitrary devices in its own mount and rewrite permissions/visibility on the host's and every other jail's devfs mounts. Devices whose drivers lack their own jailed() checks are then openable (demonstrated with /dev/zero; /dev/mem is stopped only by memio).

Privilege ceiling note: on a non-jail system the ioctls are root-only (0600 node) β€” the security boundary actually broken here is the jail boundary (jailed root != host root).

Fix validated: fixed kernel returns EPERM to the jailed caller (fix.diff, jailed(curthread->td_ucred) gate in devfs_dev_ioctl); run.fixed.log.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

jailed ioctls return EPERM (run.fixed.log); host /dev/console untouched; jail child cleanly fails

['DF-3197/run.fixed.log']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 8 10:25:07 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

host mounts devfs for jail + hides mem/zero via rules -> jail(2) as uid 0 -> open /dev/devfs (0600, uid0 passes) -> DEVFS_RULE_ADD SHOW/PERM + APPLY '*' or '/dev' -> hidden nodes reappear in the jail (opened /dev/zero); host /dev/console becomes 0666; any device whose driver lacks its own jailed() check is reachable

Evidence (decisive lines)

["run.log: 'JAIL-ESCAPE: unhidden + OPENED /dev/zero from inside jail'", "run.log: '[host] /dev/console mode post-jail: 0666' (pre: 0600)", 'run.fixed.log: jailed ioctl now EPERM, host untouched']

PoC changes

initial PoC forgot mkdir /jail/dev; needed setvbuf before fork; SHOW does not bump mnt_namecache_gen so negative/positive namecache entries had to be flushed by linking a pid-unique alias; rules from earlier runs had to be CLEARed for idempotency

Verified recommended fix

reject jailed callers in devfs_dev_ioctl(): if (jailed(curthread->td_ucred)) return EPERM;

Verdict

devfs_dev_ioctl() (sys/vfs/devfs/devfs_rules.c:419) has no priv_check() and no jail check; the only gate is the 0600 root:wheel /dev/devfs node, which jailed uid 0 opens. Rule state is the global devfs_rule_list and APPLY/RESET walk the global devfs_mnt_list, so jailed root unhid host-hidden devices in its own jail devfs (opened /dev/zero from inside the jail) and rewrote the HOST's /dev/console from 0600 to 0666 via a PERM rule with mntpoint '/dev'. Impact is a jail escape (jailed root -> host device access / host permission weakening) whenever the jail's devfs shows the devfs control node (default on this guest: no /etc/devfs.rules shipped). Impact: jail escape (jailed root defeats the host's devfs ruleset and rewrites host device permissions; full host uid=0 follows standardly from the unhide+perm primitives, e.g. raw-disk access, but was not executed to avoid destroying the guest disk).