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

v4 SIOCDLIFADDR kernel stack buffer overflow: in_lifaddr_ioctl bcopy uses unvalidated ia_sockmask/ia_dstaddr sin_len as length into 64-byte on-stack in_aliasreq β€” deterministic on stock lo0 (::1 /128) via the DF-0490 confusion, and via root-poisoned sin_len once the filter is fixed

Summary

in_lifaddr_ioctl's SIOCDLIFADDR branch copies ifaddr sockaddrs into a 64-byte on-stack struct in_aliasreq using sin_len fields of the selected in_ifaddr as bcopy lengths with no validation. Because the walk filter (DF-0490) selects AF_INET6 ifaddrs inside the IPv4 handler and casts an in6_ifaddr as in_ifaddr, ia_sockmask.sin_len reads in6_ifaddr offset 344 = ia_prefixmask.sin6_addr byte 12, which is 0xff for any IPv6 prefix >= /104 - including the stock /128 of lo0's ::1 - yielding a deterministic 255-byte bcopy (223 bytes past the stack object) that smashes the return path on the netisr0 dispatch thread. VERIFIED on stock guest: SIOCDLIFADDR with the pointer-byte-derived match address -> 'Fatal trap 12 ... fault VA = 0xe4 (supervisor write), instruction pointer = 0xfffff8004f308f46' (heap-derived smashed return address), netisr0 stopped executing heap garbage, guest wedged in DDB - deterministic on a completely default configuration. Same sinks are the IPv4 twin of DF-3318: root can poison ia_dstaddr/ia_sockmask sin_len and those flow into these stack bcopies the moment the :911 filter is corrected without length validation (fix-interaction hazard documented). DF-0490 filed only the unpriv read half; the privileged write/stack-smash half was missed. Privileged trigger; content groomable by the triggering root - ceiling privileged kernel stack corruption/hard DoS. Fix validated on rebuilt kernel (AF_INET filter fix + deref fix + consume-side clamps + set-side sin_len validation): ioctl returns EADDRNOTAVAIL, lo0 intact, v4 SIOCGLIFADDR becomes functional as regression proof.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3319 Β· 15 files
FileTypeDescriptionSize
README.md β€” 3.5 KB ↓ raw
VERDICT.md β€” 5.8 KB ↓ raw
smash.c β€” 2.6 KB view raw
getlif.c β€” 1.2 KB view raw
build.sh β€” 93 B view raw
run.sh β€” 181 B view raw
precheck.log β€” 96 B view raw
run.log β€” 96 B view raw
smash.log β€” 733 B view raw
panic.txt β€” 646 B view raw
fix.diff β€” 3.7 KB view raw
fix_run.log β€” 847 B view raw
fix_build.log β€” 5.6 MB ↓ download
verdict.json β€” 5.5 KB view raw
manifest.json β€” 1.3 KB view raw

DF-3319 β€” v4 SIOCDLIFADDR kernel stack buffer overflow (in_lifaddr_ioctl)

Status: REPRODUCED (Fatal trap 12, control-flow corruption, guest wedged in DDB) + fix validated (patched kernel returns EADDRNOTAVAIL, guest stays up).

What

in_lifaddr_ioctl() (sys/netinet/in.c) β€” the AF_INET SIOCDLIFADDR path β€” copies ifaddr sockaddrs into a 64-byte on-stack struct in_aliasreq ifra using sin_len fields read out of the selected in_ifaddr as bcopy lengths:

bcopy(&ia->ia_addr,     &ifra.ifra_addr,    ia->ia_addr.sin_len);     /* :949 */
bcopy(&ia->ia_dstaddr,  &ifra.ifra_dstaddr, ia->ia_dstaddr.sin_len);  /* :952 */
bcopy(&ia->ia_sockmask, &ifra.ifra_dstaddr, ia->ia_sockmask.sin_len); /* :955 */

None of these lengths is validated, and the ia is selected by the DF-0490 type confusion (:911 filters != AF_INET6 in the IPv4 handler, :923 casts an in6_ifaddr to in_ifaddr). Under the confusion, ia_sockmask.sin_len reads in6_ifaddr offset 344 = ia_prefixmask.sin6_addr byte 12, which is 0xff for any IPv6 prefix length β‰₯ /104 β€” including the stock /128 of lo0's ::1 β€” producing a deterministic 255-byte bcopy into the 64-byte stack object (223 bytes past its end) that smashes the return path of in_lifaddr_ioctl() (running on the netisr0 dispatch thread, in.c:230-248).

