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

Privilege bypass: rip6_output uses cr_uid==0 instead of caps_priv_check for per-packet IPv6 options

Field Value
ID DF-0621
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:L/I:L/A:N
CWE CWE-863 Incorrect Authorization
File sys/netinet6/raw_ip6.c
Lines 297-299, 302
Area netinet6 (raw IPv6 output privilege check)
Confidence likely
Discovered 2026-07-02
Reported pending

Summary

rip6_output determines whether the caller is privileged using so->so_cred->cr_uid == 0. This value is passed as the priv argument to ip6_setpktoptions/ip6_setpktoption, which gates IPV6_NEXTHOP, IPV6_HOPOPTS, IPV6_DSTOPTS, and IPV6_RTHDRDSTOPTS behind a if (!priv) return EPERM check. The sticky-option path (setsockopt via ip6_ctloutput) correctly uses caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT) (ip6_output.c:1156-1158), but the per-packet ancillary-data path via rip6_output uses the weaker uid check. A jail root with SYSCAP_NONET_RAW but without SYSCAP_RESTRICTEDROOT can therefore set privileged per-packet options via sendmsg that setsockopt would deny.

Root cause

At sys/netinet6/raw_ip6.c:297-299:

297:    priv = 0;
298:    if (so->so_cred->cr_uid == 0)
299:        priv = 1;

This priv flows to ip6_setpktoptions at raw_ip6.c:302, then to ip6_setpktoption (ip6_output.c:2555) where it controls access to: - IPV6_NEXTHOP (ip6_output.c:2712): if (!priv) return EPERM; - IPV6_HOPOPTS (ip6_output.c:2763): if (!priv) return EPERM; - IPV6_DSTOPTS / IPV6_RTHDRDSTOPTS: same gate.

The sticky (setsockopt) path computes privilege correctly (ip6_output.c:1156-1158):

privileged = (td == NULL ||
    caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT)) ? 0 : 1;

The inconsistency: a credential with cr_uid==0 but SYSCAP_RESTRICTEDROOT denied (e.g., a jail root with reduced capabilities) passes the uid check in rip6_output but fails the caps check in ip6_ctloutput. The result is that per-packet ancillary options (sendmsg cmsg) bypass the capability gate that sticky options (setsockopt) enforce.

Threat model & preconditions

  • Attacker position: uid 0 inside a jail or capability container that grants SYSCAP_NONET_RAW (to create raw sockets for tools like ping6) but denies SYSCAP_RESTRICTEDROOT (to restrict privileged operations).
  • Trigger: create a raw IPv6 socket and use sendmsg with IPV6_NEXTHOP or IPV6_HOPOPTS ancillary data to craft packets with arbitrary next-hop routing or hop-by-hop extension headers β€” operations that setsockopt(IPV6_NEXTHOP) would correctly deny with EPERM.
  • Impact: bypassing jail network ACLs or routing restrictions.
  • On systems where jail root retains SYSCAP_RESTRICTEDROOT: this is a no-op (both paths allow), but the code inconsistency is a latent privilege-confusion bug.

Replace the uid-based check with the capability check used everywhere else:

--- a/sys/netinet6/raw_ip6.c
+++ b/sys/netinet6/raw_ip6.c
@@ -295,8 +295,8 @@

-   priv = 0;
-   if (so->so_cred->cr_uid == 0)
+   priv = (caps_priv_check(so->so_cred, SYSCAP_RESTRICTEDROOT) == 0)
+       ? 1 : 0;
    dst = &dstsock->sin6_addr;

This makes the per-packet ancillary-data path consistent with the sticky-option path in ip6_ctloutput (ip6_output.c:1156-1158) and udp6_output. caps_priv_check is already used elsewhere in this file for SYSCAP_NONET_RAW at line 530, so the header dependency is satisfied.

References

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0621 Β· 7 files
FileTypeDescriptionSize
check.c trigger-source reachability check -- raw IPv6 socket as unpriv 1.8 KB view raw
fix.diff suggested-fix use caps_priv_check_td(SYSCAP_RESTRICTEDROOT) instead of cr_uid==0 416 B view raw
env.txt environment uname + reachability test output 336 B view raw
VERDICT.md verdict hardening gap analysis 3.4 KB ↓ raw
README.md readme bug description and reachability 2.4 KB ↓ 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 bug description and reachability
↓ download raw

DF-0621 β€” rip6_output priv check uses cr_uid==0 instead of capsicum

Bug

rip6_output (sys/netinet6/raw_ip6.c:297-299) computes the privileged flag for per-send cmsg (IPV6_NEXTHOP, IPV6_HOPOPTS, IPV6_DSTOPTS, IPV6_RTHDR, ...) using a weak credential test:

priv = 0;
if (so->so_cred->cr_uid == 0)
    priv = 1;

