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

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.

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

Timeline

  • 2026-06-30 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0165 Β· 18 files
FileTypeDescriptionSize
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
README.md readme what this pack is and how to reproduce
↓ download 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 per kern_jail_attach at sys/kern/kern_jail.c:227),
  • drives each gated action and reports OK / EPERM per 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.

VERDICT.md verdict full narrative + line-by-line kernel trace + recommended fix + fix-validation section
↓ download raw

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:

  1. opens a raw IPv4 socket (socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) β€” SYSCAP_NONET_RAW
  2. mounts tmpfs β€” SYSCAP_NOMOUNT_TMPFS
  3. mounts nullfs β€” SYSCAP_NOMOUNT_NULLFS (using kernel fstype "null")
  4. mounts devfs β€” SYSCAP_NOMOUNT_DEVFS
  5. 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:

  • :334 cap & __SYSCAP_GROUP_MASK = 0x61 & 0xF0 = 0x60 (truthy)
  • :335 cap = (0x61 & 0xF0) >> 4 = 0x6 (= SYSCAP_NONET, the group master)
  • :340 prison_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_attach calls caps_priv_check(ai->p_ucred, SYSCAP_NONET_RAW | __SYSCAP_NULLCRED). With cap = 0x00020061, the corruption still reduces it to 6: 0x00020061 & 0xF0 = 0x60, >> 4 = 6.

  • sys/kern/vfs_syscalls.c:152-157 β€” sys_mount calls caps_priv_check_td(td, priv) where priv = get_fscap(fstypename). get_fscap() returns the specific SYSCAP_NOMOUNT_* value, which is corrupted to SYSCAP_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) and mount("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)

  1. get_fscap() in sys/kern/vfs_syscalls.c:5386 matches strncmp("null", fsname, 5), which does NOT match the user-visible fstype "nullfs". The kernel fstype for nullfs is "null" (its vfsconf vfc_name). Anyone calling mount("nullfs", ...) falls through to the SYSCAP_RESTRICTEDROOT default β€” a separate latent surprise that the PoC works around by using "null".

  2. 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.

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.

fix_notes.md fix-notes post-verification fix rationale and correctness argument
↓ download raw

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 called caps_check_cred with it). The __SYSCAP_SELF self-restriction semantics are therefore unchanged.
  • prison_priv_check() itself strips __SYSCAP_XFLAGS before switching (switch (cap & ~__SYSCAP_XFLAGS) at sys/kern/kern_jail.c:854), so forwarding the original cap (which may carry __SYSCAP_NULLCRED etc., as the rip_attach caller 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 which cap & __SYSCAP_GROUP_MASK == 0 so the if body is skipped) keep exactly their old behavior β€” prison_priv_check(cred, 6) still hits case 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 11:16:47 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (sha256 9213f61534131ee4720f0eeaa04a588c64b881e94a63fb6185671cebf9fde92b)

Confirmed kernel references

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).