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 likeping6) but deniesSYSCAP_RESTRICTEDROOT(to restrict privileged operations). - Trigger: create a raw IPv6 socket and use
sendmsgwithIPV6_NEXTHOPorIPV6_HOPOPTSancillary data to craft packets with arbitrary next-hop routing or hop-by-hop extension headers β operations thatsetsockopt(IPV6_NEXTHOP)would correctly deny withEPERM. - 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.
Recommended fix
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
sys/netinet6/raw_ip6.c:297-299β the uid-based privilege check.sys/netinet6/raw_ip6.c:302βprivflows toip6_setpktoptions.sys/netinet6/ip6_output.c:1156-1158β the correctcaps_priv_check_td(SYSCAP_RESTRICTEDROOT)used on the sticky path.sys/netinet6/ip6_output.c:2712,2763β theif (!priv) return EPERMgates.sys/netinet6/raw_ip6.c:530β existingcaps_priv_check(SYSCAP_NONET_RAW)usage in the same file.
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0621 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 == 0but with theSYSCAP_RESTRICTEDROOTcapability revoked (capsicum-sandboxed root) would still getpriv=1viarip6_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.
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
- sys/netinet6/raw_ip6.c:297-299 β the weak check:
c priv = 0; if (so->so_cred->cr_uid == 0) priv = 1; - sys/netinet6/raw_ip6.c:302-304 β
privflows toip6_setpktoptions(control, &opt, ..., priv). - sys/netinet6/ip6_output.c:2555 β
ip6_setpktoptiontakesprivparameter. - sys/netinet6/ip6_output.c:2712 β
if (!priv) return (EPERM);for IPV6_NEXTHOP. - sys/netinet6/ip6_output.c:2763 β
if (!priv) return (EPERM);for IPV6_HOPOPTS. - sys/netinet6/ip6_output.c:1156-1158 β the correct check used by
the setsockopt (sticky-option) path:
c privileged = (td == NULL || caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT)) ? 0 : 1; - sys/netinet6/ip6_output.c:1871 β
ip6_pcbopts(IPV6_PKTOPTIONS) hardcodespriv = 0β a separate (also-incorrect-but-the-other-way) inconsistency noted by the reviewer.
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_outputat all.
Impact ceiling
- No unpriv escalation demonstrable.
- Hardening gap: a capsicum-sandboxed root process (one that has had
SYSCAP_RESTRICTEDROOTrevoked but still hasSYSCAP_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==0with capsicum restriction ofSYSCAP_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
- sys/netinet6/raw_ip6.c:297-299 β weak cr_uid check (BUG)
- sys/netinet6/raw_ip6.c:302-304 β priv flows to ip6_setpktoptions
- sys/netinet6/raw_ip6.c:530 β SYSCAP_NONET_RAW gate on raw IPv6 socket
- sys/netinet6/ip6_output.c:1156-1158 β correct capsicum check (setsockopt path)
- sys/netinet6/ip6_output.c:2555 β ip6_setpktoption priv parameter
- sys/netinet6/ip6_output.c:2712 β IPV6_NEXTHOP EPERM gate
- sys/netinet6/ip6_output.c:2763 β IPV6_HOPOPTS EPERM gate
- sys/netinet6/ip6_output.c:1871 β ip6_pcbopts hardcodes priv=0 (related inconsistency)
Fix verification
not_testablen/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.
No comments yet.