hw/net/net_tx_pkt.c | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-)
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>
---
hw/net/net_tx_pkt.c | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 54 insertions(+), 1 deletion(-)
diff --git a/hw/net/net_tx_pkt.c b/hw/net/net_tx_pkt.c
index 903238dca2..d3549a4378 100644
--- a/hw/net/net_tx_pkt.c
+++ b/hw/net/net_tx_pkt.c
@@ -274,7 +274,60 @@ 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);
+
+ /*
+ * Clamp payload_len to what the IP header says, not what iov_size()
+ * returns. When a short frame is padded to the Ethernet minimum of 60
+ * bytes, iov_size() includes the padding. Using that inflated size as
+ * payload_len causes net_tx_pkt_update_ip_hdr_checksum() to rewrite the
+ * IP total/payload length to include Ethernet padding, making the packet
+ * malformed (receiver may treat padding as payload or miscalculate
+ * checksums). Clamp to the guest-provided IP length instead.
+ *
+ * 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 malformed packets where ip_total_len is zero or
+ * smaller than the IP header itself. Without this check, the
+ * subtraction (ip_total_len - l3_hdr_len) would underflow (both
+ * types are unsigned), producing a huge payload_len and corrupting
+ * the packet. Fall back to the raw iov_size value in that case.
+ */
+ 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 {
+ pkt->payload_len = raw_payload_len;
+ }
+ } else {
+ pkt->payload_len = raw_payload_len;
+ }
+
pkt->payload_frags = iov_copy(&pkt->vec[NET_TX_PKT_PL_START_FRAG],
pkt->max_payload_frags,
pkt->raw, pkt->raw_frags,
---
base-commit: b83371668192a705b878e909c5ae9c1233cbd5fb
change-id: 20260624-net-tx-pkt-ip-length-padding-5a8f358933fc
Best regards,
--
Sanjeeva Yerrapureddy <y.sanjeevreddy@gmail.com>
On 2026/06/25 6:45, 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.
This breaks net_tx_pkt_get_total_len(), which also uses payload_len and
expects pkt->hdr_len + pkt->payload_len produces the size of the entire
packet. It is also unclear whether fall back to the raw iov_size() makes
sense. I think we need to define what payload_len should represent to
figure out a correct solution.
Regards,
Akihiko Odaki
>
> Signed-off-by: Sanjeeva Yerrapureddy <y.sanjeevreddy@gmail.com>
> ---
> hw/net/net_tx_pkt.c | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
> 1 file changed, 54 insertions(+), 1 deletion(-)
>
> diff --git a/hw/net/net_tx_pkt.c b/hw/net/net_tx_pkt.c
> index 903238dca2..d3549a4378 100644
> --- a/hw/net/net_tx_pkt.c
> +++ b/hw/net/net_tx_pkt.c
> @@ -274,7 +274,60 @@ 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);
> +
> + /*
> + * Clamp payload_len to what the IP header says, not what iov_size()
> + * returns. When a short frame is padded to the Ethernet minimum of 60
> + * bytes, iov_size() includes the padding. Using that inflated size as
> + * payload_len causes net_tx_pkt_update_ip_hdr_checksum() to rewrite the
> + * IP total/payload length to include Ethernet padding, making the packet
> + * malformed (receiver may treat padding as payload or miscalculate
> + * checksums). Clamp to the guest-provided IP length instead.
> + *
> + * 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 malformed packets where ip_total_len is zero or
> + * smaller than the IP header itself. Without this check, the
> + * subtraction (ip_total_len - l3_hdr_len) would underflow (both
> + * types are unsigned), producing a huge payload_len and corrupting
> + * the packet. Fall back to the raw iov_size value in that case.
> + */
> + 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 {
> + pkt->payload_len = raw_payload_len;
> + }
> + } else {
> + pkt->payload_len = raw_payload_len;
> + }
> +
> pkt->payload_frags = iov_copy(&pkt->vec[NET_TX_PKT_PL_START_FRAG],
> pkt->max_payload_frags,
> pkt->raw, pkt->raw_frags,
>
> ---
> base-commit: b83371668192a705b878e909c5ae9c1233cbd5fb
> change-id: 20260624-net-tx-pkt-ip-length-padding-5a8f358933fc
>
> Best regards,
> --
> Sanjeeva Yerrapureddy <y.sanjeevreddy@gmail.com>
>
On Thu, 25 Jun 2026 at 05:53, Akihiko Odaki <odaki@rsg.ci.i.u-tokyo.ac.jp> wrote: > > On 2026/06/25 6:45, 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. > > This breaks net_tx_pkt_get_total_len(), which also uses payload_len and > expects pkt->hdr_len + pkt->payload_len produces the size of the entire > packet. It is also unclear whether fall back to the raw iov_size() makes > sense. I think we need to define what payload_len should represent to > figure out a correct solution. See also this other recent thread about short packets: https://lore.kernel.org/qemu-devel/CAFEAcA_UhmCxJc16CHE=4ZR1+PA0=4-29=fEe+d+iUmhLTzpuQ@mail.gmail.com/ I'm not sure we're entirely clear and consistent about why and when we pad short packets (are we doing it to make our lives easier in writing the receive path in network device models? are we doing it because some net backends will hand us a 40 byte packet and a network device is entitled to expect a complete ethernet frame? other?). Do we need to distinguish (can we always?) the payload length and the padded length? I had a go at some patches that move the "pad this short packet" code around so we do it more consistently, but I don't feel very confident in them because I don't have an understanding of the overall requirements here yet. thanks -- PMM
© 2016 - 2026 Red Hat, Inc.