Skip to content

BSD: routes with a truncated trailing netmask are reported as prefix 0 #48

Closed

Description

@kp-mariappan-ramasamy

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

  1. Out-of-bounds read. src/unix_bsd/mod.rs:523 does let mask_sa: &sockaddr_in = unsafe { mem::transmute(sa) }, reading 16 bytes from a region that may be only sa_len (5–8) bytes. Reachable whenever a short netmask is accepted.

  2. Non-route messages are parsed as rt_msghdr. deserialize_res casts every routing-socket message to rt_msghdr. RTM_IFINFO is an if_msghdr, which shares only the first 4 bytes, so the rtm_errno read at offset 24 lands in if_data.ifi_mtu. A tun with MTU 1350 coming up yields:

    Err(Os { code: 1350, kind: Uncategorized, message: "Unknown error: 1350" })
    

    Because that return Err happens mid-buffer (src/unix_bsd/mod.rs:415), any genuine RTM_ADD/RTM_DELETE batched into the same read() is discarded with it. Filtering on rtm_type before reading rtm_errno would 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

  1. xmh0511 commented on Oct 10, 2026

    @xmh0511
    Collaborator

    @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?

  2. kp-mariappan-ramasamy commented on Oct 12, 2026

    @kp-mariappan-ramasamy
    Author

    @xmh0511 I give it a try and the fix is working

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions