caps_priv_check corrupts cap argument before prison_priv_check: bypasses per-cap jail policy (raw sockets + mounts in jail)
| Field | Value |
|---|---|
| ID | DF-0165 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N |
| CWE | CWE-863 Incorrect Authorization |
| File | sys/kern/kern_caps.c |
| Lines | 333-340 |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-06-30 |
| Reported | pending |
Summary
caps_priv_check() mutates its cap argument in the group-handling
block (:335), reducing it from the specific capability (e.g.
SYSCAP_NONET_RAW = 0x61) to the group master number (e.g. 6 =
SYSCAP_NONET). The mutated value is then passed to
prison_priv_check() (:340), which matches the group-master case
(case SYSCAP_NONET: return 0 = "allowed in jail") instead of the
specific-capability case (case SYSCAP_NONET_RAW: which checks
PRISON_CAP_NET_RAW_SOCKETS). This allows jailed root to create raw
sockets and mount restricted filesystem types even when the
corresponding jail policy toggle is disabled.
Root cause
In caps_priv_check() (sys/kern/kern_caps.c:333-340):
res = caps_check_cred(cred, cap);
if (cap & __SYSCAP_GROUP_MASK) {
cap = (cap & __SYSCAP_GROUP_MASK) >> __SYSCAP_GROUP_SHIFT; // :335
res |= caps_check_cred(cred, cap);
}
if (res & __SYSCAP_SELF)
return EPERM;
return (prison_priv_check(cred, cap)); // :340 β cap is now WRONG
The capability encoding:
- __SYSCAP_GROUP_MASK = 0x000000F0 (bits 4-7)
- __SYSCAP_GROUP_SHIFT = 4
- SYSCAP_NONET = 6 (group-0 master)
- SYSCAP_NONET_RAW = 0x61 (group 6 | index 1)
When cap = SYSCAP_NONET_RAW (0x61):
- Line 335: cap = (0x61 & 0xF0) >> 4 = 0x60 >> 4 = 6
- 6 is SYSCAP_NONET β the group master
In prison_priv_check() (sys/kern/kern_jail.c):
case SYSCAP_NONET: /* line 865 */
return (0); /* allowed in jail */
...
case SYSCAP_NONET_RAW: /* line 919 β NEVER REACHED */
if (pr->pr_caps & PRISON_CAP_NET_RAW_SOCKETS)
return (0);
return (EPERM);
The case SYSCAP_NONET_RAW at :919 is dead code on the
caps_priv_check() path β prison_priv_check always receives 6
(SYSCAP_NONET), not 0x61 (SYSCAP_NONET_RAW).
The same bypass applies to all NOMOUNT_* capabilities:
SYSCAP_NOMOUNT_NULLFS/DEVFS/TMPFS/PROCFS/FUSE are reduced to
SYSCAP_NOMOUNT (10) which hits case SYSCAP_NOMOUNT: return 0
(:872).
Threat model & preconditions
- Attacker position: Jailed root (uid 0 inside a jail).
- Impact:
- Create raw IP/IPv6 sockets despite
jail.net_raw_sockets=0β packet sniffing, spoofing, attacks on other tenants. - Mount nullfs/devfs/tmpfs/procfs/fuse despite corresponding jail toggle being off β host filesystem access, device node creation.
- Required config: Default kernel with jail support. The jail must have the relevant capability toggles disabled (the default).
- Reachability:
socket(AF_INET, SOCK_RAW, ...)from jailed root;mount -t nullfs ...from jailed root.
Proof of concept
PoC source: findings/poc/DF-0165/
Build & run
# In a jail with net_raw_sockets=0: # From jailed root: socket(AF_INET, SOCK_RAW, IPPROTO_RAW); # Returns 0 (success) instead of EPERM # In a jail with vfs_mount_nullfs=0: # From jailed root: mount -t nullfs /host/path /inside/jail # Succeeds instead of EPERM
Expected output
# Raw socket: succeeds (should fail with EPERM) # Mount: succeeds (should fail with EPERM)
Impact
Jail containment is broken for all capabilities whose jail policy is conditional/EPERM while their group master policy is "allowed". This affects every DragonFlyBSD deployment that uses jails for tenant isolation. Raw socket access allows packet injection/sniffing; mount access allows host filesystem traversal. This is a cross-tenant attack vector in multi-tenant hosting environments.
Recommended fix
Do not mutate the cap variable used for the jail lookup. Use a
separate local for the group-master bitmask test:
--- a/sys/kern/kern_caps.c
+++ b/sys/kern/kern_caps.c
@@ -331,9 +331,10 @@
res = caps_check_cred(cred, cap);
if (cap & __SYSCAP_GROUP_MASK) {
- cap = (cap & __SYSCAP_GROUP_MASK) >> __SYSCAP_GROUP_SHIFT;
- res |= caps_check_cred(cred, cap);
+ int gcap = (cap & __SYSCAP_GROUP_MASK) >> __SYSCAP_GROUP_SHIFT;
+ res |= caps_check_cred(cred, gcap);
}
if (res & __SYSCAP_SELF)
return EPERM;
- return (prison_priv_check(cred, cap));
+ return (prison_priv_check(cred, cap)); /* pass ORIGINAL cap */
}
References
sys/kern/kern_jail.c:865-866βcase SYSCAP_NONET: return 0sys/kern/kern_jail.c:919-927βcase SYSCAP_NONET_RAW(dead code on caps_priv_check path)sys/kern/kern_jail.c:951-975βcase SYSCAP_NOMOUNT_*(dead code)sys/netinet/raw_ip.c:473β caller passes SYSCAP_NONET_RAWsys/kern/vfs_syscalls.c:152-157β caller passes SYSCAP_NOMOUNT_*
Timeline
- 2026-06-30 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0165 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| bypass.c | trigger-source | self-contained jail-create + gated-action driver; proves cap-corruption bypass | 5.2 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o bypass bypass.c | 150 B | view raw |
| run.sh | run-script | echoes jail default-policy sysctls then runs ./bypass | 757 B | view raw |
| build.log | build-log | final successful PoC build, full output | 69 B | view raw |
| run.log | run-log | decisive baseline run on #0: 5 bypasses observed | 1.0 KB | view raw |
| run.2.log | run-log | repeat baseline run for reproducibility | 733 B | view raw |
| run.3.log | run-log | third baseline run for reproducibility | 733 B | view raw |
| fix_baseline.log | fix-baseline-log | PoC on UNPATCHED #0 kernel: 5 cap-gated actions bypass jail policy | 1016 B | view raw |
| fix.diff | suggested-fix | git-apply-able fix: introduce gcap local in caps_priv_check, pass original cap to prison_priv_check | 460 B | view raw |
| fix_notes.md | fix-notes | post-verification fix rationale and correctness argument | 3.1 KB | β raw |
| fix_build.log | fix-build-log | full nativekernel build of single-fix #1 kernel (NK_DONE rc=0) | 5.6 MB | β download |
| fix_run.log | fix-run-log | PoC on PATCHED #1 kernel: all 5 actions EPERM (policy enforced) | 1.0 KB | view raw |
| env.txt | environment | uname for both #0 and #1 kernels, cc version, jail default policy, patched kernel sha256 | 1.2 KB | view raw |
| VERDICT.md | verdict | full narrative + line-by-line kernel trace + recommended fix + fix-validation section | 10.4 KB | β raw |
| README.md | readme | what this pack is and how to reproduce | 2.5 KB | β raw |
| manifest.json | manifest | this file | 4.5 KB | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0165 β PoC evidence pack
What this is
Demonstrates that caps_priv_check() in sys/kern/kern_caps.c:333-340
mutates its cap argument from the specific capability (e.g.
SYSCAP_NONET_RAW = 0x61) to its group-master number (SYSCAP_NONET = 6)
before forwarding it to prison_priv_check(), which has
case SYSCAP_NONET: return 0 and case SYSCAP_NOMOUNT: return 0. The
per-capability switch arms that actually consult the jail policy flags
are dead code on this path. Result: a jailed root can do raw socket
creation and tmpfs/nullfs/devfs/procfs mounts that the jail policy
explicitly forbids.
See VERDICT.md for the full mechanism walkthrough and the line-by-line
trace.
Reproduce
./build.sh # cc -O2 -Wall -o bypass bypass.c
./run.sh # creates jail with default policy, tries gated actions
Must be run as root on the guest (the test creates+enters a jail).
run.sh first echoes the jail default-policy sysctls (proving they are
all 0 / restrictive), then runs ./bypass.
Expected output
jail() ok: jid=N (now jailed as uid=0)
=== DF-0165 demo: cap-gated actions inside jail ===
(jail default policy: allow_raw_sockets=0,
vfs_mount_{nullfs,tmpfs,devfs,procfs}=0 -> all should EPERM)
socket(AF_INET, SOCK_RAW, IPPROTO_RAW) [SYSCAP_NONET_RAW]
-> OK fd=3 *** BYPASS ***
mount("tmpfs", ...) [SYSCAP_NOMOUNT_TMPFS] -> OK *** BYPASS ***
mount("null", ...) [SYSCAP_NOMOUNT_NULLFS] -> OK *** BYPASS ***
mount("devfs", ...) [SYSCAP_NOMOUNT_DEVFS] -> OK *** BYPASS ***
mount("procfs", ...) [SYSCAP_NOMOUNT_PROCFS] -> OK *** BYPASS ***
=== end: 5 cap-gated action(s) bypassed jail policy ===
On a fixed kernel every action returns EPERM instead of OK.
Why the PoC was rewritten
The original (per-finding) PoC snippet was a 4-line shell pseudocode
("from jailed root, run mount/socket"). I implemented it as a real C
program (bypass.c) that:
- creates the jail itself (no separate
jail(8)setup needed), - attaches via
jail(2)(which auto-attaches perkern_jail_attachatsys/kern/kern_jail.c:227), - drives each gated action and reports
OK / EPERMper action.
Notable gotcha worth recording: the kernel's nullfs fstype is "null",
not "nullfs" β get_fscap()'s strncmp("null", fsname, 5) only matches
the bare name. Using mount("nullfs", ...) makes the syscall hit a
different (default) cap and fail for an unrelated reason; using
mount("null", ...) exercises the actual SYSCAP_NOMOUNT_NULLFS path
and demonstrates the bypass cleanly.
DF-0165 β caps_priv_check cap-corruption -> jail policy bypass
Verdict: REPRODUCED (5 distinct cap-gated actions bypass jail policy)
Inside a jail created with the default restrictive policy (allow_raw_sockets=0,
vfs_mount_{nullfs,tmpfs,devfs,procfs}=0), a jailed root (uid 0) successfully:
- opens a raw IPv4 socket (
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) βSYSCAP_NONET_RAW - mounts tmpfs β
SYSCAP_NOMOUNT_TMPFS - mounts nullfs β
SYSCAP_NOMOUNT_NULLFS(using kernel fstype "null") - mounts devfs β
SYSCAP_NOMOUNT_DEVFS - mounts procfs β
SYSCAP_NOMOUNT_PROCFS
On a fixed kernel, each of these returns EPERM because the per-capability
jail policy flag is clear. On this build they all succeed, proving the
bypass.
Mechanism (root cause confirmed line-by-line)
In sys/kern/kern_caps.c:333-340:
res = caps_check_cred(cred, cap); /* :333 */
if (cap & __SYSCAP_GROUP_MASK) { /* :334 */
cap = (cap & __SYSCAP_GROUP_MASK) >> __SYSCAP_GROUP_SHIFT; /* :335 -- MUTATES cap */
res |= caps_check_cred(cred, cap); /* :336 */
}
if (res & __SYSCAP_SELF)
return EPERM;
return (prison_priv_check(cred, cap)); /* :340 -- passes MUTATED cap */
For a per-capability value like SYSCAP_NONET_RAW = __SYSCAP_GROUP_6 | 1 = 0x61:
:334cap & __SYSCAP_GROUP_MASK=0x61 & 0xF0=0x60(truthy):335cap = (0x61 & 0xF0) >> 4=0x6(=SYSCAP_NONET, the group master):340prison_priv_check(cred, 0x6)β the specific cap (0x61) is never sent
In prison_priv_check (sys/kern/kern_jail.c:854-978):
case SYSCAP_NONET: /* :865-866 group master: ALLOWED */
return 0;
...
case SYSCAP_NOMOUNT: /* :872,878 group master: ALLOWED */
return 0;
...
case SYSCAP_NONET_RAW: /* :919-927 per-capability check -- DEAD on this path */
if (PRISON_CAP_ISSET(pr->pr_caps, PRISON_CAP_NET_RAW_SOCKETS)) return 0;
return EPERM;
The case SYSCAP_NONET_RAW and the case SYSCAP_NOMOUNT_* branches are
dead code on the caps_priv_check() path β prison_priv_check always
receives the group-master number and matches the unconditional return 0
case, so the per-capability PRISON_CAP_* flag is never consulted.
Encoding reference (sys/sys/caps.h)
__SYSCAP_GROUP_MASK = 0x000000F0 (bits 4..7) __SYSCAP_GROUP_SHIFT = 4 __SYSCAP_XFLAGS = 0x7FFF0000 (e.g. __SYSCAP_NULLCRED, NOROOTTEST) Group-0 master caps (these match the "ALLOWED in jail" cases): SYSCAP_NONET = 0x06 -> prison_priv_check returns 0 (allowed) SYSCAP_NOMOUNT = 0x0A -> prison_priv_check returns 0 (allowed) Per-capability values (their *specific* switch arms are the real policy): SYSCAP_NONET_RAW = 0x61 -> corrupted to 0x6 -> matches SYSCAP_NONET SYSCAP_NOMOUNT_NULLFS = 0xA0 -> corrupted to 0xA -> matches SYSCAP_NOMOUNT SYSCAP_NOMOUNT_DEVFS = 0xA1 -> corrupted to 0xA -> matches SYSCAP_NOMOUNT SYSCAP_NOMOUNT_TMPFS = 0xA2 -> corrupted to 0xA -> matches SYSCAP_NOMOUNT SYSCAP_NOMOUNT_FUSE = 0xA4 -> corrupted to 0xA -> matches SYSCAP_NOMOUNT SYSCAP_NOMOUNT_PROCFS = 0xA5 -> corrupted to 0xA -> matches SYSCAP_NOMOUNT
Caller chain (where the bypass matters)
-
sys/netinet/raw_ip.c:473βrip_attachcallscaps_priv_check(ai->p_ucred, SYSCAP_NONET_RAW | __SYSCAP_NULLCRED). Withcap = 0x00020061, the corruption still reduces it to6:0x00020061 & 0xF0 = 0x60,>> 4 = 6. -
sys/kern/vfs_syscalls.c:152-157βsys_mountcallscaps_priv_check_td(td, priv)wherepriv = get_fscap(fstypename).get_fscap()returns the specificSYSCAP_NOMOUNT_*value, which is corrupted toSYSCAP_NOMOUNT.
Threat model
- Attacker position: jailed root (uid 0 inside a jail).
- What the attacker gets:
- Raw IP sockets despite
jail.defaults.allow_raw_sockets=0. Enables packet sniffing, IP-spoofed packet injection, ICMP attacks against other tenants / host. - Mount nullfs / tmpfs / devfs / procfs inside the jail despite
jail.defaults.vfs_mount_*=0. Mounting devfs exposes device nodes; mounting nullfs over a host-visible path bypasses filesystem-level isolation; mounting procfs exposes host process metadata. - Preconditions: default DragonFlyBSD jail (no special config required).
- Reachability: trivial β
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)andmount("tmpfs", target, 0, NULL)from jailed root.
Demonstration
---- jail default policy (should all be 0): ----
jail.defaults.allow_raw_sockets: 0
jail.defaults.vfs_mount_nullfs: 0
jail.defaults.vfs_mount_tmpfs: 0
jail.defaults.vfs_mount_devfs: 0
jail.defaults.vfs_mount_procfs: 0
---- running bypass as root (will create + enter jail): ----
jail() ok: jid=11 (now jailed as uid=0)
=== DF-0165 demo: cap-gated actions inside jail ===
(jail default policy: allow_raw_sockets=0,
vfs_mount_{nullfs,tmpfs,devfs,procfs}=0 -> all should EPERM)
socket(AF_INET, SOCK_RAW, IPPROTO_RAW) [SYSCAP_NONET_RAW]
-> OK fd=3 *** BYPASS ***
mount("tmpfs", /tmp/df0165-mnt-tmpfs) [SYSCAP_NOMOUNT_TMPFS]
-> OK *** BYPASS ***
mount("null", /tmp/df0165-mnt-nullfs) [SYSCAP_NOMOUNT_NULLFS]
-> OK *** BYPASS ***
mount("devfs", /tmp/df0165-mnt-devfs) [SYSCAP_NOMOUNT_DEVFS]
-> OK *** BYPASS ***
mount("procfs", /tmp/df0165-mnt-procfs) [SYSCAP_NOMOUNT_PROCFS]
-> OK *** BYPASS ***
=== end: 5 cap-gated action(s) bypassed jail policy ===
Reproduced 3 times in a row (see run.log, run.2.log, run.3.log);
every run yields the same 5 bypasses. The only inter-run difference is
the jid= value, which is just an incrementing jail counter.
Notes / minor adjacent issues (not part of DF-0165)
-
get_fscap()insys/kern/vfs_syscalls.c:5386matchesstrncmp("null", fsname, 5), which does NOT match the user-visible fstype"nullfs". The kernel fstype for nullfs is"null"(its vfsconfvfc_name). Anyone callingmount("nullfs", ...)falls through to theSYSCAP_RESTRICTEDROOTdefault β a separate latent surprise that the PoC works around by using"null". -
The same corruption affects
SYSCAP_NONET_BT_RAW,SYSCAP_NONET_ROUTE,SYSCAP_NONET_IFCONFIG, etc., but those callers either route through a different cap or the action is independently gated. The five actions demonstrated here are the directly observable wins.
Recommended fix (matches the finding's diff)
Don't mutate the cap variable used for the jail lookup. Use a separate
local for the group-master test:
--- a/sys/kern/kern_caps.c
+++ b/sys/kern/kern_caps.c
@@ -331,9 +331,10 @@
res = caps_check_cred(cred, cap);
if (cap & __SYSCAP_GROUP_MASK) {
- cap = (cap & __SYSCAP_GROUP_MASK) >> __SYSCAP_GROUP_SHIFT;
- res |= caps_check_cred(cred, cap);
+ int gcap = (cap & __SYSCAP_GROUP_MASK) >> __SYSCAP_GROUP_SHIFT;
+ res |= caps_check_cred(cred, gcap);
}
if (res & __SYSCAP_SELF)
return EPERM;
- return (prison_priv_check(cred, cap));
+ return (prison_priv_check(cred, cap)); /* ORIGINAL specific cap */
}
After the fix, the PoC outputs EPERM for every action when the policy flag
is clear (policy honored), and OK when the flag is set (positive policy
honored) β the per-capability switch arm at kern_jail.c:919 is now actually
reached.
Fix validation (built + booted single-fix kernel)
A single-fix kernel was built from the audit with-src baseline with ONLY
fix.diff applied, installed, and booted, then the SAME PoC was re-run
against both kernels for a clean before/after comparison.
Kernel identity
| Kernel | kern.version | sha256 (/boot/kernel/kernel) |
|---|---|---|
| Unpatched (#0) | DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 |
(audit baseline) |
| Single-fix (#1) | DragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 11:16:47 UTC 2026 |
9213f61534131ee4720f0eeaa04a588c64b881e94a63fb6185671cebf9fde92b |
Build: cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC
β === NK_DONE rc=0 === (no error: / Stop in). Full log in fix_build.log.
Before (unpatched #0) β BUG PRESENT (fix_baseline.log)
socket(AF_INET, SOCK_RAW, IPPROTO_RAW) [SYSCAP_NONET_RAW]
-> OK fd=3 *** BYPASS ***
mount("tmpfs", ...) [SYSCAP_NOMOUNT_TMPFS] -> OK *** BYPASS ***
mount("null", ...) [SYSCAP_NOMOUNT_NULLFS] -> OK *** BYPASS ***
mount("devfs", ...) [SYSCAP_NOMOUNT_DEVFS] -> OK *** BYPASS ***
mount("procfs",...) [SYSCAP_NOMOUNT_PROCFS] -> OK *** BYPASS ***
=== end: 5 cap-gated action(s) bypassed jail policy ===
After (single-fix #1) β BUG FIXED (fix_run.log)
socket(AF_INET, SOCK_RAW, IPPROTO_RAW) -> Operation not permitted (errno=1) -- correctly denied
mount("tmpfs", ...) [SYSCAP_NOMOUNT_TMPFS] -> Operation not permitted (errno=1)
mount("null", ...) [SYSCAP_NOMOUNT_NULLFS] -> Operation not permitted (errno=1)
mount("devfs", ...) [SYSCAP_NOMOUNT_DEVFS] -> Operation not permitted (errno=1)
mount("procfs",...) [SYSCAP_NOMOUNT_PROCFS] -> Operation not permitted (errno=1)
=== end: 0 cap-gated action(s) bypassed jail policy ===
Deterministic across 3 consecutive runs on the patched kernel.
Positive-policy check (per-cap arm actually reached, not blanket-deny)
With jail.defaults.allow_raw_sockets=1 on the patched kernel, the raw
socket is correctly ALLOWED (1 bypass in the PoC's accounting, because the
policy now permits it) β proving prison_priv_check's
case SYSCAP_NONET_RAW: switch arm at kern_jail.c:919 is now reached and
honored in both directions, rather than being dead code. The four mount
caps stay denied because their flags remain clear.
No regression (non-jailed behavior unchanged)
non-jail raw socket: OK (fd=3 errno=0) non-jail tmpfs mount: OK (rc=0 errno=0)
Outside a jail prison_priv_check returns 0 immediately at
sys/kern/kern_jail.c:851-852, so the specific-vs-group-master distinction
is immaterial there β verified by the regression check above.
Fix classification: fixed
Bad behavior (5 bypasses) is present on the unpatched #0 baseline and
gone on the single-fix #1 kernel (0 bypasses, all EPERM), with the
positive-policy and non-jailed paths still working. The fix closes the bug.
DF-0165 β fix notes (authored post-verification)
What the fix changes
Single file: sys/kern/kern_caps.c, inside caps_priv_check() (lines 333-340).
The bug: the function mutates its cap argument in the group-handling block
(line 335), reducing a specific capability (e.g. SYSCAP_NONET_RAW = 0x61)
to its group-master number (e.g. SYSCAP_NONET = 6). The mutated value is
then forwarded to prison_priv_check() at line 340, where the group-master
case SYSCAP_NONET: return 0 / case SYSCAP_NOMOUNT: return (0)
unconditionally allow the action β bypassing the per-capability jail policy
switch arms (case SYSCAP_NONET_RAW, case SYSCAP_NOMOUNT_*) that actually
consult the PRISON_CAP_* flags.
The fix
Do not reuse cap for the group-master bitmask test. Introduce a block-local
gcap for the caps_check_cred() group-master self-restriction test, and
leave cap holding the original specific capability for the
prison_priv_check() call:
res = caps_check_cred(cred, cap);
if (cap & __SYSCAP_GROUP_MASK) {
int gcap = (cap & __SYSCAP_GROUP_MASK) >> __SYSCAP_GROUP_SHIFT;
res |= caps_check_cred(cred, gcap);
}
...
return (prison_priv_check(cred, cap)); /* now receives ORIGINAL specific cap */
Why this is correct and minimal
caps_check_cred(cred, gcap)computes the same value the old code did (the old line 335-336 produced exactly(group-master)then calledcaps_check_credwith it). The__SYSCAP_SELFself-restriction semantics are therefore unchanged.prison_priv_check()itself strips__SYSCAP_XFLAGSbefore switching (switch (cap & ~__SYSCAP_XFLAGS)atsys/kern/kern_jail.c:854), so forwarding the original cap (which may carry__SYSCAP_NULLCREDetc., as therip_attachcaller does) is the intended contract.- Non-jailed callers are unaffected:
prison_priv_check()returns 0 immediately for non-jailed creds (kern_jail.c:851-852), so passing the specific cap vs. the group-master number is immaterial outside a jail. - Group-master caps passed directly (e.g.
cap = SYSCAP_NONET = 6, for whichcap & __SYSCAP_GROUP_MASK == 0so theifbody is skipped) keep exactly their old behavior βprison_priv_check(cred, 6)still hitscase SYSCAP_NONET: return 0.
Only the specific caps whose per-capability jail policy is conditional/EPERM
while their group master returns 0 (SYSCAP_NONET_RAW, the
SYSCAP_NOMOUNT_{NULLFS,DEVFS,TMPFS,PROCFS,FUSE} set) change behavior β and
they change from "incorrectly allowed" to "correctly policy-gated", which is
exactly the bug being closed.
Validation
git apply --check findings/poc/DF-0165/fix.diffβ clean.patch --dry-run -p1 < findings/poc/DF-0165/fix.diffβ clean.- Generated against the read-only
sys/tree;sys/was not modified.
Relation to the finding markdown proposal
Matches the finding markdown's ## Recommended fix proposal. The only
deviation: this diff drops the comment-only edit to the prison_priv_check
return line (unnecessary churn) β the cap variable passed there is already
the original specific cap once the mutation is removed.
Fix verification
fixedVALIDATED. PoC bypassed jail policy 5/5 on unpatched #0 (raw socket + tmpfs/nullfs/devfs/procfs mounts all OK inside a default-policy jail). On the single-fix #1 kernel (only fix.diff applied, NK_DONE rc=0) the SAME PoC yields 0/5 bypasses β every action returns EPERM (Operation not permitted). Deterministic across 3 runs. Positive-policy check (jail.defaults.allow_raw_sockets=1) correctly re-permits the raw socket on #1, proving prison_priv_check's case SYSCAP_NONET_RAW arm is now reached and honored in both directions. No regression: non-jailed raw socket + tmpfs mount still OK. fix_status=fixed.
baseline #0: 'socket(AF_INET,SOCK_RAW,IPPROTO_RAW) -> OK fd=3 *** BYPASS ***' + 4 mounts OK (5 bypassed). patched #1: 'socket(...) -> Operation not permitted (errno=1) -- correctly denied' + 4 mounts EPERM (0 bypassed). build: '=== NK_DONE rc=0 ==='. positive-policy #1 (allow_raw_sockets=1): raw socket OK (per-cap arm reached). non-jail regression: raw socket OK, tmpfs mount OK.
Confirmed kernel references
- sys/kern/kern_caps.c:333
- sys/kern/kern_caps.c:334
- sys/kern/kern_caps.c:335
- sys/kern/kern_caps.c:340
- sys/kern/kern_jail.c:851
- sys/kern/kern_jail.c:854
- sys/kern/kern_jail.c:865
- sys/kern/kern_jail.c:866
- sys/kern/kern_jail.c:872
- sys/kern/kern_jail.c:878
- sys/kern/kern_jail.c:919
- sys/kern/kern_jail.c:923
- sys/kern/kern_jail.c:951
- sys/kern/kern_jail.c:961
- sys/kern/kern_jail.c:966
- sys/netinet/raw_ip.c:473
- sys/kern/vfs_syscalls.c:152
- sys/kern/vfs_syscalls.c:157
Detail
Exploit chain
Jail-policy bypass via cap corruption before prison_priv_check: jailed root (uid 0 inside a default-policy jail) gains raw IP socket creation (packet sniffing/injection, cross-tenant attack) and tmpfs/nullfs/devfs/procfs mounts (host fs exposure, device nodes, host process metadata) that the jail policy explicitly forbids. The fix passes the ORIGINAL specific cap to prison_priv_check so the per-cap switch arms are reached; with allow_raw_sockets=1 the raw socket is correctly ALLOWED, proving the arm is now honored both directions. No memory-corruption primitive; impact ceiling is the policy bypass itself.
Evidence (decisive lines)
BASELINE (unpatched #0, fix_baseline.log): socket(AF_INET,SOCK_RAW,IPPROTO_RAW) -> OK fd=3 *** BYPASS ***; mount tmpfs/null/devfs/procfs -> OK *** BYPASS *** (5/5 bypassed). PATCHED (single-fix #1, fix_run.log): socket(...) -> Operation not permitted (errno=1) -- correctly denied; all 4 mounts -> Operation not permitted (errno=1) (0/5 bypassed). Build NK_DONE rc=0.
PoC changes
No source changes this session (bypass.c unchanged, code_hash identical to prior session). Validated the existing fix.diff: applies cleanly via patch -p1 (Hunk #1 succeeded at 332), compiles in a full nativekernel build (NK_DONE rc=0). Updated VERDICT.md with a full fix-validation section, refreshed env.txt with both #0/#1 kernel identities and the patched-kernel sha256, and rewrote manifest.json to add fix_validation{} and the new artifacts (fix_baseline.log, fix_build.log, fix_run.log).
Verified recommended fix
In sys/kern/kern_caps.c caps_priv_check(), do not reuse cap for the group-master self-restriction test: introduce a block-local int gcap = (cap & __SYSCAP_GROUP_MASK) >> __SYSCAP_GROUP_SHIFT; and call caps_check_cred(cred, gcap), leaving cap holding the ORIGINAL specific capability for the final prison_priv_check(cred, cap) call so the per-capability switch arms in kern_jail.c are reached. One-line logical change. Matches the finding markdown's ## Recommended fix proposal. Full git-apply-able diff in findings/poc/DF-0165/fix.diff.
Verdict
REPRODUCED + FIX VALIDATED. caps_priv_check() at sys/kern/kern_caps.c:335 mutates its cap argument from the specific capability (e.g. SYSCAP_NONET_RAW=0x61) to the group-master number (SYSCAP_NONET=6) inside the if (cap & __SYSCAP_GROUP_MASK) block, then forwards the corrupted value to prison_priv_check() at :340. prison_priv_check() switches on cap & ~__SYSCAP_XFLAGS (kern_jail.c:854) and hits case SYSCAP_NONET: return 0 (:865-866) / case SYSCAP_NOMOUNT: return 0 (:872,878), so the per-capability switch arms that actually consult PRISON_CAP_ flags (NONET_RAW at :919 checking PRISON_CAP_NET_RAW_SOCKETS; NOMOUNT_{NULLFS,DEVFS,TMPFS,PROCFS} at :951-970) are dead code on this path. Confirmed on unpatched #0: inside a default-policy jail (allow_raw_sockets=0, vfs_mount_=0) a jailed root successfully opens a raw IPv4 socket AND mounts tmpfs/nullfs/devfs/procfs (5/5 bypasses).
No comments yet.