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;