Repository navigation
BSD: routes with a truncated trailing netmask are reported as prefix 0 #48
Description
Summary
On macOS, RouteListener::listen() reports ordinary routes as prefix 0, making them indistinguishable from default routes. Affects 0.2.13 and current master.
Cause
The sockaddr walk in deserialize_res skips any address whose remaining buffer is shorter than size_of::<sockaddr>() (16 bytes) — src/unix_bsd/mod.rs:454:
let buf = &msg[cur_pos..];
if buf.len() < std::mem::size_of::<sockaddr>() {
continue;
}BSD emits netmask sockaddrs truncated to their significant bytes (5–8 for IPv4), so when RTA_NETMASK is the last address in the message the remaining buffer is ~8 bytes and the walk bails. message_to_route then treats the missing entry as a zero prefix rather than as a failure — src/unix_bsd/mod.rs:517:
match route_addresses[RTAX_NETMASK as usize] {
None => prefix = 0,<DST,GATEWAY,NETMASK> is a common layout, so this is easy to hit.
Evidence
Captured on macOS 26.6 (Darwin 25.6.0), route_manager 0.2.13, while RFC1918 routes were installed.
route -n monitor on the same messages:
RTM_ADD: Add Route: len 132, flags:<UP,GATEWAY,DONE,STATIC>
sockaddrs: <DST,GATEWAY,NETMASK>
10.0.0.0 10.4.90.239 255.0.0.0
RouteListener::listen() for those same routes:
ADD dest=10.0.0.0 gw=10.4.90.239 PREFIX=0
ADD dest=172.16.0.0 gw=10.4.90.239 PREFIX=0
ADD dest=192.168.0.0 gw=10.4.90.239 PREFIX=0
ADD dest=169.254.0.0 gw=10.4.90.239 PREFIX=0
The kernel reports 255.0.0.0 (a /8); the parser reports prefix 0.
RouteManager::list() is not affected — sysctl dumps carry additional sockaddrs, so RTA_NETMASK is not last. On the same machine list() correctly returns 24, 8 and 4 for the routes listen() misreports.
Impact
Any consumer distinguishing default routes by prefix() == 0 sees non-default routes as default-route changes. In our case a VPN client treated every RFC1918 route the host installed as "the default route moved".
Two related observations
-
Out-of-bounds read.
src/unix_bsd/mod.rs:523doeslet mask_sa: &sockaddr_in = unsafe { mem::transmute(sa) }, reading 16 bytes from a region that may be onlysa_len(5–8) bytes. Reachable whenever a short netmask is accepted. -
Non-route messages are parsed as
rt_msghdr.deserialize_rescasts every routing-socket message tort_msghdr.RTM_IFINFOis anif_msghdr, which shares only the first 4 bytes, so thertm_errnoread at offset 24 lands inif_data.ifi_mtu. A tun with MTU 1350 coming up yields:Err(Os { code: 1350, kind: Uncategorized, message: "Unknown error: 1350" })Because that
return Errhappens mid-buffer (src/unix_bsd/mod.rs:415), any genuineRTM_ADD/RTM_DELETEbatched into the sameread()is discarded with it. Filtering onrtm_typebefore readingrtm_errnowould avoid both.
Suggested fix
Read sa_len first and validate against the ROUNDUP'd length rather than requiring size_of::<sockaddr>() bytes to remain; copy the available bytes into a zeroed sockaddr_in/sockaddr_in6 instead of transmuting. Failing that, distinguish "netmask absent" from "netmask unreadable" rather than mapping both to prefix = 0.
Activity
xmh0511 commentedon Oct 10, 2026
@kp-mariappan-ramasamy Thanks for reporting this bug. Can you help us to test this potential fix in the branch to check whether it works?
kp-mariappan-ramasamy commentedon Oct 12, 2026
@xmh0511 I give it a try and the fix is working