This priv flows to ip6_setpktoptions (raw_ip6.c:302) β†’ ip6_setpktoption (sys/netinet6/ip6_output.c:2555) where it gates restricted IPv6 options (e.g. line 2712 if (!priv) return (EPERM) for IPV6_NEXTHOP; line 2763 for IPV6_HOPOPTS).

The sticky-option path (setsockopt via ip6_ctloutput) correctly uses the proper capsicum check at sys/netinet6/ip6_output.c:1156-1158:

privileged = (td == NULL ||
              caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT)) ?
             0 : 1;

The inconsistency: rip6_output should use the same capsicum check but instead tests only cr_uid == 0.

Direction of the inconsistency (important)

This is NOT an "unpriv user gains privilege" bug. The check grants priv=1 only when cr_uid == 0 (root). It does NOT grant privilege to non-root users. The inconsistency is:

  • A process with cr_uid == 0 but with the SYSCAP_RESTRICTEDROOT capability revoked (capsicum-sandboxed root) would still get priv=1 via rip6_output (the cmsg path), even though the setsockopt path would correctly deny it.

This is a defense-in-depth / hardening gap, not a privilege escalation. The bypass is of capability-based restrictions on an already-root credential.

Reachability on this guest

rip6_output is the pru_send for SOCK_RAW IPv6 sockets. Opening one requires SYSCAP_NONET_RAW (raw_ip6.c:530: caps_priv_check(..., SYSCAP_NONET_RAW | __SYSCAP_NULLCRED)). On this guest an unprivileged user (uid=1001) gets EPERM:

$ ./check
[*] DF-0621 reachability check
[*] trying to open AF_INET6 SOCK_RAW as uid=1001
[+] socket(AF_INET6, SOCK_RAW) FAILED: Operation not permitted (errno=1)
[+] expected: SYSCAP_NONET_RAW required (rip6_attach raw_ip6.c:530)
[+] => unprivileged user CANNOT reach rip6_output
[+] => DF-0621 is a root-only hardening gap, not unpriv->root

So: - An unprivileged user cannot reach rip6_output at all. - The only credential that experiences the wrong check is a capsicum-restricted root, which is a defense-in-depth scenario.

Fix

Use caps_priv_check_td(curthread, SYSCAP_RESTRICTEDROOT) to match the setsockopt path. See fix.diff.

VERDICT.md verdict hardening gap analysis
↓ download raw

DF-0621 β€” Verdict: NOT REPRODUCED (hardening inconsistency only)

Verdict

NOT REPRODUCED as an unprivileged escalation. The code-path inconsistency is real (confirmed by source trace) but the "privilege bypass" is in the wrong direction for unpriv users: it can only fail to deny a capsicum-restricted root credential. An unprivileged user (cr_uid != 0) gets priv=0 either way, and cannot even reach rip6_output because rip6_attach (raw_ip6.c:530) requires SYSCAP_NONET_RAW.

This is correctly classified as a Medium hardening / correctness finding, not a Critical/High escalation.

Source trace

Reachability test

  • sys/netinet6/raw_ip6.c:530 β€” caps_priv_check(ai->p_ucred, SYSCAP_NONET_RAW | __SYSCAP_NULLCRED) gates raw IPv6 socket creation.
  • Empirically on this guest: socket(AF_INET6, SOCK_RAW, IPPROTO_RAW) as uid=1001 β†’ EPERM.
  • Therefore an unprivileged user cannot reach rip6_output at all.

Impact ceiling

  • No unpriv escalation demonstrable.
  • Hardening gap: a capsicum-sandboxed root process (one that has had SYSCAP_RESTRICTEDROOT revoked but still has SYSCAP_NONET_RAW) could set IPV6_NEXTHOP via per-send cmsg even though the system policy intended to deny it. This is a real but narrow defense-in-depth concern.
  • Realistic deployments rarely combine cr_uid==0 with capsicum restriction of SYSCAP_RESTRICTEDROOT, so practical impact is low.

Fix validation

The fix is one line: replace the weak cr_uid == 0 check with the proper caps_priv_check_td(curthread, SYSCAP_RESTRICTEDROOT). Validated by: 1. Source trace shows the fix is correct. 2. fix.diff applies cleanly with patch -p1 --dry-run against sys/netinet6/raw_ip6.c.

A kernel build/boot test is not strictly required for a hardening-only finding with no reproduced behavior to compare against, but the change is a 3-line surgical replacement that matches the existing pattern at ip6_output.c:1156.

Kernel references

Fix verification

not_testable

n/a

see evidence pack

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

NOT reproduced. rip6_output cr_uid==0 not caps_priv_check -> root-only hardening gap. Unpriv can't reach (SYSCAP_NONET_RAW). Compile validated.