Skip to content

netlink: make synthesized replies acceptable to iproute2 - #234

Merged
congwang-mk merged 2 commits into
mainfrom
netlink-reply-fixes
Sep 20, 2026
Merged

congwang-mk merged 2 commits into
mainfrom
netlink-reply-fixes

Conversation

@congwang-mk

Copy link
Copy Markdown
Contributor

Two bugs in the NETLINK_ROUTE virtualization. glibc's getifaddrs tolerates both, which is why the existing tests never saw them; iproute2 (and libnl) do not.

1. Error replies carried the wrong pid (ip route hung)

The synthesized NLMSG_ERROR copied nlmsg_pid from the request, where senders leave it 0. The kernel puts the receiving socket's port id there. iproute2's dump loop skips a message whose pid is not its own, so it dropped the EOPNOTSUPP answer to an unsupported dump and blocked in recvmsg forever.

The outer header now uses the same pid the dump replies and getsockname already report. The request header echoed inside the error keeps the sender's value, as in a kernel reply.

2. Replies had no sender address (Sender address length == 0)

The child's socket is one end of a unix socketpair, which has no peer address, so recvmsg reported msg_namelen == 0. iproute2's rtnl_talk and libnl require a sockaddr_nl and give up otherwise: ip link show died on the RTM_NEWLINK probe it sends before listing anything. The recvmsg handler cannot supply the length, since the kernel writes it after the handler has returned Continue.

The responder's end is now bound to an abstract unix name of 10 bytes. The address the kernel reports is then exactly sizeof(sockaddr_nl) and is zero where nl_pid lies, which is how a kernel reply reads. Abstract names are host-wide, so the bytes over nl_groups, which no reader checks, carry a random value, with a retry on EADDRINUSE. The socket is a connected SEQPACKET that never listens, so the name gives nobody a way to reach it (ECONNREFUSED).

Tests

  • synth.rs unit test: the error's outer pid is the socket's, the echoed header keeps the sender's.
  • test_netlink_virt.rs: a raw NETLINK_ROUTE socket sends an unsupported request and checks the reply's sender address is 12 bytes with nl_pid == 0. It fails with None when the bind is removed. Neither test runs ip.
  • Manually: ip li show lists lo, ip route prints "Operation not supported" and exits. Both failed before.
  • Locally: sandlock-core 772 lib and 446 integration tests pass. Python and Go suites were not run locally.

Follow-up, not in this PR

After fix 2 the kernel overwrites all 12 bytes of msg_name, so the recvmsg/recvfrom handler that zeroes them looks redundant. Removing it would stop every recvmsg and recvfrom in the sandbox from trapping to the supervisor.

🤖 Generated with Claude Code

The synthesized NLMSG_ERROR copied nlmsg_pid from the request, where
senders leave it 0. The kernel puts the receiving socket's port id
there, and readers rely on it: iproute2's dump loop skips a message
whose pid is not its own, so it dropped the EOPNOTSUPP answer to an
unsupported dump and blocked in recvmsg forever (`ip route` hung).

Use the same pid the dump replies and getsockname already report. The
request header echoed inside the error keeps the sender's value, as it
does in a kernel reply.

Signed-off-by: Cong Wang <cwang@multikernel.io>
The child's NETLINK_ROUTE socket is one end of a unix socketpair, which
has no peer address, so recvmsg reported msg_namelen == 0. glibc does
not look, but iproute2's rtnl_talk and libnl require a sockaddr_nl and
give up otherwise: `ip link show` died with "Sender address length ==
0" on the RTM_NEWLINK probe it sends before listing anything.

The recvmsg handler cannot supply the length, since the kernel writes it
after the handler has returned Continue. Bind the responder's end to an
abstract unix name of 10 bytes instead: the address the kernel then
reports is exactly sizeof(sockaddr_nl) and is zero where nl_pid lies,
which is how a kernel reply reads. Abstract names are host-wide, so the
bytes over nl_groups, which no reader checks, carry a random value. The
socket is a connected SEQPACKET that never listens, so the name gives
nobody a way to reach it.

Signed-off-by: Cong Wang <cwang@multikernel.io>
@congwang-mk
congwang-mk merged commit 90290e6 into main Sep 20, 2026
17 checks passed
@congwang-mk
congwang-mk deleted the netlink-reply-fixes branch September 20, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant