[PATCH 00/12] vhost-user,udp: Handle multiple iovec entries per virtqueue element
Some virtio-net drivers (notably iPXE) provide descriptors where the vnet header and the frame payload are in separate buffers, resulting in two iovec entries per virtqueue element. Currently, the RX (host to guest) path assumes a single iovec per element, which triggers: ASSERTION FAILED in virtqueue_map_desc (virtio.c:403): num_sg < max_num_sg This series reworks the UDP vhost-user receive path to support multiple iovec entries per element, fixing the iPXE crash. This series only addresses the UDP path. TCP vhost-user will be updated to use multi-iov elements in a subsequent series. Laurent Vivier (12): iov: Add iov_tail_truncate() and iov_tail_zero_end() vhost-user: Use ARRAY_SIZE(elem) instead of VIRTQUEUE_MAX_SIZE udp_vu: Use iov_tail to manage virtqueue buffers udp_vu: Move virtqueue management from udp_vu_sock_recv() to its caller iov: Add IOV_PUT_HEADER() to write header data back to iov_tail udp: Pass iov_tail to udp_update_hdr4()/udp_update_hdr6() udp_vu: Use iov_tail in udp_vu_prepare() vu_common: Pass iov_tail to vu_set_vnethdr() vu_common: Accept explicit iovec counts in vu_set_element() vu_common: Accept explicit iovec count per element in vu_init_elem() vu_common: Prepare to use multibuffer with guest RX vhost-user,udp: Use 2 iovec entries per element iov.c | 58 +++++++++++++++ iov.h | 16 +++- tcp_vu.c | 18 +++-- udp.c | 65 ++++++++--------- udp_internal.h | 10 ++- udp_vu.c | 194 +++++++++++++++++++++++++------------------------ vu_common.c | 82 +++++++++++++-------- vu_common.h | 20 +++-- 8 files changed, 282 insertions(+), 181 deletions(-) -- 2.53.0
When passing the element count to vu_init_elem(), vu_collect(), or using
it as a loop bound, use ARRAY_SIZE(elem) instead of the VIRTQUEUE_MAX_SIZE.
No functional change.
Signed-off-by: Laurent Vivier
Replace direct iovec pointer arithmetic in UDP vhost-user handling with
iov_tail operations introduced in the previous commit.
udp_vu_sock_recv() now takes a struct iov_tail and returns the received
data length rather than the number of iov entries used. It uses
iov_drop_header() to skip past L2/L3/L4 headers before receiving socket
data, iov_tail_truncate() to trim unused buffer space, and
iov_tail_zero_end() to zero-pad short frames instead of vu_pad().
udp_vu_prepare() and udp_vu_csum() take a const struct iov_tail instead
of referencing the file-scoped iov_vu array directly, making data flow
explicit.
udp_vu_csum() uses iov_drop_header() and IOV_REMOVE_HEADER() to locate
the UDP header and payload, replacing manual offset calculations via
vu_payloadv4()/vu_payloadv6().
Signed-off-by: Laurent Vivier
Add a counterpart to IOV_PEEK_HEADER() that writes header data back
to an iov_tail after modification. If the header pointer matches the
original iov buffer location, it only advances the offset. Otherwise,
it copies the data using iov_from_buf().
Signed-off-by: Laurent Vivier
Change udp_update_hdr4() and udp_update_hdr6() to take a separate
struct udphdr pointer and an iov_tail for the payload, instead of a
struct udp_payload_t pointer and an explicit data length.
This decouples the header update functions from the udp_payload_t memory
layout, which assumes all headers and data sit in a single contiguous
buffer. The vhost-user path uses virtqueue-provided scatter-gather
buffers where this assumption does not hold; passing an iov_tail lets
both the tap path and the vhost-user path share the same functions
without casting through layout-specific helpers.
Signed-off-by: Laurent Vivier
Rework udp_vu_prepare() to use IOV_REMOVE_HEADER() and IOV_PUT_HEADER()
to walk through Ethernet, IP and UDP headers instead of the layout-specific
helpers (vu_eth(), vu_ip(), vu_payloadv4(), vu_payloadv6()) that assume a
contiguous buffer. The payload length is now implicit in the iov_tail, so
drop the dlen parameter.
Signed-off-by: Laurent Vivier
udp_vu_sock_recv() currently mixes two concerns: receiving data from the
socket and managing virtqueue buffers (collecting, rewinding, releasing).
This makes the function harder to reason about and couples socket I/O
with virtqueue state.
Move all virtqueue operations, vu_collect(), vu_init_elem(),
vu_queue_rewind(), and the queue-readiness check, into
udp_vu_sock_to_tap(), which is the only caller. This turns
udp_vu_sock_recv() into a pure socket receive function that simply reads
into the provided iov_tail and adjusts its length.
Signed-off-by: Laurent Vivier
Refactor vu_set_vnethdr() to take an iov_tail pointer instead of a
direct pointer to the virtio_net_hdr_mrg_rxbuf structure.
This makes the function use IOV_PEEK_HEADER() and IOV_PUT_HEADER()
to read and write the virtio-net header through the iov_tail abstraction.
Signed-off-by: Laurent Vivier
Previously, vu_set_element() derived the number of iovec entries from
whether the pointer was NULL or not (using !!out_sg and !!in_sg). This
implicitly limited each virtqueue element to at most one iovec per
direction.
Change the function signature to accept explicit out_num and in_num
parameters, allowing callers to specify multiple iovec entries per
element when needed. Update all existing call sites to pass the
equivalent values (0 for NULL pointers, 1 for valid pointers).
No functional change.
Signed-off-by: Laurent Vivier
Extend vu_init_elem() to accept an iov_per_elem parameter specifying
how many iovec entries to assign to each virtqueue element. The iov
array is now strided by iov_per_elem rather than 1.
Update all callers to pass 1, preserving existing behavior.
No functional change.
Signed-off-by: Laurent Vivier
1b95bd6fa114 ("vhost_user: fix multibuffer from linux") introduces
multibuffer with TX (from the guest), but with iPXE we need to handle
also multibuffer for RX (to the guest). This patch makes the parameter
generic and global.
No functional change.
Signed-off-by: Laurent Vivier
iPXE places the vnet header in one virtqueue descriptor and the payload
in another. When passt maps these descriptors, it needs two iovecs per
virtqueue element to handle this layout.
Without this, passt crashes with:
ASSERTION FAILED in virtqueue_map_desc (virtio.c:403): num_sg < max_num_sg
Signed-off-by: Laurent Vivier
iov_tail_truncate() truncates a tail so it contains at most a given
number of bytes from the current position, adjusting both the last
iovec entry and the buffer count.
iov_tail_zero_end() zero-fills all backing buffer bytes beyond a given
number of leading bytes from the current tail position and can be
used to clear trailing padding in fixed-size frames.
Signed-off-by: Laurent Vivier
On Fri, Feb 27, 2026 at 03:03:19PM +0100, Laurent Vivier wrote:
iov_tail_truncate() truncates a tail so it contains at most a given number of bytes from the current position, adjusting both the last iovec entry and the buffer count.
iov_tail_zero_end() zero-fills all backing buffer bytes beyond a given number of leading bytes from the current tail position and can be used to clear trailing padding in fixed-size frames.
Signed-off-by: Laurent Vivier
--- iov.c | 39 +++++++++++++++++++++++++++++++++++++++ iov.h | 2 ++ 2 files changed, 41 insertions(+) diff --git a/iov.c b/iov.c index ad726daa4cd8..cb4d6fef5567 100644 --- a/iov.c +++ b/iov.c @@ -170,6 +170,45 @@ bool iov_tail_prune(struct iov_tail *tail) return !!tail->cnt; }
+/** + * iov_tail_truncate() - Truncate tail to at most @size bytes + * @tail: IO vector tail (modified in place, including backing iovecs) + * @size: Maximum number of bytes to keep, relative to current tail offset + */ +/* cppcheck-suppress unusedFunction */ +void iov_tail_truncate(struct iov_tail *tail, size_t size) +{ + size_t i, off; + + i = iov_skip_bytes(tail->iov, tail->cnt, tail->off + size, &off); + + if (i < tail->cnt) { + struct iovec *last = (struct iovec *)&tail->iov[i];
This cast makes me pretty nervous. Up until now, a global property of iov_tail has been that it never alters the underlying iovec array: the tail is just a view into an immutable underlying vector. Maybe it's worth changing that, but we should do so explicitly if we do, which would suggest to me removing the const from struct iov_tail, rather than making a const-discarding cast here.
+ + last->iov_len = off; + tail->cnt = i + !!off; + } +} + +/** + * iov_tail_zero_end() - Zero-fill tail bytes beyond @size + * @tail: IO vector tail (backing buffers modified in place) + * @size: Number of leading bytes to preserve + */ +/* cppcheck-suppress unusedFunction */ +void iov_tail_zero_end(struct iov_tail *tail, size_t size) +{ + size_t i, off; + + i = iov_skip_bytes(tail->iov, tail->cnt, tail->off + size, &off); + + for (; i < tail->cnt; i++) { + memset((char *)tail->iov[i].iov_base + off, 0, + tail->iov[i].iov_len - off); + off = 0; + } +} + /** * iov_tail_size() - Calculate the total size of an IO vector tail * @tail: IO vector tail diff --git a/iov.h b/iov.h index d2184bfd12bd..a7b873d58134 100644 --- a/iov.h +++ b/iov.h @@ -89,6 +89,8 @@ void *iov_peek_header_(struct iov_tail *tail, void *v, size_t len, size_t align) void *iov_remove_header_(struct iov_tail *tail, void *v, size_t len, size_t align); ssize_t iov_tail_clone(struct iovec *dst_iov, size_t dst_iov_cnt, struct iov_tail *tail); +void iov_tail_truncate(struct iov_tail *tail, size_t size); +void iov_tail_zero_end(struct iov_tail *tail, size_t size);
/** * IOV_PEEK_HEADER() - Get typed pointer to a header from an IOV tail -- 2.53.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Fri, Feb 27, 2026 at 03:03:21PM +0100, Laurent Vivier wrote:
Replace direct iovec pointer arithmetic in UDP vhost-user handling with iov_tail operations introduced in the previous commit.
udp_vu_sock_recv() now takes a struct iov_tail and returns the received data length rather than the number of iov entries used. It uses iov_drop_header() to skip past L2/L3/L4 headers before receiving socket data, iov_tail_truncate() to trim unused buffer space, and iov_tail_zero_end() to zero-pad short frames instead of vu_pad().
udp_vu_prepare() and udp_vu_csum() take a const struct iov_tail instead of referencing the file-scoped iov_vu array directly, making data flow explicit.
udp_vu_csum() uses iov_drop_header() and IOV_REMOVE_HEADER() to locate the UDP header and payload, replacing manual offset calculations via vu_payloadv4()/vu_payloadv6().
Signed-off-by: Laurent Vivier
--- iov.c | 2 - udp_vu.c | 121 +++++++++++++++++++++++++++---------------------------- 2 files changed, 60 insertions(+), 63 deletions(-) diff --git a/iov.c b/iov.c index cb4d6fef5567..8836305fb701 100644 --- a/iov.c +++ b/iov.c @@ -175,7 +175,6 @@ bool iov_tail_prune(struct iov_tail *tail) * @tail: IO vector tail (modified in place, including backing iovecs) * @size: Maximum number of bytes to keep, relative to current tail offset */ -/* cppcheck-suppress unusedFunction */ void iov_tail_truncate(struct iov_tail *tail, size_t size) { size_t i, off; @@ -195,7 +194,6 @@ void iov_tail_truncate(struct iov_tail *tail, size_t size) * @tail: IO vector tail (backing buffers modified in place) * @size: Number of leading bytes to preserve */ -/* cppcheck-suppress unusedFunction */ void iov_tail_zero_end(struct iov_tail *tail, size_t size) { size_t i, off; diff --git a/udp_vu.c b/udp_vu.c index 6f6477f7d046..8f4d0aedac10 100644 --- a/udp_vu.c +++ b/udp_vu.c @@ -59,21 +59,23 @@ static size_t udp_vu_hdrlen(bool v6) /** * udp_vu_sock_recv() - Receive datagrams from socket into vhost-user buffers * @c: Execution context + * @data: IO vector tail for the frame (modified on output) * @vq: virtqueue to use to receive data * @s: Socket to receive from * @v6: Set for IPv6 connections - * @dlen: Size of received data (output) * - * Return: number of iov entries used to store the datagram, 0 if the datagram + * Return: size of received data, 0 if the datagram * was discarded because the virtqueue is not ready, -1 on error */ -static int udp_vu_sock_recv(const struct ctx *c, struct vu_virtq *vq, int s, - bool v6, ssize_t *dlen) +static ssize_t udp_vu_sock_recv(const struct ctx *c, struct iov_tail *data, + struct vu_virtq *vq, int s, bool v6) { const struct vu_dev *vdev = c->vdev; - int iov_cnt, idx, iov_used; - size_t off, hdrlen, l2len; struct msghdr msg = { 0 }; + struct iov_tail payload; + size_t hdrlen; + ssize_t dlen; + int iov_cnt;
ASSERT(!c->no_udp);
@@ -83,82 +85,77 @@ static int udp_vu_sock_recv(const struct ctx *c, struct vu_virtq *vq, int s, if (recvmsg(s, &msg, MSG_DONTWAIT) < 0) debug_perror("Failed to discard datagram");
+ data->cnt = 0; return 0; }
/* compute L2 header length */ hdrlen = udp_vu_hdrlen(v6);
- vu_init_elem(elem, iov_vu, ARRAY_SIZE(elem)); + vu_init_elem(elem, (struct iovec *)data->iov, data->cnt);
iov_cnt = vu_collect(vdev, vq, elem, ARRAY_SIZE(elem), IP_MAX_MTU + ETH_HLEN + VNET_HLEN, NULL); if (iov_cnt == 0) return -1;
+ data->cnt = iov_cnt;
Does something limit iov_cnt to be <= data->cnt? If so, it's not very obvious from here. If not, then the line above is clearly dangerous.
+ /* reserve space for the headers */ - ASSERT(iov_vu[0].iov_len >= MAX(hdrlen, ETH_ZLEN + VNET_HLEN)); - iov_vu[0].iov_base = (char *)iov_vu[0].iov_base + hdrlen; - iov_vu[0].iov_len -= hdrlen; + ASSERT(iov_tail_size(data) >= MAX(hdrlen, ETH_ZLEN + VNET_HLEN));
- /* read data from the socket */ - msg.msg_iov = iov_vu; - msg.msg_iovlen = iov_cnt; + payload = *data; + iov_drop_header(&payload, hdrlen); + + struct iovec msg_iov[payload.cnt]; + msg.msg_iov = msg_iov; + msg.msg_iovlen = iov_tail_clone(msg.msg_iov, payload.cnt, &payload);
- *dlen = recvmsg(s, &msg, 0); - if (*dlen < 0) { + /* read data from the socket */ + dlen = recvmsg(s, &msg, 0); + if (dlen < 0) { vu_queue_rewind(vq, iov_cnt); return -1; }
- /* restore the pointer to the headers address */ - iov_vu[0].iov_base = (char *)iov_vu[0].iov_base - hdrlen; - iov_vu[0].iov_len += hdrlen; - - /* count the numbers of buffer filled by recvmsg() */ - idx = iov_skip_bytes(iov_vu, iov_cnt, *dlen + hdrlen, &off); - - /* adjust last iov length */ - if (idx < iov_cnt) - iov_vu[idx].iov_len = off; - iov_used = idx + !!off; - - /* pad frame to 60 bytes: first buffer is at least ETH_ZLEN long */ - l2len = *dlen + hdrlen - VNET_HLEN; - vu_pad(&iov_vu[0], l2len); + iov_tail_truncate(data, MAX(dlen + hdrlen, ETH_ZLEN + VNET_HLEN)); + iov_tail_zero_end(data, dlen + hdrlen); + iov_tail_truncate(data, dlen + hdrlen);
Zeroing the tail, then truncating it seems kind of weird.
- vu_set_vnethdr(vdev, iov_vu[0].iov_base, iov_used); + vu_set_vnethdr(vdev, data->iov[0].iov_base, data->cnt);
/* release unused buffers */ - vu_queue_rewind(vq, iov_cnt - iov_used); + vu_queue_rewind(vq, iov_cnt - data->cnt);
- return iov_used; + return dlen; }
/** * udp_vu_prepare() - Prepare the packet header * @c: Execution context + * @data: IO vector tail for the frame * @toside: Address information for one side of the flow * @dlen: Packet data length * * Return: Layer-4 length */ -static size_t udp_vu_prepare(const struct ctx *c, +static size_t udp_vu_prepare(const struct ctx *c, const struct iov_tail *data, const struct flowside *toside, ssize_t dlen) { + const struct iovec *iov = data->iov; struct ethhdr *eh; size_t l4len;
/* ethernet header */ - eh = vu_eth(iov_vu[0].iov_base); + eh = vu_eth(iov[0].iov_base);
Now that you have an iov_tail, could you clone it and use IOV_{PEEK,REMOVE}_HEADER() instead of the more specific vu_eth(), vu_ip() etc?
memcpy(eh->h_dest, c->guest_mac, sizeof(eh->h_dest)); memcpy(eh->h_source, c->our_tap_mac, sizeof(eh->h_source));
/* initialize header */ if (inany_v4(&toside->eaddr) && inany_v4(&toside->oaddr)) { - struct iphdr *iph = vu_ip(iov_vu[0].iov_base); - struct udp_payload_t *bp = vu_payloadv4(iov_vu[0].iov_base); + struct iphdr *iph = vu_ip(iov[0].iov_base); + struct udp_payload_t *bp = vu_payloadv4(iov[0].iov_base);
eh->h_proto = htons(ETH_P_IP);
@@ -166,8 +163,8 @@ static size_t udp_vu_prepare(const struct ctx *c,
l4len = udp_update_hdr4(iph, bp, toside, dlen, true); } else { - struct ipv6hdr *ip6h = vu_ip(iov_vu[0].iov_base); - struct udp_payload_t *bp = vu_payloadv6(iov_vu[0].iov_base); + struct ipv6hdr *ip6h = vu_ip(iov[0].iov_base); + struct udp_payload_t *bp = vu_payloadv6(iov[0].iov_base);
eh->h_proto = htons(ETH_P_IPV6);
@@ -182,25 +179,25 @@ static size_t udp_vu_prepare(const struct ctx *c, /** * udp_vu_csum() - Calculate and set checksum for a UDP packet * @toside: Address information for one side of the flow - * @iov_used: Number of used iov_vu items + * @data: IO vector tail for the frame */ -static void udp_vu_csum(const struct flowside *toside, int iov_used) +static void udp_vu_csum(const struct flowside *toside, + const struct iov_tail *data) { const struct in_addr *src4 = inany_v4(&toside->oaddr); const struct in_addr *dst4 = inany_v4(&toside->eaddr); - char *base = iov_vu[0].iov_base; - struct udp_payload_t *bp; - struct iov_tail data; + struct iov_tail payload = *data; + struct udphdr *uh, uh_storage; + bool ipv4 = src4 && dst4;
- if (src4 && dst4) { - bp = vu_payloadv4(base); - data = IOV_TAIL(iov_vu, iov_used, (char *)&bp->data - base); - csum_udp4(&bp->uh, *src4, *dst4, &data); - } else { - bp = vu_payloadv6(base); - data = IOV_TAIL(iov_vu, iov_used, (char *)&bp->data - base); - csum_udp6(&bp->uh, &toside->oaddr.a6, &toside->eaddr.a6, &data); - } + iov_drop_header(&payload, + udp_vu_hdrlen(!ipv4) - sizeof(struct udphdr)); + uh = IOV_REMOVE_HEADER(&payload, uh_storage); + + if (ipv4) + csum_udp4(uh, *src4, *dst4, &payload); + else + csum_udp6(uh, &toside->oaddr.a6, &toside->eaddr.a6, &payload); }
/** @@ -216,23 +213,25 @@ void udp_vu_sock_to_tap(const struct ctx *c, int s, int n, flow_sidx_t tosidx) bool v6 = !(inany_v4(&toside->eaddr) && inany_v4(&toside->oaddr)); struct vu_dev *vdev = c->vdev; struct vu_virtq *vq = &vdev->vq[VHOST_USER_RX_QUEUE]; + struct iov_tail data; int i;
for (i = 0; i < n; i++) { ssize_t dlen; - int iov_used;
- iov_used = udp_vu_sock_recv(c, vq, s, v6, &dlen); - if (iov_used < 0) + data = IOV_TAIL(iov_vu, VIRTQUEUE_MAX_SIZE, 0); + + dlen = udp_vu_sock_recv(c, &data, vq, s, v6); + if (dlen < 0) break;
- if (iov_used > 0) { - udp_vu_prepare(c, toside, dlen); + if (data.cnt > 0) { + udp_vu_prepare(c, &data, toside, dlen); if (*c->pcap) { - udp_vu_csum(toside, iov_used); - pcap_iov(iov_vu, iov_used, VNET_HLEN); + udp_vu_csum(toside, &data); + pcap_iov(data.iov, data.cnt, VNET_HLEN); } - vu_flush(vdev, vq, elem, iov_used); + vu_flush(vdev, vq, elem, data.cnt); } } } -- 2.53.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Fri, Feb 27, 2026 at 03:03:22PM +0100, Laurent Vivier wrote:
udp_vu_sock_recv() currently mixes two concerns: receiving data from the socket and managing virtqueue buffers (collecting, rewinding, releasing). This makes the function harder to reason about and couples socket I/O with virtqueue state.
Move all virtqueue operations, vu_collect(), vu_init_elem(), vu_queue_rewind(), and the queue-readiness check, into udp_vu_sock_to_tap(), which is the only caller. This turns udp_vu_sock_recv() into a pure socket receive function that simply reads into the provided iov_tail and adjusts its length.
Signed-off-by: Laurent Vivier
Reviewed-by: David Gibson
--- udp_vu.c | 79 ++++++++++++++++++++++++++------------------------------ 1 file changed, 37 insertions(+), 42 deletions(-)
diff --git a/udp_vu.c b/udp_vu.c index 8f4d0aedac10..aefcab0b86c2 100644 --- a/udp_vu.c +++ b/udp_vu.c @@ -58,75 +58,41 @@ static size_t udp_vu_hdrlen(bool v6)
/** * udp_vu_sock_recv() - Receive datagrams from socket into vhost-user buffers - * @c: Execution context * @data: IO vector tail for the frame (modified on output) - * @vq: virtqueue to use to receive data * @s: Socket to receive from * @v6: Set for IPv6 connections * - * Return: size of received data, 0 if the datagram - * was discarded because the virtqueue is not ready, -1 on error + * Return: size of received data, -1 on error */ -static ssize_t udp_vu_sock_recv(const struct ctx *c, struct iov_tail *data, - struct vu_virtq *vq, int s, bool v6) +static ssize_t udp_vu_sock_recv(struct iov_tail *data, int s, bool v6) { - const struct vu_dev *vdev = c->vdev; - struct msghdr msg = { 0 }; + struct iovec msg_iov[data->cnt]; + struct msghdr msg = { 0 }; struct iov_tail payload; size_t hdrlen; ssize_t dlen; - int iov_cnt; - - ASSERT(!c->no_udp); - - if (!vu_queue_enabled(vq) || !vu_queue_started(vq)) { - debug("Got UDP packet, but RX virtqueue not usable yet"); - - if (recvmsg(s, &msg, MSG_DONTWAIT) < 0) - debug_perror("Failed to discard datagram"); - - data->cnt = 0; - return 0; - }
/* compute L2 header length */ hdrlen = udp_vu_hdrlen(v6);
- vu_init_elem(elem, (struct iovec *)data->iov, data->cnt); - - iov_cnt = vu_collect(vdev, vq, elem, ARRAY_SIZE(elem), - IP_MAX_MTU + ETH_HLEN + VNET_HLEN, NULL); - if (iov_cnt == 0) - return -1; - - data->cnt = iov_cnt; - /* reserve space for the headers */ ASSERT(iov_tail_size(data) >= MAX(hdrlen, ETH_ZLEN + VNET_HLEN));
payload = *data; iov_drop_header(&payload, hdrlen);
- struct iovec msg_iov[payload.cnt]; msg.msg_iov = msg_iov; msg.msg_iovlen = iov_tail_clone(msg.msg_iov, payload.cnt, &payload);
/* read data from the socket */ dlen = recvmsg(s, &msg, 0); - if (dlen < 0) { - vu_queue_rewind(vq, iov_cnt); + if (dlen < 0) return -1; - }
iov_tail_truncate(data, MAX(dlen + hdrlen, ETH_ZLEN + VNET_HLEN)); iov_tail_zero_end(data, dlen + hdrlen); iov_tail_truncate(data, dlen + hdrlen);
- vu_set_vnethdr(vdev, data->iov[0].iov_base, data->cnt); - - /* release unused buffers */ - vu_queue_rewind(vq, iov_cnt - data->cnt); - return dlen; }
@@ -216,16 +182,45 @@ void udp_vu_sock_to_tap(const struct ctx *c, int s, int n, flow_sidx_t tosidx) struct iov_tail data; int i;
+ ASSERT(!c->no_udp); + + if (!vu_queue_enabled(vq) || !vu_queue_started(vq)) { + struct msghdr msg = { 0 }; + + debug("Got UDP packet, but RX virtqueue not usable yet"); + + for (i = 0; i < n; i++) { + if (recvmsg(s, &msg, MSG_DONTWAIT) < 0) + debug_perror("Failed to discard datagram"); + } + + return; + } + for (i = 0; i < n; i++) { ssize_t dlen; + int elem_cnt;
- data = IOV_TAIL(iov_vu, VIRTQUEUE_MAX_SIZE, 0); + vu_init_elem(elem, iov_vu, ARRAY_SIZE(elem));
- dlen = udp_vu_sock_recv(c, &data, vq, s, v6); - if (dlen < 0) + elem_cnt = vu_collect(vdev, vq, elem, ARRAY_SIZE(elem), + IP_MAX_MTU + ETH_HLEN + VNET_HLEN, NULL); + if (elem_cnt == 0) break;
+ data = IOV_TAIL(iov_vu, elem_cnt, 0); + + dlen = udp_vu_sock_recv(&data, s, v6); + if (dlen < 0) { + vu_queue_rewind(vq, elem_cnt); + continue; + } + + /* release unused buffers */ + vu_queue_rewind(vq, elem_cnt - data.cnt); + if (data.cnt > 0) { + vu_set_vnethdr(vdev, data.iov[0].iov_base, data.cnt); udp_vu_prepare(c, &data, toside, dlen); if (*c->pcap) { udp_vu_csum(toside, &data); -- 2.53.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Fri, Feb 27, 2026 at 03:03:23PM +0100, Laurent Vivier wrote:
Add a counterpart to IOV_PEEK_HEADER() that writes header data back to an iov_tail after modification. If the header pointer matches the original iov buffer location, it only advances the offset. Otherwise, it copies the data using iov_from_buf().
Signed-off-by: Laurent Vivier
Concept looks good to me.
--- iov.c | 22 ++++++++++++++++++++++ iov.h | 14 +++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-)
diff --git a/iov.c b/iov.c index 8836305fb701..2cf23d284e4a 100644 --- a/iov.c +++ b/iov.c @@ -296,6 +296,28 @@ void *iov_peek_header_(struct iov_tail *tail, void *v, size_t len, size_t align) return v; }
+/** + * iov_put_header_() - Write header back to an IOV tail + * @tail: IOV tail to write header to + * @v: Pointer to header data to write + * @len: Length of header to write, in bytes + * + * Return: number of bytes written + */ +/* cppcheck-suppress unusedFunction */ +size_t iov_put_header_(struct iov_tail *tail, const void *v, size_t len) +{ + size_t l = len; + + /* iov_peek_header_() already called iov_check_header() */ + if ((char *)tail->iov[0].iov_base + tail->off != v) + l = iov_from_buf(tail->iov, tail->cnt, tail->off, v, len);
IIUC, there's a strong requirement that this only be called after a matching iov_peek_header_(), yes? If that's the case then (l != len) would mean something has already gone badly wrong - so should it be an ASSERT()?
+ tail->off += l; + + return l; +} + /** * iov_remove_header_() - Remove a header from an IOV tail * @tail: IOV tail to remove header from (modified) diff --git a/iov.h b/iov.h index a7b873d58134..08cf60639358 100644 --- a/iov.h +++ b/iov.h @@ -86,6 +86,7 @@ bool iov_tail_prune(struct iov_tail *tail); size_t iov_tail_size(struct iov_tail *tail); bool iov_drop_header(struct iov_tail *tail, size_t len); void *iov_peek_header_(struct iov_tail *tail, void *v, size_t len, size_t align); +size_t iov_put_header_(struct iov_tail *tail, const void *v, size_t len); void *iov_remove_header_(struct iov_tail *tail, void *v, size_t len, size_t align); ssize_t iov_tail_clone(struct iovec *dst_iov, size_t dst_iov_cnt, struct iov_tail *tail); @@ -110,6 +111,16 @@ void iov_tail_zero_end(struct iov_tail *tail, size_t size); sizeof(var_), \ __alignof__(var_))))
+/** + * IOV_PUT_HEADER() - Write header back to an IOV tail + * @tail_: IOV tail to write header to + * @var_: Pointer to a variable containing the header data to write + * + * Return: number of bytes written + */ +#define IOV_PUT_HEADER(tail_, var_) \ + (iov_put_header_((tail_), (var_), sizeof(*var_))) + /** * IOV_REMOVE_HEADER() - Remove and return typed header from an IOV tail * @tail_: IOV tail to remove header from (modified) @@ -128,7 +139,8 @@ void iov_tail_zero_end(struct iov_tail *tail, size_t size); ((__typeof__(var_) *)(iov_remove_header_((tail_), &(var_), \ sizeof(var_), __alignof__(var_))))
-/** IOV_DROP_HEADER() - Remove a typed header from an IOV tail +/** + * IOV_DROP_HEADER() - Remove a typed header from an IOV tail * @tail_: IOV tail to remove header from (modified) * @type_: Data type of the header to remove * -- 2.53.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Fri, Feb 27, 2026 at 03:03:20PM +0100, Laurent Vivier wrote:
When passing the element count to vu_init_elem(), vu_collect(), or using it as a loop bound, use ARRAY_SIZE(elem) instead of the VIRTQUEUE_MAX_SIZE.
No functional change.
Signed-off-by: Laurent Vivier
Reviewed-by: David Gibson
--- tcp_vu.c | 6 +++--- udp_vu.c | 4 ++-- vu_common.c | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/tcp_vu.c b/tcp_vu.c index bb05fbf45826..98e5974fee0e 100644 --- a/tcp_vu.c +++ b/tcp_vu.c @@ -201,17 +201,17 @@ static ssize_t tcp_vu_sock_recv(const struct ctx *c, struct vu_virtq *vq,
hdrlen = tcp_vu_hdrlen(v6);
- vu_init_elem(elem, &iov_vu[DISCARD_IOV_NUM], VIRTQUEUE_MAX_SIZE); + vu_init_elem(elem, &iov_vu[DISCARD_IOV_NUM], ARRAY_SIZE(elem));
elem_cnt = 0; *head_cnt = 0; - while (fillsize > 0 && elem_cnt < VIRTQUEUE_MAX_SIZE) { + while (fillsize > 0 && elem_cnt < ARRAY_SIZE(elem)) { struct iovec *iov; size_t frame_size, dlen; int cnt;
cnt = vu_collect(vdev, vq, &elem[elem_cnt], - VIRTQUEUE_MAX_SIZE - elem_cnt, + ARRAY_SIZE(elem) - elem_cnt, MAX(MIN(mss, fillsize) + hdrlen, ETH_ZLEN + VNET_HLEN), &frame_size); if (cnt == 0) diff --git a/udp_vu.c b/udp_vu.c index 51f3718f5925..6f6477f7d046 100644 --- a/udp_vu.c +++ b/udp_vu.c @@ -89,9 +89,9 @@ static int udp_vu_sock_recv(const struct ctx *c, struct vu_virtq *vq, int s, /* compute L2 header length */ hdrlen = udp_vu_hdrlen(v6);
- vu_init_elem(elem, iov_vu, VIRTQUEUE_MAX_SIZE); + vu_init_elem(elem, iov_vu, ARRAY_SIZE(elem));
- iov_cnt = vu_collect(vdev, vq, elem, VIRTQUEUE_MAX_SIZE, + iov_cnt = vu_collect(vdev, vq, elem, ARRAY_SIZE(elem), IP_MAX_MTU + ETH_HLEN + VNET_HLEN, NULL); if (iov_cnt == 0) return -1; diff --git a/vu_common.c b/vu_common.c index aa14598ea028..7a8cd18f4e81 100644 --- a/vu_common.c +++ b/vu_common.c @@ -174,7 +174,7 @@ static void vu_handle_tx(struct vu_dev *vdev, int index,
count = 0; out_sg_count = 0; - while (count < VIRTQUEUE_MAX_SIZE && + while (count < ARRAY_SIZE(elem) && out_sg_count + VU_MAX_TX_BUFFER_NB <= VIRTQUEUE_MAX_SIZE) { int ret; struct iov_tail data; @@ -259,10 +259,10 @@ int vu_send_single(const struct ctx *c, const void *buf, size_t size) return -1; }
- vu_init_elem(elem, in_sg, VIRTQUEUE_MAX_SIZE); + vu_init_elem(elem, in_sg, ARRAY_SIZE(elem));
size += VNET_HLEN; - elem_cnt = vu_collect(vdev, vq, elem, VIRTQUEUE_MAX_SIZE, size, &total); + elem_cnt = vu_collect(vdev, vq, elem, ARRAY_SIZE(elem), size, &total); if (total < size) { debug("vu_send_single: no space to send the data " "elem_cnt %d size %zd", elem_cnt, total); -- 2.53.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Fri, Feb 27, 2026 at 03:03:24PM +0100, Laurent Vivier wrote:
Change udp_update_hdr4() and udp_update_hdr6() to take a separate struct udphdr pointer and an iov_tail for the payload, instead of a struct udp_payload_t pointer and an explicit data length.
This decouples the header update functions from the udp_payload_t memory layout, which assumes all headers and data sit in a single contiguous buffer. The vhost-user path uses virtqueue-provided scatter-gather buffers where this assumption does not hold; passing an iov_tail lets both the tap path and the vhost-user path share the same functions without casting through layout-specific helpers.
Signed-off-by: Laurent Vivier
Concept looks good, a couple of nits below.
--- udp.c | 65 ++++++++++++++++++++++---------------------------- udp_internal.h | 10 ++++---- udp_vu.c | 14 +++++++++-- 3 files changed, 47 insertions(+), 42 deletions(-)
diff --git a/udp.c b/udp.c index 19adda065f44..52e93e357a94 100644 --- a/udp.c +++ b/udp.c @@ -257,18 +257,17 @@ static void udp_iov_init(const struct ctx *c) * @ip4h: Pre-filled IPv4 header (except for tot_len and saddr) * @bp: Pointer to udp_payload_t to update
Needs to be replaced with @payload description.
* @toside: Flowside for destination side - * @dlen: Length of UDP payload * @no_udp_csum: Do not set UDP checksum * * Return: size of IPv4 payload (UDP header + data) */ -size_t udp_update_hdr4(struct iphdr *ip4h, struct udp_payload_t *bp, - const struct flowside *toside, size_t dlen, - bool no_udp_csum) +size_t udp_update_hdr4(struct iphdr *ip4h, struct udphdr *uh, + struct iov_tail *payload, + const struct flowside *toside, bool no_udp_csum) { const struct in_addr *src = inany_v4(&toside->oaddr); const struct in_addr *dst = inany_v4(&toside->eaddr); - size_t l4len = dlen + sizeof(bp->uh); + size_t l4len = iov_tail_size(payload) + sizeof(*uh); size_t l3len = l4len + sizeof(*ip4h);
ASSERT(src && dst); @@ -278,19 +277,13 @@ size_t udp_update_hdr4(struct iphdr *ip4h, struct udp_payload_t *bp, ip4h->saddr = src->s_addr; ip4h->check = csum_ip4_header(l3len, IPPROTO_UDP, *src, *dst);
- bp->uh.source = htons(toside->oport); - bp->uh.dest = htons(toside->eport); - bp->uh.len = htons(l4len); - if (no_udp_csum) { - bp->uh.check = 0; - } else { - const struct iovec iov = { - .iov_base = bp->data, - .iov_len = dlen - }; - struct iov_tail data = IOV_TAIL(&iov, 1, 0); - csum_udp4(&bp->uh, *src, *dst, &data); - } + uh->source = htons(toside->oport); + uh->dest = htons(toside->eport); + uh->len = htons(l4len); + if (no_udp_csum) + uh->check = 0; + else + csum_udp4(uh, *src, *dst, payload);
return l4len; } @@ -306,11 +299,11 @@ size_t udp_update_hdr4(struct iphdr *ip4h, struct udp_payload_t *bp, *
This comment also needs updates for the changed parameters.
* Return: size of IPv6 payload (UDP header + data) */ -size_t udp_update_hdr6(struct ipv6hdr *ip6h, struct udp_payload_t *bp, - const struct flowside *toside, size_t dlen, - bool no_udp_csum) +size_t udp_update_hdr6(struct ipv6hdr *ip6h, struct udphdr *uh, + struct iov_tail *payload, + const struct flowside *toside, bool no_udp_csum) { - uint16_t l4len = dlen + sizeof(bp->uh); + uint16_t l4len = iov_tail_size(payload) + sizeof(*uh);
ip6h->payload_len = htons(l4len); ip6h->daddr = toside->eaddr.a6; @@ -319,22 +312,17 @@ size_t udp_update_hdr6(struct ipv6hdr *ip6h, struct udp_payload_t *bp, ip6h->nexthdr = IPPROTO_UDP; ip6h->hop_limit = 255;
- bp->uh.source = htons(toside->oport); - bp->uh.dest = htons(toside->eport); - bp->uh.len = ip6h->payload_len; + uh->source = htons(toside->oport); + uh->dest = htons(toside->eport); + uh->len = ip6h->payload_len; if (no_udp_csum) { /* 0 is an invalid checksum for UDP IPv6 and dropped by * the kernel stack, even if the checksum is disabled by virtio * flags. We need to put any non-zero value here. */ - bp->uh.check = 0xffff; + uh->check = 0xffff; } else { - const struct iovec iov = { - .iov_base = bp->data, - .iov_len = dlen - }; - struct iov_tail data = IOV_TAIL(&iov, 1, 0); - csum_udp6(&bp->uh, &toside->oaddr.a6, &toside->eaddr.a6, &data); + csum_udp6(uh, &toside->oaddr.a6, &toside->eaddr.a6, payload); }
return l4len; @@ -374,12 +362,17 @@ static void udp_tap_prepare(const struct mmsghdr *mmh, struct ethhdr *eh = (*tap_iov)[UDP_IOV_ETH].iov_base; struct udp_payload_t *bp = &udp_payload[idx]; struct udp_meta_t *bm = &udp_meta[idx]; + const struct iovec iov = { + .iov_base = bp->data, + .iov_len = mmh[idx].msg_len, + }; + struct iov_tail payload = IOV_TAIL(&iov, 1, 0); size_t l4len, l2len;
eth_update_mac(eh, NULL, tap_omac); if (!inany_v4(&toside->eaddr) || !inany_v4(&toside->oaddr)) { - l4len = udp_update_hdr6(&bm->ip6h, bp, toside, - mmh[idx].msg_len, no_udp_csum); + l4len = udp_update_hdr6(&bm->ip6h, &bp->uh, &payload, toside, + no_udp_csum);
l2len = MAX(l4len + sizeof(bm->ip6h) + ETH_HLEN, ETH_ZLEN); tap_hdr_update(&bm->taph, l2len); @@ -387,8 +380,8 @@ static void udp_tap_prepare(const struct mmsghdr *mmh, eh->h_proto = htons_constant(ETH_P_IPV6); (*tap_iov)[UDP_IOV_IP] = IOV_OF_LVALUE(bm->ip6h); } else { - l4len = udp_update_hdr4(&bm->ip4h, bp, toside, - mmh[idx].msg_len, no_udp_csum); + l4len = udp_update_hdr4(&bm->ip4h, &bp->uh, &payload, toside, + no_udp_csum);
l2len = MAX(l4len + sizeof(bm->ip4h) + ETH_HLEN, ETH_ZLEN); tap_hdr_update(&bm->taph, l2len); diff --git a/udp_internal.h b/udp_internal.h index 0a8fe490b07e..aca831808ab4 100644 --- a/udp_internal.h +++ b/udp_internal.h @@ -22,11 +22,13 @@ struct udp_payload_t { } __attribute__ ((packed, aligned(__alignof__(unsigned int)))); #endif
-size_t udp_update_hdr4(struct iphdr *ip4h, struct udp_payload_t *bp, - const struct flowside *toside, size_t dlen, +size_t udp_update_hdr4(struct iphdr *ip4h, struct udphdr *uh, + struct iov_tail *payload, + const struct flowside *toside, bool no_udp_csum); -size_t udp_update_hdr6(struct ipv6hdr *ip6h, struct udp_payload_t *bp, - const struct flowside *toside, size_t dlen, +size_t udp_update_hdr6(struct ipv6hdr *ip6h, struct udphdr *uh, + struct iov_tail *payload, + const struct flowside *toside, bool no_udp_csum); void udp_sock_fwd(const struct ctx *c, int s, int rule_hint, uint8_t frompif, in_port_t port, const struct timespec *now); diff --git a/udp_vu.c b/udp_vu.c index aefcab0b86c2..dd8904d65a38 100644 --- a/udp_vu.c +++ b/udp_vu.c @@ -122,21 +122,31 @@ static size_t udp_vu_prepare(const struct ctx *c, const struct iov_tail *data, if (inany_v4(&toside->eaddr) && inany_v4(&toside->oaddr)) { struct iphdr *iph = vu_ip(iov[0].iov_base); struct udp_payload_t *bp = vu_payloadv4(iov[0].iov_base); + const struct iovec payload_iov = { + .iov_base = bp->data, + .iov_len = dlen, + }; + struct iov_tail payload = IOV_TAIL(&payload_iov, 1, 0);
eh->h_proto = htons(ETH_P_IP);
*iph = (struct iphdr)L2_BUF_IP4_INIT(IPPROTO_UDP);
- l4len = udp_update_hdr4(iph, bp, toside, dlen, true); + l4len = udp_update_hdr4(iph, &bp->uh, &payload, toside, true); } else { struct ipv6hdr *ip6h = vu_ip(iov[0].iov_base); struct udp_payload_t *bp = vu_payloadv6(iov[0].iov_base); + const struct iovec payload_iov = { + .iov_base = bp->data, + .iov_len = dlen, + }; + struct iov_tail payload = IOV_TAIL(&payload_iov, 1, 0);
eh->h_proto = htons(ETH_P_IPV6);
*ip6h = (struct ipv6hdr)L2_BUF_IP6_INIT(IPPROTO_UDP);
- l4len = udp_update_hdr6(ip6h, bp, toside, dlen, true); + l4len = udp_update_hdr6(ip6h, &bp->uh, &payload, toside, true); }
return l4len; -- 2.53.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Fri, Feb 27, 2026 at 03:03:25PM +0100, Laurent Vivier wrote:
Rework udp_vu_prepare() to use IOV_REMOVE_HEADER() and IOV_PUT_HEADER() to walk through Ethernet, IP and UDP headers instead of the layout-specific helpers (vu_eth(), vu_ip(), vu_payloadv4(), vu_payloadv6()) that assume a contiguous buffer. The payload length is now implicit in the iov_tail, so drop the dlen parameter.
Signed-off-by: Laurent Vivier
LGTM, a few nits below.
--- iov.c | 1 - udp_vu.c | 63 ++++++++++++++++++++++++++++++-------------------------- 2 files changed, 34 insertions(+), 30 deletions(-)
diff --git a/iov.c b/iov.c index 2cf23d284e4a..7c9641968271 100644 --- a/iov.c +++ b/iov.c @@ -304,7 +304,6 @@ void *iov_peek_header_(struct iov_tail *tail, void *v, size_t len, size_t align) * * Return: number of bytes written */ -/* cppcheck-suppress unusedFunction */ size_t iov_put_header_(struct iov_tail *tail, const void *v, size_t len) { size_t l = len; diff --git a/udp_vu.c b/udp_vu.c index dd8904d65a38..6d87f4872268 100644 --- a/udp_vu.c +++ b/udp_vu.c @@ -101,52 +101,54 @@ static ssize_t udp_vu_sock_recv(struct iov_tail *data, int s, bool v6) * @c: Execution context * @data: IO vector tail for the frame * @toside: Address information for one side of the flow - * @dlen: Packet data length * * Return: Layer-4 length */ static size_t udp_vu_prepare(const struct ctx *c, const struct iov_tail *data, - const struct flowside *toside, ssize_t dlen) + const struct flowside *toside) { - const struct iovec *iov = data->iov; - struct ethhdr *eh; + struct iov_tail current = *data; + struct ethhdr *eh, eh_storage; + struct udphdr *uh, uh_storage; size_t l4len;
/* ethernet header */ - eh = vu_eth(iov[0].iov_base); + eh = IOV_REMOVE_HEADER(¤t, eh_storage);
memcpy(eh->h_dest, c->guest_mac, sizeof(eh->h_dest)); memcpy(eh->h_source, c->our_tap_mac, sizeof(eh->h_source));
/* initialize header */ if (inany_v4(&toside->eaddr) && inany_v4(&toside->oaddr)) { - struct iphdr *iph = vu_ip(iov[0].iov_base); - struct udp_payload_t *bp = vu_payloadv4(iov[0].iov_base); - const struct iovec payload_iov = { - .iov_base = bp->data, - .iov_len = dlen, - }; - struct iov_tail payload = IOV_TAIL(&payload_iov, 1, 0); + struct iphdr *iph, iph_storage;
eh->h_proto = htons(ETH_P_IP);
+ iph = IOV_REMOVE_HEADER(¤t, iph_storage); *iph = (struct iphdr)L2_BUF_IP4_INIT(IPPROTO_UDP);
- l4len = udp_update_hdr4(iph, &bp->uh, &payload, toside, true); + uh = IOV_REMOVE_HEADER(¤t, uh_storage); + l4len = udp_update_hdr4(iph, uh, ¤t, toside, true); + + current = *data; + IOV_PUT_HEADER(¤t, eh); + IOV_PUT_HEADER(¤t, iph); + IOV_PUT_HEADER(¤t, uh);
The puts for eh and uh can be factored out of the if/else.
} else { - struct ipv6hdr *ip6h = vu_ip(iov[0].iov_base); - struct udp_payload_t *bp = vu_payloadv6(iov[0].iov_base); - const struct iovec payload_iov = { - .iov_base = bp->data, - .iov_len = dlen, - }; - struct iov_tail payload = IOV_TAIL(&payload_iov, 1, 0); + struct ipv6hdr *ip6h, ip6h_storage;
eh->h_proto = htons(ETH_P_IPV6);
+ ip6h = IOV_REMOVE_HEADER(¤t, ip6h_storage); *ip6h = (struct ipv6hdr)L2_BUF_IP6_INIT(IPPROTO_UDP);
- l4len = udp_update_hdr6(ip6h, &bp->uh, &payload, toside, true); + uh = IOV_REMOVE_HEADER(¤t, uh_storage); + l4len = udp_update_hdr6(ip6h, uh, ¤t, toside, true); + + current = *data; + IOV_PUT_HEADER(¤t, eh); + IOV_PUT_HEADER(¤t, ip6h); + IOV_PUT_HEADER(¤t, uh); }
return l4len; @@ -165,9 +167,10 @@ static void udp_vu_csum(const struct flowside *toside, struct iov_tail payload = *data; struct udphdr *uh, uh_storage; bool ipv4 = src4 && dst4; + int hdrlen = sizeof(struct ethhdr) + + (ipv4 ? sizeof(struct iphdr) : sizeof(struct ipv6hdr));
- iov_drop_header(&payload, - udp_vu_hdrlen(!ipv4) - sizeof(struct udphdr)); + iov_drop_header(&payload, hdrlen);
udp_vu_prepare() and udp_vu_csum() independently locate the UDP header, but they're called in fairly close proximity. Would it make more sense to pass the UDP header and payload tail separately to each of them?
uh = IOV_REMOVE_HEADER(&payload, uh_storage);
if (ipv4) @@ -208,8 +211,8 @@ void udp_vu_sock_to_tap(const struct ctx *c, int s, int n, flow_sidx_t tosidx) }
for (i = 0; i < n; i++) { + int elem_cnt, elem_used; ssize_t dlen; - int elem_cnt;
vu_init_elem(elem, iov_vu, ARRAY_SIZE(elem));
@@ -225,18 +228,20 @@ void udp_vu_sock_to_tap(const struct ctx *c, int s, int n, flow_sidx_t tosidx) vu_queue_rewind(vq, elem_cnt); continue; } + elem_used = data.cnt;
/* release unused buffers */ - vu_queue_rewind(vq, elem_cnt - data.cnt); + vu_queue_rewind(vq, elem_cnt - elem_used);
if (data.cnt > 0) { - vu_set_vnethdr(vdev, data.iov[0].iov_base, data.cnt); - udp_vu_prepare(c, &data, toside, dlen); + vu_set_vnethdr(vdev, data.iov[0].iov_base, elem_used); + iov_drop_header(&data, VNET_HLEN); + udp_vu_prepare(c, &data, toside); if (*c->pcap) { udp_vu_csum(toside, &data); - pcap_iov(data.iov, data.cnt, VNET_HLEN); + pcap_iov(data.iov, data.cnt, 0); } - vu_flush(vdev, vq, elem, data.cnt); + vu_flush(vdev, vq, elem, elem_used); } } } -- 2.53.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Fri, Feb 27, 2026 at 03:03:26PM +0100, Laurent Vivier wrote:
Refactor vu_set_vnethdr() to take an iov_tail pointer instead of a direct pointer to the virtio_net_hdr_mrg_rxbuf structure. This makes the function use IOV_PEEK_HEADER() and IOV_PUT_HEADER() to read and write the virtio-net header through the iov_tail abstraction.
Signed-off-by: Laurent Vivier
LGTM, with one nit.
--- tcp_vu.c | 8 +++++--- udp_vu.c | 3 +-- vu_common.c | 24 ++++++++++++++++-------- vu_common.h | 3 +-- 4 files changed, 23 insertions(+), 15 deletions(-)
diff --git a/tcp_vu.c b/tcp_vu.c index 98e5974fee0e..92667507ac9b 100644 --- a/tcp_vu.c +++ b/tcp_vu.c @@ -94,10 +94,11 @@ int tcp_vu_send_flag(const struct ctx *c, struct tcp_tap_conn *conn, int flags) if (elem_cnt != 1) return -1;
- ASSERT(flags_elem[0].in_sg[0].iov_len >= + payload = IOV_TAIL(&flags_elem[0].in_sg[0], elem_cnt, 0);
Shouldn't a later initialization of payload be removed to match this?
+ ASSERT(iov_tail_size(&payload) >= MAX(hdrlen + sizeof(*opts), ETH_ZLEN + VNET_HLEN));
- vu_set_vnethdr(vdev, flags_elem[0].in_sg[0].iov_base, 1); + vu_set_vnethdr(vdev, &payload, 1);
eh = vu_eth(flags_elem[0].in_sg[0].iov_base);
@@ -448,11 +449,12 @@ int tcp_vu_data_from_sock(const struct ctx *c, struct tcp_tap_conn *conn) for (i = 0, previous_dlen = -1, check = NULL; i < head_cnt; i++) { struct iovec *iov = &elem[head[i]].in_sg[0]; int buf_cnt = head[i + 1] - head[i]; + struct iov_tail data = IOV_TAIL(iov, buf_cnt, 0); ssize_t dlen = iov_size(iov, buf_cnt) - hdrlen; bool push = i == head_cnt - 1; size_t l2len;
- vu_set_vnethdr(vdev, iov->iov_base, buf_cnt); + vu_set_vnethdr(vdev, &data, buf_cnt);
/* The IPv4 header checksum varies only with dlen */ if (previous_dlen != dlen) diff --git a/udp_vu.c b/udp_vu.c index 6d87f4872268..5ae79b9bb0c5 100644 --- a/udp_vu.c +++ b/udp_vu.c @@ -234,8 +234,7 @@ void udp_vu_sock_to_tap(const struct ctx *c, int s, int n, flow_sidx_t tosidx) vu_queue_rewind(vq, elem_cnt - elem_used);
if (data.cnt > 0) { - vu_set_vnethdr(vdev, data.iov[0].iov_base, elem_used); - iov_drop_header(&data, VNET_HLEN); + vu_set_vnethdr(vdev, &data, elem_used); udp_vu_prepare(c, &data, toside); if (*c->pcap) { udp_vu_csum(toside, &data); diff --git a/vu_common.c b/vu_common.c index 7a8cd18f4e81..a8d5fcdaea83 100644 --- a/vu_common.c +++ b/vu_common.c @@ -120,18 +120,25 @@ int vu_collect(const struct vu_dev *vdev, struct vu_virtq *vq, }
/** - * vu_set_vnethdr() - set virtio-net headers + * vu_set_vnethdr() - set virtio-net header * @vdev: vhost-user device - * @vnethdr: Address of the header to set + * @data: IOV tail to write header to, updated to + * point after the virtio-net header * @num_buffers: Number of guest buffers of the frame */ -void vu_set_vnethdr(const struct vu_dev *vdev, - struct virtio_net_hdr_mrg_rxbuf *vnethdr, +void vu_set_vnethdr(const struct vu_dev *vdev, struct iov_tail *data, int num_buffers) { + struct virtio_net_hdr_mrg_rxbuf vnethdr_storage, *vnethdr; + + vnethdr = IOV_PEEK_HEADER(data, vnethdr_storage); + vnethdr->hdr = VU_HEADER; + if (vu_has_feature(vdev, VIRTIO_NET_F_MRG_RXBUF)) vnethdr->num_buffers = htole16(num_buffers); + + IOV_PUT_HEADER(data, vnethdr); }
/** @@ -248,6 +255,7 @@ int vu_send_single(const struct ctx *c, const void *buf, size_t size) struct vu_virtq *vq = &vdev->vq[VHOST_USER_RX_QUEUE]; struct vu_virtq_element elem[VIRTQUEUE_MAX_SIZE]; struct iovec in_sg[VIRTQUEUE_MAX_SIZE]; + struct iov_tail data; size_t total; int elem_cnt; int i; @@ -269,15 +277,15 @@ int vu_send_single(const struct ctx *c, const void *buf, size_t size) goto err; }
- vu_set_vnethdr(vdev, in_sg[0].iov_base, elem_cnt); - + data = IOV_TAIL(&in_sg[0], elem_cnt, 0); + vu_set_vnethdr(vdev, &data, elem_cnt); total -= VNET_HLEN;
/* copy data from the buffer to the iovec */ - iov_from_buf(in_sg, elem_cnt, VNET_HLEN, buf, total); + iov_from_buf(data.iov, data.cnt, data.off, buf, total);
if (*c->pcap) - pcap_iov(in_sg, elem_cnt, VNET_HLEN); + pcap_iov(data.iov, data.cnt, data.off);
vu_flush(vdev, vq, elem, elem_cnt);
diff --git a/vu_common.h b/vu_common.h index 052aff710502..41cf18936300 100644 --- a/vu_common.h +++ b/vu_common.h @@ -49,8 +49,7 @@ void vu_init_elem(struct vu_virtq_element *elem, struct iovec *iov, int vu_collect(const struct vu_dev *vdev, struct vu_virtq *vq, struct vu_virtq_element *elem, int max_elem, size_t size, size_t *collected); -void vu_set_vnethdr(const struct vu_dev *vdev, - struct virtio_net_hdr_mrg_rxbuf *vnethdr, +void vu_set_vnethdr(const struct vu_dev *vdev, struct iov_tail *data, int num_buffers); void vu_flush(const struct vu_dev *vdev, struct vu_virtq *vq, struct vu_virtq_element *elem, int elem_cnt); -- 2.53.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Fri, Feb 27, 2026 at 03:03:27PM +0100, Laurent Vivier wrote:
Previously, vu_set_element() derived the number of iovec entries from whether the pointer was NULL or not (using !!out_sg and !!in_sg). This implicitly limited each virtqueue element to at most one iovec per direction.
Change the function signature to accept explicit out_num and in_num parameters, allowing callers to specify multiple iovec entries per element when needed. Update all existing call sites to pass the equivalent values (0 for NULL pointers, 1 for valid pointers).
No functional change.
Signed-off-by: Laurent Vivier
Reviewed-by: David Gibson
--- tcp_vu.c | 4 ++-- vu_common.c | 5 +++-- vu_common.h | 13 ++++++++----- 3 files changed, 13 insertions(+), 9 deletions(-)
diff --git a/tcp_vu.c b/tcp_vu.c index 92667507ac9b..77d6e75496a0 100644 --- a/tcp_vu.c +++ b/tcp_vu.c @@ -87,7 +87,7 @@ int tcp_vu_send_flag(const struct ctx *c, struct tcp_tap_conn *conn, int flags)
hdrlen = tcp_vu_hdrlen(CONN_V6(conn));
- vu_set_element(&flags_elem[0], NULL, &flags_iov[0]); + vu_set_element(&flags_elem[0], 0, NULL, 1, &flags_iov[0]);
elem_cnt = vu_collect(vdev, vq, &flags_elem[0], 1, MAX(hdrlen + sizeof(*opts), ETH_ZLEN + VNET_HLEN), NULL); @@ -149,7 +149,7 @@ int tcp_vu_send_flag(const struct ctx *c, struct tcp_tap_conn *conn, int flags) nb_ack = 1;
if (flags & DUP_ACK) { - vu_set_element(&flags_elem[1], NULL, &flags_iov[1]); + vu_set_element(&flags_elem[1], 0, NULL, 1, &flags_iov[1]);
elem_cnt = vu_collect(vdev, vq, &flags_elem[1], 1, flags_elem[0].in_sg[0].iov_len, NULL); diff --git a/vu_common.c b/vu_common.c index a8d5fcdaea83..b6753a556049 100644 --- a/vu_common.c +++ b/vu_common.c @@ -59,12 +59,13 @@ int vu_packet_check_range(struct vdev_memory *memory, * @iov: Array of iovec to assign to virtqueue element * @elem_cnt: Number of virtqueue element */ -void vu_init_elem(struct vu_virtq_element *elem, struct iovec *iov, int elem_cnt) +void vu_init_elem(struct vu_virtq_element *elem, struct iovec *iov, + int elem_cnt) { int i;
for (i = 0; i < elem_cnt; i++) - vu_set_element(&elem[i], NULL, &iov[i]); + vu_set_element(&elem[i], 0, NULL, 1, &iov[i]); }
/** diff --git a/vu_common.h b/vu_common.h index 41cf18936300..c1d2ce888f12 100644 --- a/vu_common.h +++ b/vu_common.h @@ -32,15 +32,18 @@ static inline void *vu_payloadv6(void *base) /** * vu_set_element() - Initialize a vu_virtq_element * @elem: Element to initialize - * @out_sg: One out iovec entry to set in elem - * @in_sg: One in iovec entry to set in elem + * @out_num: Number of outgoing iovec buffers + * @out_sg: Out iovec entry to set in elem + * @in_num: Number of incoming iovec buffers + * @in_sg: In iovec entry to set in elem */ static inline void vu_set_element(struct vu_virtq_element *elem, - struct iovec *out_sg, struct iovec *in_sg) + unsigned int out_num, struct iovec *out_sg, + unsigned int in_num, struct iovec *in_sg) { - elem->out_num = !!out_sg; + elem->out_num = out_num; elem->out_sg = out_sg; - elem->in_num = !!in_sg; + elem->in_num = in_num; elem->in_sg = in_sg; }
-- 2.53.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Fri, Feb 27, 2026 at 03:03:28PM +0100, Laurent Vivier wrote:
Extend vu_init_elem() to accept an iov_per_elem parameter specifying how many iovec entries to assign to each virtqueue element. The iov array is now strided by iov_per_elem rather than 1.
Update all callers to pass 1, preserving existing behavior.
No functional change.
Signed-off-by: Laurent Vivier
Reviewed-by: David Gibson
--- tcp_vu.c | 2 +- udp_vu.c | 2 +- vu_common.c | 19 ++++++++++--------- vu_common.h | 2 +- 4 files changed, 13 insertions(+), 12 deletions(-)
diff --git a/tcp_vu.c b/tcp_vu.c index 77d6e75496a0..51f7834f0307 100644 --- a/tcp_vu.c +++ b/tcp_vu.c @@ -202,7 +202,7 @@ static ssize_t tcp_vu_sock_recv(const struct ctx *c, struct vu_virtq *vq,
hdrlen = tcp_vu_hdrlen(v6);
- vu_init_elem(elem, &iov_vu[DISCARD_IOV_NUM], ARRAY_SIZE(elem)); + vu_init_elem(elem, &iov_vu[DISCARD_IOV_NUM], ARRAY_SIZE(elem), 1);
elem_cnt = 0; *head_cnt = 0; diff --git a/udp_vu.c b/udp_vu.c index 5ae79b9bb0c5..7e486b74883e 100644 --- a/udp_vu.c +++ b/udp_vu.c @@ -214,7 +214,7 @@ void udp_vu_sock_to_tap(const struct ctx *c, int s, int n, flow_sidx_t tosidx) int elem_cnt, elem_used; ssize_t dlen;
- vu_init_elem(elem, iov_vu, ARRAY_SIZE(elem)); + vu_init_elem(elem, iov_vu, ARRAY_SIZE(elem), 1);
elem_cnt = vu_collect(vdev, vq, elem, ARRAY_SIZE(elem), IP_MAX_MTU + ETH_HLEN + VNET_HLEN, NULL); diff --git a/vu_common.c b/vu_common.c index b6753a556049..e32a56d881a3 100644 --- a/vu_common.c +++ b/vu_common.c @@ -54,18 +54,19 @@ int vu_packet_check_range(struct vdev_memory *memory, }
/** - * vu_init_elem() - initialize an array of virtqueue elements with 1 iov in each - * @elem: Array of virtqueue elements to initialize - * @iov: Array of iovec to assign to virtqueue element - * @elem_cnt: Number of virtqueue element + * vu_init_elem() - Initialize an array of virtqueue elements + * @elem: Array of virtqueue elements to initialize + * @iov: Array of iovecs to assign to virtqueue elements + * @elem_cnt: Number of virtqueue elements + * @iov_per_elem: Number of iovecs per element */ void vu_init_elem(struct vu_virtq_element *elem, struct iovec *iov, - int elem_cnt) + int elem_cnt, int iov_per_elem) { - int i; + int i, j;
- for (i = 0; i < elem_cnt; i++) - vu_set_element(&elem[i], 0, NULL, 1, &iov[i]); + for (i = 0, j = 0; i < elem_cnt; i++, j += iov_per_elem) + vu_set_element(&elem[i], 0, NULL, iov_per_elem, &iov[j]); }
/** @@ -268,7 +269,7 @@ int vu_send_single(const struct ctx *c, const void *buf, size_t size) return -1; }
- vu_init_elem(elem, in_sg, ARRAY_SIZE(elem)); + vu_init_elem(elem, in_sg, ARRAY_SIZE(elem), 1);
size += VNET_HLEN; elem_cnt = vu_collect(vdev, vq, elem, ARRAY_SIZE(elem), size, &total); diff --git a/vu_common.h b/vu_common.h index c1d2ce888f12..51639c04df14 100644 --- a/vu_common.h +++ b/vu_common.h @@ -48,7 +48,7 @@ static inline void vu_set_element(struct vu_virtq_element *elem, }
void vu_init_elem(struct vu_virtq_element *elem, struct iovec *iov, - int elem_cnt); + int elem_cnt, int iov_per_elem); int vu_collect(const struct vu_dev *vdev, struct vu_virtq *vq, struct vu_virtq_element *elem, int max_elem, size_t size, size_t *collected); -- 2.53.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Fri, Feb 27, 2026 at 03:03:30PM +0100, Laurent Vivier wrote:
iPXE places the vnet header in one virtqueue descriptor and the payload in another. When passt maps these descriptors, it needs two iovecs per virtqueue element to handle this layout.
Without this, passt crashes with:
ASSERTION FAILED in virtqueue_map_desc (virtio.c:403): num_sg < max_num_sg
Signed-off-by: Laurent Vivier
Reviewed-by: David Gibson
--- udp_vu.c | 8 ++++---- vu_common.c | 34 +++++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 15 deletions(-)
diff --git a/udp_vu.c b/udp_vu.c index 7e486b74883e..13fea87e1b9f 100644 --- a/udp_vu.c +++ b/udp_vu.c @@ -34,7 +34,7 @@ #include "vu_common.h"
static struct iovec iov_vu [VIRTQUEUE_MAX_SIZE]; -static struct vu_virtq_element elem [VIRTQUEUE_MAX_SIZE]; +static struct vu_virtq_element elem [VIRTQUEUE_MAX_SIZE / IOV_PER_ELEM];
/** * udp_vu_hdrlen() - Sum size of all headers, from UDP to virtio-net @@ -214,21 +214,21 @@ void udp_vu_sock_to_tap(const struct ctx *c, int s, int n, flow_sidx_t tosidx) int elem_cnt, elem_used; ssize_t dlen;
- vu_init_elem(elem, iov_vu, ARRAY_SIZE(elem), 1); + vu_init_elem(elem, iov_vu, ARRAY_SIZE(elem), IOV_PER_ELEM);
elem_cnt = vu_collect(vdev, vq, elem, ARRAY_SIZE(elem), IP_MAX_MTU + ETH_HLEN + VNET_HLEN, NULL); if (elem_cnt == 0) break;
- data = IOV_TAIL(iov_vu, elem_cnt, 0); + data = IOV_TAIL(iov_vu, (size_t)(elem_cnt * IOV_PER_ELEM), 0);
dlen = udp_vu_sock_recv(&data, s, v6); if (dlen < 0) { vu_queue_rewind(vq, elem_cnt); continue; } - elem_used = data.cnt; + elem_used = DIV_ROUND_UP(data.cnt, IOV_PER_ELEM);
/* release unused buffers */ vu_queue_rewind(vq, elem_cnt - elem_used); diff --git a/vu_common.c b/vu_common.c index 67d8f3e47338..3f50d31da633 100644 --- a/vu_common.c +++ b/vu_common.c @@ -63,8 +63,15 @@ void vu_init_elem(struct vu_virtq_element *elem, struct iovec *iov, { int i, j;
- for (i = 0, j = 0; i < elem_cnt; i++, j += iov_per_elem) + for (i = 0, j = 0; i < elem_cnt; i++, j += iov_per_elem) { + int k; + + for (k = 0; k < iov_per_elem; k++) { + iov[j + k].iov_base = NULL; + iov[j + k].iov_len = 0; + } vu_set_element(&elem[i], 0, NULL, iov_per_elem, &iov[j]); + } }
/** @@ -88,7 +95,8 @@ int vu_collect(const struct vu_dev *vdev, struct vu_virtq *vq, int elem_cnt = 0;
while (current_size < size && elem_cnt < max_elem) { - struct iovec *iov; + struct iov_tail tail; + size_t elem_size; int ret;
ret = vu_queue_pop(vdev, vq, &elem[elem_cnt]); @@ -101,12 +109,14 @@ int vu_collect(const struct vu_dev *vdev, struct vu_virtq *vq, break; }
- iov = &elem[elem_cnt].in_sg[0]; + tail = IOV_TAIL(elem[elem_cnt].in_sg, elem[elem_cnt].in_num, 0); + iov_tail_truncate(&tail, size - current_size); + elem[elem_cnt].in_num = tail.cnt;
- if (iov->iov_len > size - current_size) - iov->iov_len = size - current_size; + elem_size = iov_size(elem[elem_cnt].in_sg, + elem[elem_cnt].in_num);
- current_size += iov->iov_len; + current_size += elem_size; elem_cnt++;
if (!vu_has_feature(vdev, VIRTIO_NET_F_MRG_RXBUF)) @@ -153,8 +163,10 @@ void vu_flush(const struct vu_dev *vdev, struct vu_virtq *vq, { int i;
- for (i = 0; i < elem_cnt; i++) - vu_queue_fill(vdev, vq, &elem[i], elem[i].in_sg[0].iov_len, i); + for (i = 0; i < elem_cnt; i++) { + size_t elem_size = iov_size(elem[i].in_sg, elem[i].in_num); + vu_queue_fill(vdev, vq, &elem[i], elem_size, i); + }
vu_queue_flush(vdev, vq, elem_cnt); vu_queue_notify(vdev, vq); @@ -253,7 +265,7 @@ int vu_send_single(const struct ctx *c, const void *buf, size_t size) { struct vu_dev *vdev = c->vdev; struct vu_virtq *vq = &vdev->vq[VHOST_USER_RX_QUEUE]; - struct vu_virtq_element elem[VIRTQUEUE_MAX_SIZE]; + struct vu_virtq_element elem[VIRTQUEUE_MAX_SIZE / IOV_PER_ELEM]; struct iovec in_sg[VIRTQUEUE_MAX_SIZE]; struct iov_tail data; size_t total; @@ -267,7 +279,7 @@ int vu_send_single(const struct ctx *c, const void *buf, size_t size) return -1; }
- vu_init_elem(elem, in_sg, ARRAY_SIZE(elem), 1); + vu_init_elem(elem, in_sg, ARRAY_SIZE(elem), IOV_PER_ELEM);
size += VNET_HLEN; elem_cnt = vu_collect(vdev, vq, elem, ARRAY_SIZE(elem), size, &total); @@ -277,7 +289,7 @@ int vu_send_single(const struct ctx *c, const void *buf, size_t size) goto err; }
- data = IOV_TAIL(&in_sg[0], elem_cnt, 0); + data = IOV_TAIL(&in_sg[0], (size_t)(elem_cnt * IOV_PER_ELEM), 0); vu_set_vnethdr(vdev, &data, elem_cnt); total -= VNET_HLEN;
-- 2.53.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Fri, Feb 27, 2026 at 03:03:29PM +0100, Laurent Vivier wrote:
1b95bd6fa114 ("vhost_user: fix multibuffer from linux") introduces multibuffer with TX (from the guest), but with iPXE we need to handle also multibuffer for RX (to the guest). This patch makes the parameter generic and global.
No functional change.
Signed-off-by: Laurent Vivier
Reviewed-by: David Gibson
--- vu_common.c | 6 ++---- vu_common.h | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/vu_common.c b/vu_common.c index e32a56d881a3..67d8f3e47338 100644 --- a/vu_common.c +++ b/vu_common.c @@ -20,8 +20,6 @@ #include "migrate.h" #include "epoll_ctl.h"
-#define VU_MAX_TX_BUFFER_NB 2 - /** * vu_packet_check_range() - Check if a given memory zone is contained in * a mapped guest memory region @@ -184,11 +182,11 @@ static void vu_handle_tx(struct vu_dev *vdev, int index, count = 0; out_sg_count = 0; while (count < ARRAY_SIZE(elem) && - out_sg_count + VU_MAX_TX_BUFFER_NB <= VIRTQUEUE_MAX_SIZE) { + out_sg_count + IOV_PER_ELEM <= VIRTQUEUE_MAX_SIZE) { int ret; struct iov_tail data;
- elem[count].out_num = VU_MAX_TX_BUFFER_NB; + elem[count].out_num = IOV_PER_ELEM; elem[count].out_sg = &out_sg[out_sg_count]; elem[count].in_num = 0; elem[count].in_sg = NULL; diff --git a/vu_common.h b/vu_common.h index 51639c04df14..2c2d11abb26f 100644 --- a/vu_common.h +++ b/vu_common.h @@ -9,6 +9,8 @@ #define VU_COMMON_H #include
+#define IOV_PER_ELEM (2) + static inline void *vu_eth(void *base) { return ((char *)base + VNET_HLEN); -- 2.53.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
participants (2)
-
David Gibson
-
Laurent Vivier