Dispatch write, writev, and readv on netlink fds - #305
Merged
Conversation
Xalestar
force-pushed
the
netlink-write
branch
from
August 17, 2026 06:57
956d9f7 to
1d924f0
Compare
Xalestar
force-pushed
the
netlink-write
branch
2 times, most recently
from
August 17, 2026 09:43
6ba89db to
105a246
Compare
A netlink guest fd holds the read end of the readiness pipe as its host
fd. write() and writev() on it fall through to that pipe end, which is
open read-only, and report EBADF. readv() with more than one entry reads
the same pipe instead of the response buffer and returns nothing.
Linux accepts write() on a netlink socket as sendto() with no explicit
destination, and busybox ip sends every rtnetlink request that way, so
"busybox ip addr" dies on its first dump request with "ip: write error:
Bad file descriptor". glibc getifaddrs() uses sendto and recvmsg, both
already routed, so the gap only shows on a sender that takes the write()
spelling.
One request, and one response, spans the whole iovec.
netlink_send_iov() gathers every entry into the request it parses and
netlink_recv_iov() fills every entry from the response, so all eight
spellings agree: a request split across entries is not truncated, a
first entry too small for the reply does not cap the read, and
nl_complete_span() bounds every receive to whole messages rather than
only the two that spell it recv.
The gather reads every entry before it reports any, so the count it
returns is bytes that were validated and parsed, and an unmapped entry
anywhere in the vector is EFAULT. A request past the staging buffer is
refused with EMSGSIZE, the errno Linux reports past sk_sndbuf; that
ceiling is the emulation's own and sits well past any request
nl_process_request() answers.
The receive counts what landed rather than whole entries:
guest_write_partial() reports the bytes a faulting copy placed, and
those bytes are in guest memory whatever the caller returns, so done and
buf_pos stay exact to the byte.
Four lengths behave differently, each measured against Linux 6.18 under
qemu-aarch64 rather than derived:
write(fd, buf, 0) -ENODATA. netlink_sendmsg refuses an empty
message before it builds an skb.
writev(fd, empty, n) 0. do_readv_writev returns on a zero total
before the socket is reached.
sendmsg(fd, empty) -ENODATA. ___sys_sendmsg hands the empty vector
to the socket instead of answering it above.
1 to 15 bytes the byte count, nothing parsed.
netlink_rcv_skb enters its loop only from
nlmsg_total_size(0) bytes up.
The netlink check in sys_writev sits ahead of the single-entry shortcut
so the second row holds for a one-entry vector too.
Verified: tests/test-netlink.c passes unchanged against elfuse and
against the qemu-aarch64 kernel, and each assertion fails when the
dispatch it covers is removed.
Xalestar
force-pushed
the
netlink-write
branch
from
August 17, 2026 09:49
105a246 to
65937f4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
busybox ip addrunder elfuse exits 1 on its first rtnetlink request:The verbose trace has the socket, the bind and the getsockname reaching the netlink handlers, and the request itself failing:
netlink_socket()registerspipefd[0]as the host fd so that poll and epoll have something to wait on.sys_write()andsys_writev()carry noFD_NETLINKcase, so the request reaches that pipe end, which is open read-only, and macOS returns EBADF.sys_readv()has the same gap foriovcnt > 1; a single entry already delegates tosys_read().Linux accepts
write()on a netlink socket assendto()with no explicit destination, and busybox's libiproute sends every request that way. glibcgetifaddrs()usessendtoandrecvmsg, both already routed, which is why the existing coverage passes over the gap.Scope: all eight spellings (
sendto,sendmsg,write,writev,recvfrom,recvmsg,read,readv) reach one gather and one scatter, so a request split across iov entries is not truncated, a receive is not capped by its first entry, andnl_complete_span()bounds all four receive spellings alike.tests/test-netlink.cdrives each of the eight against aNETLINK_ROUTEsocket and then the end-to-endgetifaddrs()path that originally regressed. Every receive polls before it reads, so a regression fails an assertion instead of blocking the run, and the assertions hold for a real kernel too and run in the qemu column of the matrix.Verification
Verified on macOS 26.6.1 (build 25G76), SDK 26.5, Apple M5 Pro, on a branch based on
upstream/mainat 3541d18.make test-matrixstays within baseline in all three modes with zero failures: elfuse-aarch64 252 passed, qemu-aarch64 231 passed, elfuse-x86_64 78 passed.test-netlinkis OK in the qemu-aarch64 column as well as the elfuse-aarch64 one, so all 22 assertions hold against a real 6.18 kernel.make lint,.ci/check-format.sh,.ci/check-newline.sh,.ci/check-security.shand.ci/check-matrix-lists.shpass..ci/check-cppcheck.shneedsmapfile, so it is left to the CI runner rather than this host's bash 3.2.make checkpasses the unit suite and reports 81 of 84 in the BusyBox smoke, with one failure that is this host and not the branch:nslookuptimes out because no DNS server is reachable from here, and it fails the same way with the branch's changes backed out.The new assertions fail without the change rather than passing vacuously. Against the branch's own sources backed out, the four that cover
sendmsgandrecvmsg:With only the
recvmsgscatter removed, so the two receive assertions stand on their own rather than on the send above them:Removing the
sys_writeandsys_writevdispatch:Removing the
sys_readvdispatch leaves the readv drain empty, and the recvmsg pair after it fails on the buffer that drain would have emptied: