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)
PoC verification
Evidence pack
findings/poc/DF-3319 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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/128included (in6_var.h:116layout: 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 ofin_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):SIOCGLIFADDRreturnsfamily=0 len=0 addr=0.0.0.0β the type confusion, live on a stock interface../smash lo0as root: SIOCDLIFADDR with match address 0.248.255.255 (derived in smash.c from the:915-916candidate 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 dataat0xe4,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). Seepanic.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:
:911filterAF_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);:915-916candidate(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);- consume-side clamps (
alen/dlen/mlenβ€sizeof(struct sockaddr_in)) at:927/:930/:949/:952/:955β defense in depth for any garbage/poisonedsin_lenin a live ifaddr; - set-side validation: SIOCAIFADDR (
addr/mask/dstaddr/broadaddr), SIOCSIFDSTADDR, SIOCSIFBRDADDR rejectsin_len > sizeof(struct sockaddr_in)before persisting;in_ifinit()rejects oversizedsin->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
sys/netinet/in.c:911β AF_INET6 filter in IPv4 handler (DF-0490)sys/netinet/in.c:915-916β candidate read through&ifa->ifa_addrsys/netinet/in.c:923β in6_ifaddr cast to in_ifaddrsys/netinet/in.c:941-959β on-stackin_aliasreq ifra+ unvalidated bcopy lengths (:949-950,:952-953,:955-956)sys/netinet/in.c:230-248β SIOCDLIFADDR dispatch to netisr0sys/netinet/in.c:437-441β privilege gate (privileged trigger)sys/netinet/in.c:602, :668β privileged sin_len persistence (twin route)sys/netinet6/in6_var.h:116-132β in6_ifaddr layout (offset arithmetic)
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
fixedfix.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)
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.
No comments yet.