From 65937f421559de5f8f48171eafbd2cb689c92849 Mon Sep 17 00:00:00 2001 From: Xalestar Date: Mon, 17 Aug 2026 14:13:59 +0800 Subject: [PATCH] Dispatch every netlink spelling on the whole iovec 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. --- src/core/guest.c | 25 +++ src/core/guest.h | 14 ++ src/syscall/io.c | 19 +- src/syscall/net.h | 11 ++ src/syscall/netlink.c | 422 ++++++++++++++++++++++++++---------------- tests/test-netlink.c | 178 +++++++++++++++++- 6 files changed, 500 insertions(+), 169 deletions(-) diff --git a/src/core/guest.c b/src/core/guest.c index 93a0ddb5..a2a6cf09 100644 --- a/src/core/guest.c +++ b/src/core/guest.c @@ -1737,6 +1737,31 @@ int guest_write(guest_t *g, uint64_t gva, const void *src, size_t len) return guest_copy(g, gva, NULL, src, len, MEM_PERM_W); } +size_t guest_write_partial(guest_t *g, + uint64_t gva, + const void *src, + size_t len) +{ + size_t done = 0; + while (done < len) { + uint64_t avail; + void *dst = gva_resolve_perm(g, gva + done, &avail, MEM_PERM_W, + (uint64_t) (len - done)); + if (!dst) + return done; + + size_t chunk = len - done; + if (chunk > avail) + chunk = avail; + size_t moved = + guest_host_copy_partial(dst, (const uint8_t *) src + done, chunk); + done += moved; + if (moved < chunk) + return done; + } + return done; +} + int guest_write_small(guest_t *g, uint64_t gva, const void *src, size_t len) { uint64_t avail = 0; diff --git a/src/core/guest.h b/src/core/guest.h index 52a0f2c7..9e85aa59 100644 --- a/src/core/guest.h +++ b/src/core/guest.h @@ -1055,6 +1055,20 @@ int guest_read_small(const guest_t *g, uint64_t gva, void *dst, size_t len); */ int guest_write(guest_t *g, uint64_t gva, const void *src, size_t len); +/* Bounds-checked copy from host buffer into guest memory, reporting how many + * bytes landed. + * + * A caller whose return value is a byte count uses this instead of + * guest_write(), which reports only whether the whole copy survived: a copy + * that faults or leaves the mapping partway still places the bytes before that + * point, and they are in guest memory whatever the caller reports. The count is + * exact to the bound guest_host_copy_partial() documents. + */ +size_t guest_write_partial(guest_t *g, + uint64_t gva, + const void *src, + size_t len); + /* Optimized host-to-guest copy for small fixed-size outputs. Uses a direct * guest pointer when the full range is contiguous and writable, otherwise falls * back to guest_write() for boundary-crossing safety. diff --git a/src/syscall/io.c b/src/syscall/io.c index 830f79e5..23e9c418 100644 --- a/src/syscall/io.c +++ b/src/syscall/io.c @@ -1005,6 +1005,15 @@ int64_t sys_write(guest_t *g, int fd, uint64_t buf_gva, uint64_t count) if (type == FD_EVENTFD) return eventfd_write(fd, g, buf_gva, count); + /* Linux accepts write() on a netlink socket as sendto() with no explicit + * destination, and iproute2-style senders use it in place of sendto. The + * host fd behind a netlink guest fd is the read end of the readiness pipe, + * so falling through would write to a read-only pipe end and report EBADF + * for a request the emulation can answer. + */ + if (type == FD_NETLINK) + return netlink_send(fd, g, buf_gva, count); + host_fd_ref_t host_ref; int64_t err = host_fd_ref_open_checked(fd, &host_ref, true); if (err < 0) @@ -1411,6 +1420,8 @@ int64_t sys_readv(guest_t *g, int fd, uint64_t iov_gva, int iovcnt) { if (iovcnt == 0) return vec_zero_iovcnt(fd, false, false); + if (fd_get_type(fd) == FD_NETLINK) + return netlink_readv(fd, g, iov_gva, iovcnt); if (iovcnt == 1) { linux_iovec_t giov; int64_t err = single_guest_iov(g, iov_gva, &giov); @@ -1564,6 +1575,11 @@ int64_t sys_writev(guest_t *g, int fd, uint64_t iov_gva, int iovcnt) { if (iovcnt == 0) return vec_zero_iovcnt(fd, true, false); + /* Ahead of the single-entry shortcut: a one-entry writev of nothing reports + * 0, while the write(2) the shortcut would reach reports ENODATA. + */ + if (fd_get_type(fd) == FD_NETLINK) + return netlink_writev(fd, g, iov_gva, iovcnt); if (iovcnt == 1) { linux_iovec_t giov; int64_t err = single_guest_iov(g, iov_gva, &giov); @@ -1577,7 +1593,8 @@ int64_t sys_writev(guest_t *g, int fd, uint64_t iov_gva, int iovcnt) * iovs) because the data is at giov.iov_base which is only giov.iov_len * bytes. eventfd expects exactly 8 bytes. */ - if (fd_get_type(fd) == FD_EVENTFD) { + int wtype = fd_get_type(fd); + if (wtype == FD_EVENTFD) { if (iovcnt <= 0) return -LINUX_EINVAL; linux_iovec_t giov; diff --git a/src/syscall/net.h b/src/syscall/net.h index ace6ff94..51be64b2 100644 --- a/src/syscall/net.h +++ b/src/syscall/net.h @@ -15,6 +15,7 @@ #include #include "core/guest.h" +#include "syscall/linux-wire.h" /* linux_iovec_t */ /* Linux address families. */ #define LINUX_AF_UNSPEC 0 @@ -227,6 +228,16 @@ int64_t netlink_read(int guest_fd, int64_t netlink_send(int guest_fd, guest_t *g, uint64_t buf_gva, uint64_t len); +/* Vectored forms of the two above, taking the guest iovec array they stage. + * One netlink request, and one response, spans the whole iovec: the send + * gathers every entry into one request and the receive fills every entry from + * one response, so neither stops at the first entry the way the scalar special + * fds do. + */ +int64_t netlink_writev(int guest_fd, guest_t *g, uint64_t iov_gva, int iovcnt); + +int64_t netlink_readv(int guest_fd, guest_t *g, uint64_t iov_gva, int iovcnt); + int64_t netlink_recv(int guest_fd, guest_t *g, uint64_t buf_gva, diff --git a/src/syscall/netlink.c b/src/syscall/netlink.c index 81deddd8..861e99c0 100644 --- a/src/syscall/netlink.c +++ b/src/syscall/netlink.c @@ -112,6 +112,13 @@ typedef struct { #define MAX_NETLINK_FDS 16 #define NETLINK_BUF_SIZE 8192 +/* Ceiling on one staged request. Linux bounds a send by sk_sndbuf and reports + * EMSGSIZE past it; this is that refusal against a fixed buffer. The largest + * request nl_process_request() reads is an RTM_GETLINK carrying an IFLA_IFNAME + * filter, an nlmsghdr and an ifinfomsg and an attribute holding IFNAMSIZ. + */ +#define NETLINK_REQ_MAX 512 + typedef struct { bool in_use; int guest_fd; /* Guest fd number */ @@ -621,9 +628,80 @@ static int nl_process_request(netlink_state_t *ns, return (ret < 0) ? -LINUX_EIO : 0; } -int64_t netlink_sendmsg(int guest_fd, guest_t *g, uint64_t msg_gva, int flags) +/* A staged guest iovec vector, on the caller's stack for the common count. */ +typedef struct { + linux_iovec_t stack[SYSCALL_IOV_STACK_MAX]; + linux_iovec_t *iov; + linux_iovec_t *heap; /* non-NULL only when iov was heap-allocated */ +} nl_iov_buf_t; + +/* Stage the iovcnt guest iovec entries at iov_gva into buf. + * + * Both directions want the entries themselves rather than the resolved host + * pointers host_iov_prepare() builds, since one netlink request and one + * response each span the whole vector and are staged through ns->buf. + * + * Returns 0, or a negative Linux errno. Pair every return with nl_iov_free(). + * iovcnt is bounded by the caller, whose spelling decides what an empty vector + * means. + */ +static int64_t nl_iov_stage(guest_t *g, + uint64_t iov_gva, + int iovcnt, + nl_iov_buf_t *buf) +{ + buf->iov = buf->stack; + buf->heap = NULL; + if (iovcnt > SYSCALL_IOV_STACK_MAX) { + buf->heap = malloc((size_t) iovcnt * sizeof(*buf->heap)); + if (!buf->heap) + return -LINUX_ENOMEM; + buf->iov = buf->heap; + } + + uint64_t total = 0; + for (int i = 0; i < iovcnt; i++) { + uint64_t entry_gva = iov_gva + (uint64_t) i * sizeof(*buf->iov); + if (guest_read_small(g, entry_gva, &buf->iov[i], sizeof(*buf->iov)) < 0) + return -LINUX_EFAULT; + if (!iov_total_add(total, buf->iov[i].iov_len, &total)) + return -LINUX_EINVAL; + } + return 0; +} + +static void nl_iov_free(nl_iov_buf_t *buf) +{ + free(buf->heap); + buf->heap = NULL; +} + +/* Bound msg_iovlen, which is uint64_t on Linux, before the int narrowing, so a + * 64-bit value whose low 32 bits fall inside the cap cannot slip past it. + * sys_sendmsg and sys_recvmsg refuse the same count the same way. + */ +static int64_t nl_msg_iovcnt(const linux_msghdr_t *mhdr, int *iovcnt) +{ + if (mhdr->msg_iovlen > SYSCALL_IOV_MAX) + return -LINUX_EINVAL; + *iovcnt = (int) mhdr->msg_iovlen; + return 0; +} + +/* The send half of sendmsg(2), sendto(2), write(2) and writev(2) on a netlink + * socket. + * + * One request spans the whole iovec, so it is gathered before it is parsed. + * A gathered length between one byte and one nlmsghdr transfers and does + * nothing: the loop in netlink_rcv_skb() is entered only from + * nlmsg_total_size(0) bytes up, and the send reports the byte count rather + * than an error. Measured against Linux 6.18 under qemu-aarch64. + */ +static int64_t netlink_send_iov(int guest_fd, + guest_t *g, + const linux_iovec_t *iov, + int iovcnt) { - (void) flags; pthread_mutex_lock(&nl_lock); netlink_state_t *ns = nl_find(guest_fd); if (!ns) { @@ -632,40 +710,57 @@ int64_t netlink_sendmsg(int guest_fd, guest_t *g, uint64_t msg_gva, int flags) } int64_t result; - - /* Parse the linux_msghdr_t to get the iovec */ - linux_msghdr_t mhdr; - if (guest_read_small(g, msg_gva, &mhdr, sizeof(mhdr)) < 0) { - result = -LINUX_EFAULT; - goto out; + uint64_t total = 0; + for (int i = 0; i < iovcnt; i++) { + if (iov[i].iov_len > UINT64_MAX - total) { + result = -LINUX_EINVAL; + goto out; + } + total += iov[i].iov_len; } - if (mhdr.msg_iovlen == 0) { - result = -LINUX_EINVAL; + /* Linux's netlink_sendmsg() refuses an empty message before it builds an + * skb, which is what a sendmsg or a write of nothing reports. No entries at + * all sums to nothing too. The writev spelling never arrives here empty: + * do_readv_writev() returns on a zero total above the socket, and + * netlink_writev() carries that rule. + */ + if (total == 0) { + result = -LINUX_ENODATA; goto out; } - struct { - uint64_t iov_base, iov_len; - } iov; - if (guest_read_small(g, mhdr.msg_iov, &iov, sizeof(iov)) < 0) { - result = -LINUX_EFAULT; + /* Ahead of the read, the order netlink_sendmsg() checks its length in: an + * oversized send is refused whatever its buffers hold. + */ + if (total > NETLINK_REQ_MAX) { + result = -LINUX_EMSGSIZE; goto out; } - if (iov.iov_len < (uint64_t) NLMSG_HDRLEN) { - result = -LINUX_EINVAL; + if (total < (uint64_t) NLMSG_HDRLEN) { + result = (int64_t) total; goto out; } - /* Copy the whole request: the dispatcher inspects filter attributes past - * the fixed nlmsghdr. + /* Every entry is read, so total is the byte count that was validated and + * parsed, and an unmapped entry anywhere in the vector is EFAULT rather + * than a report of bytes nothing looked at. An entry of no length reads + * nothing and validates nothing, which is what Linux does with it. + * + * A total of NLMSG_HDRLEN or more rules out an empty vector, so the loop + * always runs. req is zeroed anyway: cppcheck reads the loop as skippable + * and calls the parse below an uninitialized read. */ - uint8_t req[512]; - size_t rlen = (iov.iov_len < sizeof(req)) ? iov.iov_len : sizeof(req); - if (guest_read(g, iov.iov_base, req, rlen) < 0) { - result = -LINUX_EFAULT; - goto out; + uint8_t req[NETLINK_REQ_MAX] = {0}; + size_t rlen = 0; + for (int i = 0; i < iovcnt; i++) { + if (guest_read(g, iov[i].iov_base, req + rlen, + (size_t) iov[i].iov_len) < 0) { + result = -LINUX_EFAULT; + goto out; + } + rlen += (size_t) iov[i].iov_len; } bool was_empty = ns->buf_pos >= ns->buf_len; @@ -674,47 +769,62 @@ int64_t netlink_sendmsg(int guest_fd, guest_t *g, uint64_t msg_gva, int flags) if (was_empty && ns->buf_pos < ns->buf_len) netlink_signal_readable(ns); } - result = (ret < 0) ? ret : (int64_t) iov.iov_len; + result = (ret < 0) ? ret : (int64_t) total; out: pthread_mutex_unlock(&nl_lock); return result; } -/* sendto(2) on a netlink socket: a flat request buffer (no msghdr). */ int64_t netlink_send(int guest_fd, guest_t *g, uint64_t buf_gva, uint64_t len) { - pthread_mutex_lock(&nl_lock); - netlink_state_t *ns = nl_find(guest_fd); - if (!ns) { - pthread_mutex_unlock(&nl_lock); - return -LINUX_EBADF; - } - - int64_t result; - if (len < (uint64_t) NLMSG_HDRLEN) { - result = -LINUX_EINVAL; - goto out; - } + linux_iovec_t one = {.iov_base = buf_gva, .iov_len = len}; + return netlink_send_iov(guest_fd, g, &one, 1); +} - uint8_t req[512]; - size_t rlen = (len < sizeof(req)) ? len : sizeof(req); - if (guest_read(g, buf_gva, req, rlen) < 0) { - result = -LINUX_EFAULT; - goto out; - } +int64_t netlink_writev(int guest_fd, guest_t *g, uint64_t iov_gva, int iovcnt) +{ + if (!iov_count_ok(iovcnt)) + return -LINUX_EINVAL; - bool was_empty = ns->buf_pos >= ns->buf_len; - int ret = nl_process_request(ns, req, rlen); + nl_iov_buf_t buf; + int64_t ret = nl_iov_stage(g, iov_gva, iovcnt, &buf); if (ret == 0) { - if (was_empty && ns->buf_pos < ns->buf_len) - netlink_signal_readable(ns); + uint64_t total = 0; + for (int i = 0; i < iovcnt; i++) + total += buf.iov[i].iov_len; /* nl_iov_stage bounds the sum */ + + /* A vectored write carrying nothing stops in do_readv_writev() before + * the socket is reached, so it reports 0 where write(2) of nothing + * reports ENODATA. Measured against Linux 6.18 under qemu-aarch64. + */ + ret = total == 0 ? 0 : netlink_send_iov(guest_fd, g, buf.iov, iovcnt); } - result = (ret < 0) ? ret : (int64_t) len; + nl_iov_free(&buf); + return ret; +} -out: - pthread_mutex_unlock(&nl_lock); - return result; +int64_t netlink_sendmsg(int guest_fd, guest_t *g, uint64_t msg_gva, int flags) +{ + (void) flags; + linux_msghdr_t mhdr; + if (guest_read_small(g, msg_gva, &mhdr, sizeof(mhdr)) < 0) + return -LINUX_EFAULT; + + int iovcnt; + int64_t ret = nl_msg_iovcnt(&mhdr, &iovcnt); + if (ret < 0) + return ret; + + /* ___sys_sendmsg() carries an empty vector down to the socket rather than + * answering it, so netlink_send_iov() decides this one too. + */ + nl_iov_buf_t buf; + ret = nl_iov_stage(g, mhdr.msg_iov, iovcnt, &buf); + if (ret == 0) + ret = netlink_send_iov(guest_fd, g, buf.iov, iovcnt); + nl_iov_free(&buf); + return ret; } /* Block until the netlink receive buffer has data. Called with nl_lock held. @@ -820,16 +930,22 @@ static void nl_write_kernel_src(guest_t *g, guest_write_small(g, namelen_gva, &namelen, sizeof(namelen)); } -/* recvfrom(2) on a netlink socket: drain whole messages; write back a kernel - * sockaddr_nl (nl_pid 0) when src is requested. +/* The receive half of recvmsg(2), recvfrom(2), read(2) and readv(2) on a + * netlink socket: drain whole messages, filling every entry in turn. + * + * One response spans the whole iovec, so a first entry too small for it does + * not cap the transfer. nl_complete_span() bounds every spelling alike, so none + * of them hands out a message split across two calls. + * + * Returns the byte count, or a negative Linux errno. Takes nl_lock and releases + * it before returning. flags carries MSG_DONTWAIT; pass 0 for read(2), which + * only honors O_NONBLOCK. */ -int64_t netlink_recv(int guest_fd, - guest_t *g, - uint64_t buf_gva, - uint64_t len, - int flags, - uint64_t src_gva, - uint64_t addrlen_gva) +static int64_t netlink_recv_iov(int guest_fd, + guest_t *g, + const linux_iovec_t *iov, + int iovcnt, + int flags) { pthread_mutex_lock(&nl_lock); netlink_state_t *ns = nl_find(guest_fd); @@ -838,36 +954,76 @@ int64_t netlink_recv(int guest_fd, return -LINUX_EBADF; } - if (len == 0) { + uint64_t total = 0; + for (int i = 0; i < iovcnt; i++) { + if (iov[i].iov_len > UINT64_MAX - total) { + pthread_mutex_unlock(&nl_lock); + return -LINUX_EINVAL; + } + total += iov[i].iov_len; + } + + if (total == 0) { pthread_mutex_unlock(&nl_lock); return 0; } - /* Wait for data to become available, blocking on the host pipe read end - * unless MSG_DONTWAIT or O_NONBLOCK is set. - */ int64_t werr = nl_wait_readable_locked(ns, guest_fd, flags); if (werr < 0) return werr; size_t avail = ns->buf_len - ns->buf_pos; - size_t to_copy = (avail < len) ? avail : len; + size_t to_copy = (avail < total) ? avail : (size_t) total; size_t msg_end = nl_complete_span(ns, to_copy); - if (guest_write(g, buf_gva, ns->buf + ns->buf_pos, msg_end) < 0) { - pthread_mutex_unlock(&nl_lock); - return -LINUX_EFAULT; + size_t done = 0; + for (int i = 0; i < iovcnt && done < msg_end; i++) { + size_t remain = msg_end - done; + size_t chunk = + (iov[i].iov_len < remain) ? (size_t) iov[i].iov_len : remain; + if (chunk == 0) + continue; + + /* The count is what landed, not whole entries: guest memory is copied + * chunk by chunk and a chunk that faults still places the bytes ahead + * of it, which is what copy_to_iter() counts. + */ + size_t moved = guest_write_partial(g, iov[i].iov_base, + ns->buf + ns->buf_pos, chunk); + ns->buf_pos += moved; + done += moved; + if (moved < chunk) { + pthread_mutex_unlock(&nl_lock); + /* Bytes already placed are transferred; reporting EFAULT over them + * would lose them, since buf_pos has moved past. + */ + return done ? (int64_t) done : -LINUX_EFAULT; + } } - ns->buf_pos += msg_end; if (ns->buf_pos >= ns->buf_len) netlink_clear_readable(ns); - if (src_gva && addrlen_gva) - nl_write_kernel_src(g, src_gva, addrlen_gva); - pthread_mutex_unlock(&nl_lock); - return (int64_t) msg_end; + return (int64_t) done; +} + +/* recvfrom(2) on a netlink socket: write back a kernel sockaddr_nl (nl_pid 0) + * when src is requested. + */ +int64_t netlink_recv(int guest_fd, + guest_t *g, + uint64_t buf_gva, + uint64_t len, + int flags, + uint64_t src_gva, + uint64_t addrlen_gva) +{ + linux_iovec_t one = {.iov_base = buf_gva, .iov_len = len}; + int64_t ret = netlink_recv_iov(guest_fd, g, &one, 1, flags); + if (ret >= 0 && src_gva && addrlen_gva) + nl_write_kernel_src(g, src_gva, addrlen_gva); + return ret; } /* getsockname(2) on a netlink socket: returns the bound/auto-assigned pid. */ @@ -905,113 +1061,53 @@ int64_t netlink_getsockname(int guest_fd, int64_t netlink_recvmsg(int guest_fd, guest_t *g, uint64_t msg_gva, int flags) { - pthread_mutex_lock(&nl_lock); - netlink_state_t *ns = nl_find(guest_fd); - if (!ns) { - pthread_mutex_unlock(&nl_lock); - return -LINUX_EBADF; - } - - /* Parse msghdr to get iovec */ linux_msghdr_t mhdr; - if (guest_read_small(g, msg_gva, &mhdr, sizeof(mhdr)) < 0) { - pthread_mutex_unlock(&nl_lock); + if (guest_read_small(g, msg_gva, &mhdr, sizeof(mhdr)) < 0) return -LINUX_EFAULT; - } - if (mhdr.msg_iovlen == 0) { - pthread_mutex_unlock(&nl_lock); - return 0; - } + int iovcnt; + int64_t ret = nl_msg_iovcnt(&mhdr, &iovcnt); + if (ret < 0) + return ret; - struct { - uint64_t iov_base, iov_len; - } iov; - if (guest_read_small(g, mhdr.msg_iov, &iov, sizeof(iov)) < 0) { - pthread_mutex_unlock(&nl_lock); - return -LINUX_EFAULT; - } - - if (iov.iov_len == 0) { - pthread_mutex_unlock(&nl_lock); - return 0; - } - - /* Wait for data to become available, blocking on the host pipe read end - * unless MSG_DONTWAIT or O_NONBLOCK is set. - */ - int64_t werr = nl_wait_readable_locked(ns, guest_fd, flags); - if (werr < 0) - return werr; + nl_iov_buf_t buf; + ret = nl_iov_stage(g, mhdr.msg_iov, iovcnt, &buf); + if (ret == 0) + ret = netlink_recv_iov(guest_fd, g, buf.iov, iovcnt, flags); + nl_iov_free(&buf); + if (ret < 0) + return ret; - size_t avail = ns->buf_len - ns->buf_pos; - size_t to_copy = (avail < iov.iov_len) ? avail : iov.iov_len; - size_t msg_end = nl_complete_span(ns, to_copy); - - if (guest_write(g, iov.iov_base, ns->buf + ns->buf_pos, msg_end) < 0) { - pthread_mutex_unlock(&nl_lock); - return -LINUX_EFAULT; - } - - ns->buf_pos += msg_end; - - if (ns->buf_pos >= ns->buf_len) - netlink_clear_readable(ns); - - /* Write back sockaddr_nl if caller provided msg_name. msg_namelen sits at - * offset 8 in the msghdr (after the 8-byte msg_name pointer). - */ if (mhdr.msg_name && mhdr.msg_namelen >= sizeof(sockaddr_nl_t)) - nl_write_kernel_src(g, mhdr.msg_name, msg_gva + 8); + nl_write_kernel_src(g, mhdr.msg_name, + msg_gva + offsetof(linux_msghdr_t, msg_namelen)); - /* Clear msg_flags and msg_controllen */ int32_t zero_flags = 0; guest_write_small(g, msg_gva + offsetof(linux_msghdr_t, msg_flags), &zero_flags, sizeof(zero_flags)); uint64_t zero_controllen = 0; guest_write_small(g, msg_gva + offsetof(linux_msghdr_t, msg_controllen), &zero_controllen, sizeof(zero_controllen)); + return ret; +} - pthread_mutex_unlock(&nl_lock); - return (int64_t) msg_end; +int64_t netlink_readv(int guest_fd, guest_t *g, uint64_t iov_gva, int iovcnt) +{ + if (!iov_count_ok(iovcnt)) + return -LINUX_EINVAL; + + nl_iov_buf_t buf; + int64_t ret = nl_iov_stage(g, iov_gva, iovcnt, &buf); + if (ret == 0) + ret = netlink_recv_iov(guest_fd, g, buf.iov, iovcnt, 0); + nl_iov_free(&buf); + return ret; } int64_t netlink_read(int guest_fd, guest_t *g, uint64_t buf_gva, uint64_t count) { - pthread_mutex_lock(&nl_lock); - netlink_state_t *ns = nl_find(guest_fd); - if (!ns) { - pthread_mutex_unlock(&nl_lock); - return -LINUX_EBADF; - } - - if (count == 0) { - pthread_mutex_unlock(&nl_lock); - return 0; - } - - /* Wait for data to become available, blocking on the host pipe read end - * unless O_NONBLOCK is set. read(2) has no per-call MSG_DONTWAIT. - */ - int64_t werr = nl_wait_readable_locked(ns, guest_fd, 0); - if (werr < 0) - return werr; - - size_t avail = ns->buf_len - ns->buf_pos; - size_t to_copy = (avail < count) ? avail : count; - - if (guest_write(g, buf_gva, ns->buf + ns->buf_pos, to_copy) < 0) { - pthread_mutex_unlock(&nl_lock); - return -LINUX_EFAULT; - } - - ns->buf_pos += to_copy; - - if (ns->buf_pos >= ns->buf_len) - netlink_clear_readable(ns); - - pthread_mutex_unlock(&nl_lock); - return (int64_t) to_copy; + linux_iovec_t one = {.iov_base = buf_gva, .iov_len = count}; + return netlink_recv_iov(guest_fd, g, &one, 1, 0); } static void netlink_close(int guest_fd) diff --git a/tests/test-netlink.c b/tests/test-netlink.c index b49f27cb..ff4f9ef3 100644 --- a/tests/test-netlink.c +++ b/tests/test-netlink.c @@ -1,5 +1,6 @@ /* - * Exercise the AF_NETLINK getsockname/sendto/recvfrom dispatch paths. + * Exercise the AF_NETLINK getsockname, send and receive dispatch paths in every + * spelling: sendto, sendmsg, write, writev, recvfrom, recvmsg, read and readv. * * Copyright 2026 elfuse contributors * Copyright 2025 Moritz Angermann, zw3rk pte. ltd. @@ -8,9 +9,12 @@ * Regression guard for the netlink socket emulation. Before getsockname, * sendto, and recvfrom were routed to the netlink handlers, these calls fell * through to the host socket syscalls on the underlying pipe fd and failed with - * ENOTSOCK (errno 88), which in turn broke glibc getifaddrs(). The test drives - * each of the three syscalls directly against a NETLINK_ROUTE socket and then - * validates the end-to-end getifaddrs() path that originally regressed. + * ENOTSOCK (errno 88), which in turn broke glibc getifaddrs(). write() and + * writev() kept falling through afterwards and reached the read end of the + * readiness pipe, which reports EBADF and broke senders that use write() in + * place of sendto(), busybox "ip" among them. The test drives each syscall + * directly against a NETLINK_ROUTE socket and then validates the end-to-end + * getifaddrs() path that originally regressed. * * The assertions hold for both the elfuse emulation and a real Linux kernel * (the test matrix runs the same binary under qemu-aarch64), so only @@ -19,14 +23,24 @@ #include #include +#include #include #include #include #include +#include #include #include +/* Upper bound on how long one dump message may take to show up. */ +#define DUMP_POLL_MS 5000 + +/* First readv() entry: smaller than any RTM_NEWLINK message, so a receive that + * ignores the entries past it cannot return more than this. + */ +#define READV_HEAD 16 + static int pass, fail; #define CHECK(cond, msg) \ @@ -46,6 +60,40 @@ struct getlink_req { struct ifinfomsg ifi; }; +/* Drain one dump and report whether it carried an RTM_NEWLINK. Both the + * emulation and a real kernel end a dump with NLMSG_DONE. + * + * Every receive waits through poll() first. A request that never reached the + * netlink layer leaves nothing to read, and a bare blocking recvfrom() would + * turn that regression into a hung test run instead of a failed assertion. + */ +static int dump_has_newlink(int fd) +{ + int saw_newlink = 0, saw_done = 0; + for (int iter = 0; iter < 64 && !saw_done; iter++) { + struct pollfd pfd = {.fd = fd, .events = POLLIN}; + if (poll(&pfd, 1, DUMP_POLL_MS) <= 0) + break; + + char buf[8192]; + struct sockaddr_nl src = {0}; + socklen_t srclen = sizeof(src); + ssize_t n = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr *) &src, + &srclen); + if (n <= 0) + break; + for (struct nlmsghdr *nlh = (struct nlmsghdr *) buf; + NLMSG_OK(nlh, (unsigned) n); nlh = NLMSG_NEXT(nlh, n)) { + if (nlh->nlmsg_type == RTM_NEWLINK) + saw_newlink = 1; + else if (nlh->nlmsg_type == NLMSG_DONE || + nlh->nlmsg_type == NLMSG_ERROR) + saw_done = 1; + } + } + return saw_newlink; +} + int main(void) { int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); @@ -113,9 +161,129 @@ int main(void) CHECK(saw_newlink, "recvfrom() returns at least one RTM_NEWLINK"); CHECK(src_ok, "recvfrom() fills an AF_NETLINK source address"); + /* 4. write(): Linux treats it as sendto() with no explicit destination, so + * a request sent this way has to reach the same dump. busybox "ip" sends + * its rtnetlink requests with write(). + */ + req.nlh.nlmsg_seq = 2; + ssize_t wrote = write(fd, &req, req.nlh.nlmsg_len); + CHECK(wrote == (ssize_t) req.nlh.nlmsg_len, + "write(RTM_GETLINK) accepts the request"); + CHECK(dump_has_newlink(fd), "write() request produces an RTM_NEWLINK dump"); + + /* 5. Sends carrying nothing. netlink_sendmsg() refuses an empty message, + * but a vectored write never reaches it: do_readv_writev() returns 0 for a + * zero total first. The asymmetry is the point of testing both. + */ + errno = 0; + CHECK(write(fd, &req, 0) == -1 && errno == ENODATA, + "write() of zero bytes fails with ENODATA"); + struct iovec empty[2] = { + {.iov_base = &req, .iov_len = 0}, + {.iov_base = &req, .iov_len = 0}, + }; + CHECK(writev(fd, empty, 2) == 0, "writev() of zero bytes returns 0"); + + /* 6. writev(): the request split mid-header across two entries. A receiver + * that reads only the first entry gets a runt it cannot parse, so this + * fails unless the entries are gathered into one request. + */ + req.nlh.nlmsg_seq = 3; + struct iovec iov[2] = { + {.iov_base = &req, .iov_len = 8}, + {.iov_base = (char *) &req + 8, .iov_len = req.nlh.nlmsg_len - 8}, + }; + ssize_t wrotev = writev(fd, iov, 2); + CHECK(wrotev == (ssize_t) req.nlh.nlmsg_len, + "writev(RTM_GETLINK) accepts a split request"); + CHECK(dump_has_newlink(fd), + "writev() gathers the split request into one dump"); + + /* 7. readv(): a first entry far too small for the response. A receive that + * stops at entry 0 cannot exceed READV_HEAD bytes. The entries are adjacent + * halves of one buffer, so the scattered bytes stay contiguous and parse + * as an ordinary message stream. + */ + req.nlh.nlmsg_seq = 4; + sent = sendto(fd, &req, req.nlh.nlmsg_len, 0, (struct sockaddr *) &kernel, + sizeof(kernel)); + CHECK(sent == (ssize_t) req.nlh.nlmsg_len, + "sendto() before the readv() drain accepts the request"); + + static char rbuf[8192]; + struct iovec riov[2] = { + {.iov_base = rbuf, .iov_len = READV_HEAD}, + {.iov_base = rbuf + READV_HEAD, .iov_len = sizeof(rbuf) - READV_HEAD}, + }; + struct pollfd rpfd = {.fd = fd, .events = POLLIN}; + ssize_t rn = poll(&rpfd, 1, DUMP_POLL_MS) > 0 ? readv(fd, riov, 2) : -1; + CHECK(rn > (ssize_t) READV_HEAD, "readv() fills past the first entry"); + + int rv_newlink = 0; + for (struct nlmsghdr *nlh = (struct nlmsghdr *) rbuf; + rn > 0 && NLMSG_OK(nlh, (unsigned) rn); nlh = NLMSG_NEXT(nlh, rn)) + if (nlh->nlmsg_type == RTM_NEWLINK) + rv_newlink = 1; + CHECK(rv_newlink, "the scattered readv() bytes parse as RTM_NEWLINK"); + + dump_has_newlink(fd); /* leave the socket idle for the next case */ + + /* 8. sendmsg(): the same split request through msg_iov. All four send + * spellings gather, so a first entry shorter than an nlmsghdr is a split + * request rather than a malformed one. + */ + req.nlh.nlmsg_seq = 5; + struct iovec siov[2] = { + {.iov_base = &req, .iov_len = 8}, + {.iov_base = (char *) &req + 8, .iov_len = req.nlh.nlmsg_len - 8}, + }; + struct msghdr smsg = { + .msg_name = &kernel, + .msg_namelen = sizeof(kernel), + .msg_iov = siov, + .msg_iovlen = 2, + }; + CHECK(sendmsg(fd, &smsg, 0) == (ssize_t) req.nlh.nlmsg_len, + "sendmsg() gathers a split request"); + + /* 9. recvmsg(): a first entry far too small for the response, as in 7. */ + static char mbuf[8192]; + struct iovec mriov[2] = { + {.iov_base = mbuf, .iov_len = READV_HEAD}, + {.iov_base = mbuf + READV_HEAD, .iov_len = sizeof(mbuf) - READV_HEAD}, + }; + struct sockaddr_nl from = {0}; + struct msghdr rmsg = { + .msg_name = &from, + .msg_namelen = sizeof(from), + .msg_iov = mriov, + .msg_iovlen = 2, + }; + struct pollfd mpfd = {.fd = fd, .events = POLLIN}; + ssize_t mn = poll(&mpfd, 1, DUMP_POLL_MS) > 0 ? recvmsg(fd, &rmsg, 0) : -1; + CHECK(mn > (ssize_t) READV_HEAD, "recvmsg() fills past the first entry"); + + int rm_newlink = 0; + for (struct nlmsghdr *nlh = (struct nlmsghdr *) mbuf; + mn > 0 && NLMSG_OK(nlh, (unsigned) mn); nlh = NLMSG_NEXT(nlh, mn)) + if (nlh->nlmsg_type == RTM_NEWLINK) + rm_newlink = 1; + CHECK(rm_newlink, "the scattered recvmsg() bytes parse as RTM_NEWLINK"); + + dump_has_newlink(fd); + + /* 10. sendmsg() carrying nothing. ___sys_sendmsg() hands an empty vector + * down to the socket instead of answering it above, so this reports what + * write(2) of nothing reports and not what writev(2) does. + */ + errno = 0; + struct msghdr emsg = {.msg_iov = siov, .msg_iovlen = 0}; + CHECK(sendmsg(fd, &emsg, 0) == -1 && errno == ENODATA, + "sendmsg() of an empty iovec fails with ENODATA"); + close(fd); - /* 4. End-to-end: glibc getifaddrs() drives getsockname + sendto + recv + /* 11. End-to-end: glibc getifaddrs() drives getsockname + sendto + recv * internally. This is the exact call that regressed with ENOTSOCK. */ struct ifaddrs *ifa = NULL;