hw/net/net_tx_pkt.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 3 deletions(-)
When a guest transmits a short Ethernet frame, iov_size() returns the
wire length including any Ethernet padding bytes added to reach the
minimum 60-byte frame size. net_tx_pkt_rebuild_payload() used that
inflated size directly as payload_len. net_tx_pkt_update_ip_hdr_
checksum() then rewrites the IP Total Length (IPv4) or Payload Length
(IPv6) field using this value, producing a malformed packet on the
wire: the receiver interprets Ethernet padding bytes as IP payload.
In practice this breaks protocol stacks that validate IP lengths
strictly. A nested ESXi host running on OpenStack using the e1000e
emulation sends short TCP ACK packets in response to a Windows guest's
virtio TLS client hello, which get padded to 60 bytes; the inflated IP
Total Length corrupts the IP header seen by Windows, causing TLS
handshakes and TCP connections to fail.
Fix net_tx_pkt_rebuild_payload() to read the IP header's declared
length and clamp payload_len accordingly:
IPv4: ip_total_len - l3_hdr_len
IPv6: ip6_plen adjusted for parsed extension headers
Guard both paths against malformed guest packets where the header-
declared length is shorter than the IP header itself; without the
guard the unsigned subtraction would underflow, producing a huge
payload_len. Fall back to the raw iov_size() value in that case.
Signed-off-by: Sanjeeva Yerrapureddy <y.sanjeevreddy@gmail.com>
---
Changes in v2:
- Fix iov_copy() to use raw_payload_len (full wire size including Ethernet
padding) instead of the IP-clamped payload_len; v1 silently dropped
padding bytes from the TAP frame, causing short frames to be transmitted
with incorrect wire size (addresses part 1 of Akihiko Odaki's review).
- Fix net_tx_pkt_get_total_len() to return iov_size(pkt->raw, pkt->raw_frags)
instead of hdr_len + payload_len, which under-counted for short padded
frames (addresses part 1 of Akihiko Odaki's review).
- Expand comments to make the two sizes explicit: payload_len is the
IP-declared payload length used only for header/checksum operations;
raw_payload_len is the full wire size used for iov_copy and total_len.
- Document the LSO/TSO super-packet (ip_len=0) and IPv6 jumbogram cases
as known real-world triggers for the fallback path.
- Link to v1: https://lore.kernel.org/qemu-devel/20260624-net-tx-pkt-ip-length-padding-v1-1-7a22fe9b4e40@gmail.com
On the fallback to raw_payload_len when ip_len is too small (Akihiko Odaki):
The fallback is necessary for LSO/TSO super-packets where the ESXi e1000e
driver sets ip_len=0 as a placeholder in the unfragmented super-packet
header before the NIC performs segmentation. The following log traces
(captured with a temporary debug printk and then removed) confirm this
path is triggered in practice:
IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len=14793), using raw size
IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len=16445), using raw size
IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len= 5864), using raw size
IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len= 1528), using raw size
IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len= 3870), using raw size
IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len= 2621), using raw size
IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len= 1659), using raw size
IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len= 1528), using raw size
IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len= 4610), using raw size
IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len= 1830), using raw size
For these packets the TCP segmentation engine drives the loop and does
not rely on payload_len to determine the payload extent, so using
raw_payload_len is correct and safe.
On the relationship to the short-packet receive-path discussion (Peter Maydell):
https://lore.kernel.org/qemu-devel/CAFEAcA_UhmCxJc16CHE=4ZR1+PA0=4-29=fEe+d+iUmhLTzpuQ@mail.gmail.com/
The linked thread concerns the receive path — whether tap_send is the
right place to pad short incoming frames for guest NIC models. This
patch addresses the transmit path: the guest NIC adds Ethernet padding
correctly, but net_tx_pkt_rebuild_payload was then using the padded
wire size to compute payload_len, causing net_tx_pkt_update_ip_hdr_
checksum to rewrite ip_len to include the padding bytes.
This fix does not change when or where padding is applied. It only
ensures that payload_len reflects the IP-declared payload length, while
the full padded frame continues to be forwarded to the TAP via
iov_copy using raw_payload_len.
---
hw/net/net_tx_pkt.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 88 insertions(+), 3 deletions(-)
diff --git a/hw/net/net_tx_pkt.c b/hw/net/net_tx_pkt.c
index 903238dca2..2dcbf23354 100644
--- a/hw/net/net_tx_pkt.c
+++ b/hw/net/net_tx_pkt.c
@@ -274,11 +274,89 @@ static bool net_tx_pkt_parse_headers(struct NetTxPkt *pkt)
static void net_tx_pkt_rebuild_payload(struct NetTxPkt *pkt)
{
- pkt->payload_len = iov_size(pkt->raw, pkt->raw_frags) - pkt->hdr_len;
+ size_t raw_payload_len = iov_size(pkt->raw, pkt->raw_frags) - pkt->hdr_len;
+ struct iovec *l2hdr = &pkt->vec[NET_TX_PKT_L2HDR_FRAG];
+ uint16_t l3_proto = eth_get_l3_proto(l2hdr, 1, l2hdr->iov_len);
+
+ /*
+ * payload_len tracks the IP-declared payload size. It is used by
+ * net_tx_pkt_update_ip_hdr_checksum() to rewrite ip_len and by the L4
+ * pseudo-header checksum helpers. It must NOT include Ethernet
+ * minimum-frame padding: if the guest sends a short frame (e.g. a pure
+ * TCP ACK of 54 bytes) padded to 60 bytes, iov_size() returns 60 but the
+ * IP header still reports the payload as 20 bytes. Using the inflated size
+ * would cause ip_len to be rewritten to include the padding, making the
+ * packet malformed on the receiver.
+ *
+ * Note: the actual iov_copy below still copies raw_payload_len bytes
+ * (the full wire payload including padding) into the payload fragments so
+ * the TAP receives a correctly-sized Ethernet frame. The receiver's IP
+ * stack uses ip_len to find the real payload boundary and ignores the
+ * trailing padding bytes, which is exactly how a real NIC behaves.
+ *
+ * IPv4: ip_len is the total length including the IP header.
+ * IPv6: ip6_plen is the payload length after the base 40-byte header
+ * (extension headers are included in ip6_plen but also in
+ * l3_hdr_len, so subtract them out).
+ */
+ if (l3_proto == ETH_P_IP) {
+ uint16_t ip_total_len = be16_to_cpu(pkt->l3_hdr.ip.ip_len);
+ size_t l3_hdr_len = pkt->vec[NET_TX_PKT_L3HDR_FRAG].iov_len;
+ /*
+ * Guard against ip_total_len that does not cover the IP header.
+ * Subtracting would underflow (both types unsigned), producing a
+ * huge payload_len. Fall back to raw_payload_len in that case.
+ *
+ * Known trigger: some LSO/TSO implementations (e.g. ESXi e1000e
+ * driver) set ip_len=0 in the super-packet header as a placeholder
+ * when offloading segmentation to the NIC. In that case
+ * raw_payload_len correctly reflects the full super-packet payload
+ * from the TX descriptors, and the TCP segmentation path does not
+ * rely on payload_len to drive the loop, so this is safe.
+ */
+ if (ip_total_len > l3_hdr_len) {
+ pkt->payload_len = MIN(raw_payload_len, ip_total_len - l3_hdr_len);
+ } else {
+ pkt->payload_len = raw_payload_len;
+ }
+ } else if (l3_proto == ETH_P_IPV6) {
+ uint16_t ip6_payload_len = be16_to_cpu(pkt->l3_hdr.ip6.ip6_plen);
+ size_t l3_hdr_len = pkt->vec[NET_TX_PKT_L3HDR_FRAG].iov_len;
+ size_t ext_hdr_len = l3_hdr_len - sizeof(struct ip6_header);
+ /*
+ * Guard against malformed packets where ip6_plen is zero or does
+ * not cover the extension headers that were already parsed. Without
+ * this check, the subtraction (ip6_payload_len - ext_hdr_len) would
+ * underflow (unsigned), producing a huge payload_len. Fall back to
+ * the raw iov_size value in that case.
+ */
+ if (ip6_payload_len > ext_hdr_len) {
+ pkt->payload_len = MIN(raw_payload_len,
+ ip6_payload_len - ext_hdr_len);
+ } else {
+ /*
+ * ip6_plen=0 is valid for IPv6 jumbograms (payload length
+ * carried in a Hop-by-Hop option), but QEMU does not support
+ * jumbograms so raw_payload_len is the best we can do.
+ * Also guards against malformed packets where ip6_plen is
+ * smaller than the extension headers already parsed.
+ */
+ pkt->payload_len = raw_payload_len;
+ }
+ } else {
+ pkt->payload_len = raw_payload_len;
+ }
+
+ /*
+ * Copy the full raw_payload_len bytes (including any Ethernet padding)
+ * so the wire frame sent to the TAP has the correct size. payload_len
+ * (the IP-declared value set above) is used only for header/checksum
+ * operations; raw_payload_len preserves the original frame size.
+ */
pkt->payload_frags = iov_copy(&pkt->vec[NET_TX_PKT_PL_START_FRAG],
pkt->max_payload_frags,
pkt->raw, pkt->raw_frags,
- pkt->hdr_len, pkt->payload_len);
+ pkt->hdr_len, raw_payload_len);
}
bool net_tx_pkt_parse(struct NetTxPkt *pkt)
@@ -428,7 +506,14 @@ size_t net_tx_pkt_get_total_len(struct NetTxPkt *pkt)
{
assert(pkt);
- return pkt->hdr_len + pkt->payload_len;
+ /*
+ * Use the raw iov size rather than hdr_len + payload_len. payload_len
+ * reflects the IP-declared payload length (excluding any Ethernet minimum-
+ * frame padding), so hdr_len + payload_len would under-count for short
+ * frames. pkt->raw always holds the exact bytes that arrived from the
+ * guest TX descriptors, padding included, giving the true wire size.
+ */
+ return iov_size(pkt->raw, pkt->raw_frags);
}
void net_tx_pkt_dump(struct NetTxPkt *pkt)
---
base-commit: b83371668192a705b878e909c5ae9c1233cbd5fb
change-id: 20260624-net-tx-pkt-ip-length-padding-5a8f358933fc
Best regards,
--
Sanjeeva Yerrapureddy <y.sanjeevreddy@gmail.com>
On 2026/06/26 10:10, Sanjeeva Yerrapureddy wrote:
> When a guest transmits a short Ethernet frame, iov_size() returns the
> wire length including any Ethernet padding bytes added to reach the
> minimum 60-byte frame size. net_tx_pkt_rebuild_payload() used that
> inflated size directly as payload_len. net_tx_pkt_update_ip_hdr_
> checksum() then rewrites the IP Total Length (IPv4) or Payload Length
> (IPv6) field using this value, producing a malformed packet on the
> wire: the receiver interprets Ethernet padding bytes as IP payload.
>
> In practice this breaks protocol stacks that validate IP lengths
> strictly. A nested ESXi host running on OpenStack using the e1000e
> emulation sends short TCP ACK packets in response to a Windows guest's
> virtio TLS client hello, which get padded to 60 bytes; the inflated IP
> Total Length corrupts the IP header seen by Windows, causing TLS
> handshakes and TCP connections to fail.
>
> Fix net_tx_pkt_rebuild_payload() to read the IP header's declared
> length and clamp payload_len accordingly:
>
> IPv4: ip_total_len - l3_hdr_len
> IPv6: ip6_plen adjusted for parsed extension headers
>
> Guard both paths against malformed guest packets where the header-
> declared length is shorter than the IP header itself; without the
> guard the unsigned subtraction would underflow, producing a huge
> payload_len. Fall back to the raw iov_size() value in that case.
>
> Signed-off-by: Sanjeeva Yerrapureddy <y.sanjeevreddy@gmail.com>
> ---
> Changes in v2:
> - Fix iov_copy() to use raw_payload_len (full wire size including Ethernet
> padding) instead of the IP-clamped payload_len; v1 silently dropped
> padding bytes from the TAP frame, causing short frames to be transmitted
> with incorrect wire size (addresses part 1 of Akihiko Odaki's review).
> - Fix net_tx_pkt_get_total_len() to return iov_size(pkt->raw, pkt->raw_frags)
> instead of hdr_len + payload_len, which under-counted for short padded
> frames (addresses part 1 of Akihiko Odaki's review).
> - Expand comments to make the two sizes explicit: payload_len is the
> IP-declared payload length used only for header/checksum operations;
> raw_payload_len is the full wire size used for iov_copy and total_len.
> - Document the LSO/TSO super-packet (ip_len=0) and IPv6 jumbogram cases
> as known real-world triggers for the fallback path.
> - Link to v1: https://lore.kernel.org/qemu-devel/20260624-net-tx-pkt-ip-length-padding-v1-1-7a22fe9b4e40@gmail.com
>
> On the fallback to raw_payload_len when ip_len is too small (Akihiko Odaki):
>
> The fallback is necessary for LSO/TSO super-packets where the ESXi e1000e
> driver sets ip_len=0 as a placeholder in the unfragmented super-packet
> header before the NIC performs segmentation. The following log traces
> (captured with a temporary debug printk and then removed) confirm this
> path is triggered in practice:
>
> IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len=14793), using raw size
> IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len=16445), using raw size
> IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len= 5864), using raw size
> IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len= 1528), using raw size
> IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len= 3870), using raw size
> IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len= 2621), using raw size
> IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len= 1659), using raw size
> IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len= 1528), using raw size
> IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len= 4610), using raw size
> IPv4 ip_len=0 <= IP hdr len 20 (raw_payload_len= 1830), using raw size
>
> For these packets the TCP segmentation engine drives the loop and does
> not rely on payload_len to determine the payload extent, so using
> raw_payload_len is correct and safe.
I think this should be based on the device specification rather than on
a heuristic that happens to match one driver.
For 82574/e1000e, the datasheet explicitly describes this case.
Section 7.3.4, "TCP Segmentation Source Data", says that the IPv4
header "Length should be set to zero"; it says the same for the IPv6
header length. The per-frame length is then generated by the device as
part of TSO, as described in section 7.3.6.2, "IP/TCP/UDP Header
Updating".
For IPv4, the first and subsequent frames use:
IP Total Length = MSS + HDRLEN - IPCSS
and the last frame uses:
IP Total Length = (last frame payload bytes + HDRLEN) - IPCSS
where the last-frame payload is derived from PAYLEN - (N * MSS).
Those variables come from the e1000e transmit context descriptor, not
from the IP header. Section 7.2.10.2 describes the context transmit
descriptor, section 7.2.10.2.1 defines IPCSS/TUCSS, section 7.2.10.3
defines MSS, section 7.2.10.3.1 defines HDRLEN, and section 7.2.10.4
defines PAYLEN.
So I do not think common net_tx_pkt code should define a generic policy
for packets whose IP length field is invalid. Outside a device-specific
offload context, the correct payload length is not well-defined. For
e1000e TSO, however, the hardware contract gives us the needed
device-specific state, so the e1000e path should derive the length from
the TSO context instead of relying on raw_payload_len as a common-code
fallback.
Regards,
Akihiko Odaki
>
> On the relationship to the short-packet receive-path discussion (Peter Maydell):
> https://lore.kernel.org/qemu-devel/CAFEAcA_UhmCxJc16CHE=4ZR1+PA0=4-29=fEe+d+iUmhLTzpuQ@mail.gmail.com/
>
> The linked thread concerns the receive path — whether tap_send is the
> right place to pad short incoming frames for guest NIC models. This
> patch addresses the transmit path: the guest NIC adds Ethernet padding
> correctly, but net_tx_pkt_rebuild_payload was then using the padded
> wire size to compute payload_len, causing net_tx_pkt_update_ip_hdr_
> checksum to rewrite ip_len to include the padding bytes.
>
> This fix does not change when or where padding is applied. It only
> ensures that payload_len reflects the IP-declared payload length, while
> the full padded frame continues to be forwarded to the TAP via
> iov_copy using raw_payload_len.
> ---
> hw/net/net_tx_pkt.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++--
> 1 file changed, 88 insertions(+), 3 deletions(-)
>
> diff --git a/hw/net/net_tx_pkt.c b/hw/net/net_tx_pkt.c
> index 903238dca2..2dcbf23354 100644
> --- a/hw/net/net_tx_pkt.c
> +++ b/hw/net/net_tx_pkt.c
> @@ -274,11 +274,89 @@ static bool net_tx_pkt_parse_headers(struct NetTxPkt *pkt)
>
> static void net_tx_pkt_rebuild_payload(struct NetTxPkt *pkt)
> {
> - pkt->payload_len = iov_size(pkt->raw, pkt->raw_frags) - pkt->hdr_len;
> + size_t raw_payload_len = iov_size(pkt->raw, pkt->raw_frags) - pkt->hdr_len;
> + struct iovec *l2hdr = &pkt->vec[NET_TX_PKT_L2HDR_FRAG];
> + uint16_t l3_proto = eth_get_l3_proto(l2hdr, 1, l2hdr->iov_len);
> +
> + /*
> + * payload_len tracks the IP-declared payload size. It is used by
> + * net_tx_pkt_update_ip_hdr_checksum() to rewrite ip_len and by the L4
> + * pseudo-header checksum helpers. It must NOT include Ethernet
> + * minimum-frame padding: if the guest sends a short frame (e.g. a pure
> + * TCP ACK of 54 bytes) padded to 60 bytes, iov_size() returns 60 but the
> + * IP header still reports the payload as 20 bytes. Using the inflated size
> + * would cause ip_len to be rewritten to include the padding, making the
> + * packet malformed on the receiver.
> + *
> + * Note: the actual iov_copy below still copies raw_payload_len bytes
> + * (the full wire payload including padding) into the payload fragments so
> + * the TAP receives a correctly-sized Ethernet frame. The receiver's IP
> + * stack uses ip_len to find the real payload boundary and ignores the
> + * trailing padding bytes, which is exactly how a real NIC behaves.
> + *
> + * IPv4: ip_len is the total length including the IP header.
> + * IPv6: ip6_plen is the payload length after the base 40-byte header
> + * (extension headers are included in ip6_plen but also in
> + * l3_hdr_len, so subtract them out).
> + */
> + if (l3_proto == ETH_P_IP) {
> + uint16_t ip_total_len = be16_to_cpu(pkt->l3_hdr.ip.ip_len);
> + size_t l3_hdr_len = pkt->vec[NET_TX_PKT_L3HDR_FRAG].iov_len;
> + /*
> + * Guard against ip_total_len that does not cover the IP header.
> + * Subtracting would underflow (both types unsigned), producing a
> + * huge payload_len. Fall back to raw_payload_len in that case.
> + *
> + * Known trigger: some LSO/TSO implementations (e.g. ESXi e1000e
> + * driver) set ip_len=0 in the super-packet header as a placeholder
> + * when offloading segmentation to the NIC. In that case
> + * raw_payload_len correctly reflects the full super-packet payload
> + * from the TX descriptors, and the TCP segmentation path does not
> + * rely on payload_len to drive the loop, so this is safe.
> + */
> + if (ip_total_len > l3_hdr_len) {
> + pkt->payload_len = MIN(raw_payload_len, ip_total_len - l3_hdr_len);
> + } else {
> + pkt->payload_len = raw_payload_len;
> + }
> + } else if (l3_proto == ETH_P_IPV6) {
> + uint16_t ip6_payload_len = be16_to_cpu(pkt->l3_hdr.ip6.ip6_plen);
> + size_t l3_hdr_len = pkt->vec[NET_TX_PKT_L3HDR_FRAG].iov_len;
> + size_t ext_hdr_len = l3_hdr_len - sizeof(struct ip6_header);
> + /*
> + * Guard against malformed packets where ip6_plen is zero or does
> + * not cover the extension headers that were already parsed. Without
> + * this check, the subtraction (ip6_payload_len - ext_hdr_len) would
> + * underflow (unsigned), producing a huge payload_len. Fall back to
> + * the raw iov_size value in that case.
> + */
> + if (ip6_payload_len > ext_hdr_len) {
> + pkt->payload_len = MIN(raw_payload_len,
> + ip6_payload_len - ext_hdr_len);
> + } else {
> + /*
> + * ip6_plen=0 is valid for IPv6 jumbograms (payload length
> + * carried in a Hop-by-Hop option), but QEMU does not support
> + * jumbograms so raw_payload_len is the best we can do.
> + * Also guards against malformed packets where ip6_plen is
> + * smaller than the extension headers already parsed.
> + */
> + pkt->payload_len = raw_payload_len;
> + }
> + } else {
> + pkt->payload_len = raw_payload_len;
> + }
> +
> + /*
> + * Copy the full raw_payload_len bytes (including any Ethernet padding)
> + * so the wire frame sent to the TAP has the correct size. payload_len
> + * (the IP-declared value set above) is used only for header/checksum
> + * operations; raw_payload_len preserves the original frame size.
> + */
> pkt->payload_frags = iov_copy(&pkt->vec[NET_TX_PKT_PL_START_FRAG],
> pkt->max_payload_frags,
> pkt->raw, pkt->raw_frags,
> - pkt->hdr_len, pkt->payload_len);
> + pkt->hdr_len, raw_payload_len);
> }
>
> bool net_tx_pkt_parse(struct NetTxPkt *pkt)
> @@ -428,7 +506,14 @@ size_t net_tx_pkt_get_total_len(struct NetTxPkt *pkt)
> {
> assert(pkt);
>
> - return pkt->hdr_len + pkt->payload_len;
> + /*
> + * Use the raw iov size rather than hdr_len + payload_len. payload_len
> + * reflects the IP-declared payload length (excluding any Ethernet minimum-
> + * frame padding), so hdr_len + payload_len would under-count for short
> + * frames. pkt->raw always holds the exact bytes that arrived from the
> + * guest TX descriptors, padding included, giving the true wire size.
> + */
> + return iov_size(pkt->raw, pkt->raw_frags);
> }
>
> void net_tx_pkt_dump(struct NetTxPkt *pkt)
>
> ---
> base-commit: b83371668192a705b878e909c5ae9c1233cbd5fb
> change-id: 20260624-net-tx-pkt-ip-length-padding-5a8f358933fc
>
> Best regards,
> --
> Sanjeeva Yerrapureddy <y.sanjeevreddy@gmail.com>
>
© 2016 - 2026 Red Hat, Inc.