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
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
Recommended fix
--- 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)
PoC verification
Evidence pack
findings/poc/DF-3197 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:419devfs_dev_ioctl()β nopriv_check(), no jail check; only gate is the 0600 root:wheel/dev/devfsnode (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_RESETwalk the GLOBALdevfs_mnt_list(devfs_core.c:1912-1931) β every devfs mount on the system, including the host's/devand every other jail's.
What a jailed root can do (all demonstrated on the guest)
- Open
/dev/devfsinside 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 hidedevfs). DEVFS_RULE_ADDaSHOWrule βDEVFS_RULE_APPLY "*"β un-hides nodes the HOST hid in the jail's devfs (defeats the jail's devfs ruleset).DEVFS_RULE_ADDaPERMrule withmntpoint "/dev"βDEVFS_RULE_APPLY "/dev"β rewrites permissions on the HOST's/dev(demonstrated:/dev/console0600 β 0666).- Same primitive on any other jail's mounts.
Files
jail_show.cβ PoC (host phase mounts jail devfs + hidesmem/zero; jail phase un-hides + opens)build.sh,run.shrun.logβ decisive output (JAIL-ESCAPE: unhidden + OPENED /dev/zero from inside jail, host/dev/console0666)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/memwas un-hidden and stat-able from the jail; opening it returns EPERM only because memio(4) itself checksjailed()β 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_genon SHOW/HIDE (devfs_core.c:277bumps 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.
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
fixedjailed ioctls return EPERM (run.fixed.log); host /dev/console untouched; jail child cleanly fails
['DF-3197/run.fixed.log']
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).
No comments yet.