DF-0490 claimed only the unprivileged read side (:927); this is the missed privileged write/stack-smash side of the same confusion, plus the DF-3318 twin persistence path (root can poison ia_dstaddr/ia_sockmask.sin_len directly via SIOCSIFDSTADDR/SIOCAIFADDR at :602/:668, which becomes the live route into :949-956 the moment the :911 filter is fixed without length validation).

How to reproduce

cc -O -o smash smash.c && cc -O -o getlif getlif.c   # build.sh
./getlif lo0     # stock kernel: garbage (type confusion visible)
./smash lo0      # as root; Fatal trap 12 within ~1s; guest wedges in DDB

The query address 0.248.255.255 is derived in smash.c: the walk's candidate expression (:915-916) reads through &ifa->ifa_addr β€” the address of the pointer field β€” so candidate is always the high 4 bytes of the kernel pointer (0xfffff800...) for every ifaddr; 00 f8 ff ff as network bytes is 0.248.255.255, matching the first AF_INET6 ifaddr on lo0 (::1, /128).

Expected (stock INVARIANTS kernel #0)

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xe4     (supervisor write data, page not present)
instruction pointer   = 0x8:0xfffff8004f308f46   <- heap address: smashed
frame pointer         = 0xfffff8004f308f28          return address jumped
current process       = Idle                        (netisr0 dispatch)
Stopped at -0x7ffb0cf70ba: addb %al,(%rax)          <- executing heap garbage

See panic.txt (serial console) β€” ssh dies; vm.sh status => down.

Fix validation (fix.diff, kernel #3 Thu Sep 10 19:51:37 UTC 2026)

check stock #0 patched #3
./smash lo0 Fatal trap 12, DDB wedge EADDRNOTAVAIL, guest up
lo0 after smash (wedged) 127.0.0.1 intact (no bogus delete)
./getlif lo0 family=0 len=0 (confused) 127.0.0.1 len=16 /8 β€” v4 SIOCGLIFADDR now works
normal alias add/del n/a works (no regression)

The accidental observation on intermediate kernel #2 (clamps only, candidate hunk missing) is recorded in fix_run.log history: no panic, but the pointer-derived match still deleted 127.0.0.1 β€” evidence that the full fix (filter + candidate deref + clamps + set-side validation) is required.

VERDICT.md
↓ download raw

DF-3319 VERDICT

Status: reproduced β€” privileged (SIOCDLIFADDR requires SYSCAP_RESTRICTEDROOT, in.c:437-441) deterministic kernel stack buffer overflow with control-flow corruption; fix validated on a rebuilt kernel.

The bug

Three bcopy calls in in_lifaddr_ioctl() (sys/netinet/in.c) use sin_len fields of the selected ifaddr as copy lengths into the 64-byte on-stack struct in_aliasreq ifra (:941-959), with no validation. The ifaddr is selected by the DF-0490 confusion (:911 != AF_INET6 in the IPv4 handler), so ia (:923) is really a struct in6_ifaddr:

  • ia_sockmask.sin_len = in6_ifaddr offset 344 = ia_prefixmask.sin6_addr[12] β†’ 0xff for every v6 prefix β‰₯ /104, lo0's stock ::1/128 included (in6_var.h:116 layout: ia_addr@240, ia_net@268, ia_dstaddr@296, ia_prefixmask@324).
  • the resulting bcopy(&ia->ia_sockmask, &ifra.ifra_dstaddr, 255) writes 255 bytes at stack offset +32 of a 64-byte object β€” 223 bytes past its end, smashing the return path of in_lifaddr_ioctl(), which runs on netisr0 (in.c:230-248 dispatches SIOCDLIFADDR).

DF-0490's filed claim covers only the unprivileged read at :927. The stack-smash at :949-956 (and its DF-3318-twin persistence route: root poisons ia_dstaddr.sin_len via SIOCSIFDSTADDR :602 / ia_sockmask.sin_len via SIOCAIFADDR :668, reachable directly once :911 is corrected) is the residual this pass-2 finding captures.

Reproduction (stock INVARIANTS kernel #0, guest dfbsd 6.5-DEVELOPMENT)

  • ./getlif lo0 (precheck.log): SIOCGLIFADDR returns family=0 len=0 addr=0.0.0.0 β€” the type confusion, live on a stock interface.
  • ./smash lo0 as root: SIOCDLIFADDR with match address 0.248.255.255 (derived in smash.c from the :915-916 candidate expression, which reads through &ifa->ifa_addr, i.e. the high 4 bytes of the pointer β€” always 0xfffff800 on this guest). Result: Fatal trap 12, supervisor write data at 0xe4, instruction pointer = 0xfffff8004f308f46 (a kernel-heap address β€” the smashed return address contained a pointer from the bcopy source region), current process = Idle (netisr0 thread), CPU stopped executing heap garbage (addb %al,(%rax)), guest wedged in DDB, ssh dead (vm.sh status β‡’ down). See panic.txt.
  • Two attempts total: the first used INADDR_ANY as the match (walk arithmetic then never matched β†’ EADDRNOTAVAIL); after deriving the pointer-byte match, the panic is deterministic (it fires on the single ioctl; reproduced once per guest boot because the guest must be reset afterwards).

Why the impact ceiling is what it is

The trigger ioctl is privileged, so this is not an unpriv→root escalation by itself; it is kernel stack memory corruption with content sourced from the in6_ifaddr tail plus adjacent slab (groomable by the same privileged user), deterministic control-flow destruction (RIP observed executing a heap pointer), and a hard DoS (wedged DDB). The unprivileged side of the same confusion remains DF-0490's :927 leak. Fixing DF-0490's filter alone would activate the DF-3318-twin route (poisoned v4 sin_len → these same stack bcopies), which is why the fix below validates lengths as well.

Fix validation

fix.diff (git-apply-able against sys/netinet/in.c) contains:

  1. :911 filter AF_INET6 β†’ AF_INET (the DF-0490 typo β€” needed so the v4 handler stops feeding in6_ifaddr objects into these bcopies and so v4 SIOC[GD]LIFADDR work at all);
  2. :915-916 candidate (struct sockaddr_in *)&ifa->ifa_addr β†’ deref the sockaddr (second half of the same botched copy; without it the walk matches every ifaddr by pointer bytes and deletes wrong addresses);
  3. consume-side clamps (alen/dlen/mlen ≀ sizeof(struct sockaddr_in)) at :927/:930/:949/:952/:955 β€” defense in depth for any garbage/poisoned sin_len in a live ifaddr;
  4. set-side validation: SIOCAIFADDR (addr/mask/dstaddr/broadaddr), SIOCSIFDSTADDR, SIOCSIFBRDADDR reject sin_len > sizeof(struct sockaddr_in) before persisting; in_ifinit() rejects oversized sin->sin_len (kills the DF-3320 persistence at :998).

Built in-guest (make -j4 nativekernel KERNCONF=X86_64_GENERIC, 0 warnings), installed, rebooted into kernel #3 (Thu Sep 10 19:51:37 UTC 2026):

  • baseline (stock #0): smash β‡’ Fatal trap 12 / DDB wedge (panic.txt).
  • patched (#3): smash β‡’ EADDRNOTAVAIL, lo0 keeps 127.0.0.1 (no bogus delete), guest stays up; ./getlif lo0 β‡’ 127.0.0.1 len=16 /8 (v4 SIOCGLIFADDR now functional β€” regression proof); normal alias add/remove works (fix_run.log).
  • intermediate kernel #2 (clamps present, candidate hunk missing due to a patch-application slip, documented): no panic, but the pointer-byte match deleted 127.0.0.1 β€” demonstrating the clamps alone are insufficient for correct behavior and motivating hunks 1-2.

Lines cited

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied in-guest to /usr/src/sys/netinet/in.c, make -j4 nativekernel (0 warnings), installkernel, reboot into kernel #3. Baseline stock kernel: Fatal trap 12 / DDB wedge. Patched: ./smash lo0 => EADDRNOTAVAIL, no panic, lo0 keeps 127.0.0.1 (no bogus delete), guest stays up; SIOCGLIFADDR(lo0) now returns 127.0.0.1 (v4 LIFADDR functional, regression proof); normal alias add/remove unaffected. Bad behavior gone, correct error returned.

fix_run.log (full validation matrix), fix_build.log (kernel #3 build), fix.diff (git-apply-able, applied with 'patch' in guest)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #3: Thu Sep 10 19:51:37 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

root: socket(AF_INET,SOCK_DGRAM) -> ioctl(SIOCDLIFADDR, {lo0, addr=0.248.255.255/32}) -> in_control dispatch to netisr0 (in.c:230-248) -> in_lifaddr_ioctl walk: filter '!= AF_INET6' (in.c:911, DF-0490 typo) skips AF_INET, selects ::1's in6_ifaddr via the pointer-byte candidate match (in.c:915-916) -> cast to in_ifaddr (in.c:923) -> bcopy(&ia->ia_sockmask, &ifra.ifra_dstaddr, ia_sockmask.sin_len) with len=0xff read from prefixmask.sin6_addr[12] of the /128 mask (in.c:955-956) -> 255 bytes written into the 64-byte stack in_aliasreq at +32 -> return address overwritten with in6_ifaddr-tail/adjacent-slab content -> RIP follows kernel heap pointer -> Fatal trap 12, DDB wedge, DoS.

Evidence (decisive lines)

["panic.txt: 'Fatal trap 12: page fault while in kernel mode', 'fault virtual address = 0xe4', 'instruction pointer = 0x8:0xfffff8004f308f46', 'current process = Idle', 'Stopped at -0x7ffb0cf70ba: addb %al,(%rax)'", 'precheck.log / run.log: stock-kernel SIOCGLIFADDR(lo0) returns family=0 len=0 addr=0.0.0.0 (the DF-0490 confusion feeding these bcopy lengths)', 'smash.log: ssh session hangs on the ioctl; serial console shows the trap', "fix_run.log: patched kernel #3 -- smash => EADDRNOTAVAIL, 127.0.0.1 intact, getlif returns '127.0.0.1 len=16 prefixlen=8', guest up", 'fix.diff: AF_INET filter fix (:911), candidate deref fix (:915-916), consume-side clamps (alen/dlen/mlen <= sizeof(sockaddr_in)), set-side sin_len validation', "fix_build.log: '>>> Kernel build for X86_64_GENERIC completed', 0 warnings"]

PoC changes

Derived the walk match address: the candidate expression at in.c:915-916 reads through &ifa->ifa_addr (address of the pointer field), so candidate is always the pointer's high 4 bytes (0xfffff800 on this guest) -> query 0.248.255.255; first attempt with INADDR_ANY returned EADDRNOTAVAIL. Added arpa/inet.h include and corrected the printf string.

Verified recommended fix

in.c:911 AF_INET6->AF_INET; in.c:915-916 dereference ifa->ifa_addr; clamp all sin_len bcopy lengths in in_lifaddr_ioctl to sizeof(struct sockaddr_in); reject sin_len>sizeof(struct sockaddr_in) on SIOCAIFADDR/SIOCSIFDSTADDR/SIOCSIFBRDADDR and in in_ifinit().

Verdict

REPRODUCED. The AF_INET SIOCDLIFADDR path in in_lifaddr_ioctl() bcopy's ifaddr sockaddls into a 64-byte on-stack struct in_aliasreq using unvalidated sin_len values as lengths (in.c:949-956). Because the walk filter at in.c:911 selects AF_INET6 ifaddrs in the IPv4 handler (DF-0490) and :923 casts an in6_ifaddr as in_ifaddr, ia_sockmask.sin_len reads in6_ifaddr offset 344 = prefixmask.sin6_addr byte 12, which is 0xff for any IPv6 prefix >= /104 -- including lo0's stock ::1/128 -- giving a deterministic 255-byte bcopy (223 bytes past the stack object) that smashes the return path on the netisr0 dispatch thread. Observed: Fatal trap 12, supervisor write at 0xe4, instruction pointer = 0xfffff8004f308f46 (heap address -- the smashed return address contained a kernel heap pointer), CPU stopped executing heap garbage, guest wedged in DDB (panic.txt). Privileged trigger (SIOCDLIFADDR requires SYSCAP_RESTRICTEDROOT, in.c:437-441); deterministic kernel stack control-flow corruption. Fix validated on rebuilt kernel #3: ioctl returns EADDRNOTAVAIL, lo0 intact, guest stays up, and v4 SIOCGLIFADDR becomes functional (127.0.0.1 returned) as regression proof.