Unprivileged kernel heap info leak via route(4) sockaddr alignment padding: route keys/gateways stored RT_ROUNDUP-sized but only sa_len initialized, copied out rounded to any local user via RTM_GET
Summary
rt_setgate() allocates the route key+gateway chunk at rounded length (dlen=RT_ROUNDUP(dst->sa_len)) but rtrequest1 initializes only sa_len bytes of the key, and rt_setgate copies the gateway with the rounded glen. rt_msg_buffer() then copies RT_ROUNDUP(sa->sa_len) bytes of every sockaddr into routing-socket replies, and RTM_GET is the one route message exempt from the privilege check. For any family with sa_len%8!=0 (AF_INET6 sin6_len=28 -> 4-byte tail; AF_LINK/AF_MPLS variable), each RTM_GET discloses 1-7 bytes of stale M_RTABLE heap per sockaddr to any local user. VERIFIED on guest 5/5 as uid 1001: root grooms 32 host routes whose sockaddr_in6 keys carry sin6_len=32 + marker bytes at [28..32) (sa_len never validated against the family template), deletes them, adds one victim route with normal sin6_len=28 - unpriv RTM_GET returns DST sockaddr pad[28..32) = 41 42 43 44 (cross-object: bytes planted by a privileged process in one route's key padding surface, unmodified, in another route's reply). Silent, repeatable, default config. Fix validated (zero key/gateway padding at the store sites): leak gone, no regression. Companion to DF-0389/DF-3359 (rtsock side).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3366 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| leak_get.c | β | 3.2 KB | view raw | |
| groom.c | β | 5.1 KB | view raw | |
| build.sh | β | 136 B | view raw | |
| run.sh | β | 195 B | view raw | |
| build.log | β | 67 B | view raw | |
| run.baseline.log | β | 641 B | view raw | |
| groom.log | β | 146 B | view raw | |
| run.log | β | 1.1 KB | view raw | |
| fix.diff | β | 1.2 KB | view raw | |
| VERDICT.md | β | 5.4 KB | β raw | |
| verdict.json | β | 4.8 KB | view raw |
DF-3366 β VERDICT
Status: reproduced (unprivileged local kernel heap info leak). Impact: leak β uninitialized/stale kernel heap bytes disclosed to any local user via PF_ROUTE RTM_GET. Confidence: certain.
Root cause
rt_setgate()(sys/net/route.c:1182, 1213-1223) allocates the route's key+gateway chunk withdlen = RT_ROUNDUP(dst->sa_len)andglen = RT_ROUNDUP(gate->sa_len)β rounded lengths.rtrequest1()RTM_ADD/RTM_RESOLVE then initializes the key with onlysa_lenvalid bytes: * route.c:925bcopy(dst, ndst, dst->sa_len)β nothing initializes[sa_len, RT_ROUNDUP(sa_len)); * route.c:922rt_maskedcopy()β zeroes only up tocplim2 = dst + src->sa_len(route.c:1295, 1303-1304), again not to the rounded bound. The gateway is copied with the rounded lengthglen(route.c:1227), which also reads up to 7 bytes past the source sockaddr's valid content when the adding message's final sockaddr is unpadded (rtsock.c:1023 checkscp + sa->sa_len > cplim, not the rounded bound).rt_msg_buffer()(sys/net/rtsock.c:1134-1143) copiesRT_ROUNDUP(sa->sa_len)bytes of every sockaddr into the reply mbuf that is delivered to routing sockets.route_output()privilege check (rtsock.c:738-740) exemptsRTM_GETβ any local user can read the padded bytes.
Consequence: for any route whose key/gateway family has sa_len % 8 != 0
(AF_INET6: 28 β 4-byte tail; AF_LINK/AF_MPLS: variable), 1-7 stale heap
bytes per sockaddr are disclosed. The bytes are recycled M_RTABLE slab
content β i.e. fragments of previously freed routing objects.
How it was reproduced (guest DragonFly dfbsd 6.5-DEVELOPMENT #0 ... X86_64_GENERIC)
- Baseline: unprivileged
RTM_GET ::1β pads00 00 00 00(boot-time slab came from zeroed pages; leak content is benign here, the disclosure channel is nonetheless exercised) βrun.baseline.log. - Groom (root): 32 RTM_ADDs of host routes with a
sockaddr_in6carryingsin6_len = 32and marker41 42 43 44at bytes [28..32).sin6_lenis never validated against the family template, sortrequest1copies all 32 bytes into the 64-byte key chunk (bcopy(dst, ndst, dst->sa_len)). EEXIST-failed adds also work: the chunk is allocated and freed on the error path (route.c:976). Then 32 RTM_DELETEs free the marker chunks; one victim route with a normalsin6_len = 28sockaddr is added and reuses a chunk from the same M_RTABLE size class β its key tail is the stale marker. - Decisive read: unprivileged user
maxx(uid 1001)RTM_GET 2001:db8:99::1β DST pad41 42 43 44, 5/5 identical runs (run.log).
This proves cross-object stale kernel heap disclosure: bytes written by one (privileged) routing operation into a freed kernel allocation surface, unmodified, in another object's route entry, readable by an unprivileged user. On a multi-user host any user can silently harvest fragments of freed routing state (addresses, labels, malloc metadata) and, with route churn, recycled kernel heap generally.
Notes / adjacent surface (same consumer, not fixed here)
- The
RTA_IFP/RTA_IFAsockaddrs filled by_fillrtmsgare also copied out rounded: the link-levelsockaddr_dlis allocated pre-rounded (sys/net/if.c:568,sdl_len = socksize), AF_INET is 16 bytes β no tail; embedded AF_INET6 ifaddr sockaddrs expose 4 adjacent struct bytes (in-allocation, structured data, not stale heap). rt_msg_mbuf()(rtsock.c:1157+, route-change broadcasts) has the same rounded copy; those messages are delivered to all routing sockets.
Fix validation
fix.diff(in this pack): zero the key padding inrtrequest1after the key copy, and copy/zero the gateway by exactsa_len(also removes the β€7-byte over-read of the source sockaddr). Authored against the read-onlysys/tree; applied to the guest's/usr/srccopy only.- Procedure:
vm.shguest,cd /usr/src && patch -p1 < fix.diff && make -j6 nativekernel KERNCONF=X86_64_GENERIC, reboot into the patched kernel (uname recorded infix_validation.log), re-ran the exact PoC. - Baseline (stock kernel): DST pad
41 42 43 44. - Patched kernel: DST pad
00 00 00 00β leak gone; groom/victim still succeed (route add works, so no functional regression). fix_status: fixed.
Known-findings cross-reference (pass 2)
- DF-0397/DF-0398 (rt_setshims): still unfixed in this tree; note the
leak component is broader than the partial-alloc case β
rtfree()(route.c:299-307) and the RTM_DELETE path never freert_shim[0..2]at all, so every MPLS route deletion leaks its shims. - DF-0399 (rtredirect ignores rt_setgate): still unfixed.
- DF-0400 (rt_fixchange mask OOB): still unfixed, still latent (radix allocates masks key-sized; no new trigger path found).
- DF-3358 (rtmask_add_global pollution): still unfixed; confirmed there
is no free path for the per-CPU route mask trees β
rn_freemaskis only called for pf's mask head (sys/net/pf/pf_ioctl.c:3402), so pollution is permanent for the lifetime of the booted kernel.
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 (2 hunks in sys/net/route.c) applied to guest /usr/src; make -j6 nativekernel + installkernel + reboot; identical groom sequence runs clean (32/32 adds, victim created) and the unprivileged RTM_GET pads are 00 00 00 00 (5/5) vs 41 42 43 44 (5/5) on stock. Leak eliminated, no functional regression.
findings/poc/DF-3366/fix_validation.log (patched run), fix_uname.txt (patched kernel identity), run.log (baseline), fix.diff
Confirmed kernel references
Detail
Exploit chain
unpriv user: socket(PF_ROUTE) -> RTM_GET
Evidence (decisive lines)
['findings/poc/DF-3366/run.log β decisive: unpriv RTM_GET 2001:db8:99::1 -> RTAX[0] pad 41 42 43 44, 5/5 runs, uid=1001(maxx)', 'findings/poc/DF-3366/groom.log β root groom: spray 32/32 (marker keys), delete 32/32, victim added', 'findings/poc/DF-3366/run.baseline.log β unpriv ::1 baseline (zero pads from zeroed boot pages; channel exercised)', 'findings/poc/DF-3366/fix_validation.log β patched kernel #1: same groom, pads 00 00 00 00 5/5', 'findings/poc/DF-3366/fix.diff β validated fix (route.c: key-tail zero; gateway exact-length copy + tail zero)']
PoC changes
seed concept reworked: original sketch had no runnable code. Fixed inet_pton form (marker dsts need :: compression), non-blocking PF_ROUTE reads with poll+drain (rcvbuf flooded by broadcast replies), 28-byte delete form (rn_match compares min(clen) so tails never block deletion), gateway switched to a global on-link prefix (link-local gw needs KAME scope embedding).
Verified recommended fix
Zero sockaddr alignment padding when storing route keys and gateways (route.c:925-after, route.c:1227): bcopy only sa_len valid bytes and bzero the RT_ROUNDUP remainder.
Verdict
Uninitialized/stale kernel heap bytes are disclosed to unprivileged local users through routing-socket replies. rt_setgate() allocates route keys/gateways at RT_ROUNDUP(sa_len) size (sys/net/route.c:1213-1223) but only sa_len bytes are initialized (route.c:925 bcopy; rt_maskedcopy route.c:1299-1304 zeroes only to sa_len), while rt_msg_buffer() copies the full rounded length back to userland (rtsock.c:1140-1141) and RTM_GET requires no privilege (rtsock.c:738-740). Any AF with sa_len%8!=0 (AF_INET6: 4-byte tail; AF_LINK/AF_MPLS: variable) leaks 1-7 stale M_RTABLE bytes per sockaddr. Reproduced on the stock INVARIANTS guest: a root-side groom planted marker 41 42 43 44 at key offset [28..32) of 32 to-be-freed 64-byte route-key chunks; after free+realloc, unprivileged user maxx (uid 1001) read the marker back in the DST sockaddr padding of RTM_GET 2001:db8:99::1 replies β 5/5 deterministic runs β proving cross-object stale-heap disclosure. Baseline boot-time routes show zero pads only because fresh pages are zeroed. Fix validated by in-guest kernel rebuild: with the two zero-the-padding hunks applied, the same groom+read yields 00 00 00 00 (5/5) with no functional regression.
No comments yet.