hw/net/e1000e_core.c | 27 ++++++++++++----- hw/net/net_tx_pkt.c | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++-- hw/net/net_tx_pkt.h | 14 +++++++++ 3 files changed, 116 insertions(+), 10 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.
net_tx_pkt_rebuild_payload() now handles three cases:
1. ip_len is valid (ip_total_len > l3_hdr_len): clamp payload_len to
the IP-declared payload length, stripping Ethernet padding.
2. ip_len is invalid and pkt->payload_len == 0: no authoritative source
is available; fall back to raw_payload_len as a safe default.
3. ip_len is invalid and pkt->payload_len != 0: a device pre-set an
authoritative value via net_tx_pkt_set_payload_len() before calling
net_tx_pkt_parse(); leave it unchanged.
Add net_tx_pkt_set_payload_len() to the public API so devices that
carry the payload length in a hardware context descriptor can provide
it before net_tx_pkt_parse() is called (case 3 above).
For e1000e, TSO super-packets have ip_len=0 as a placeholder per the
82574 datasheet §7.3.4. Use the descriptor fields to compute the
correct IP payload length before net_tx_pkt_parse():
payload_len = paylen + (hdr_len - tucss)
= L4 data + L4 header length
net_tx_pkt_rebuild_payload() then sees a non-zero payload_len and
preserves it, never reaching the raw_payload_len fallback.
Signed-off-by: Sanjeeva Yerrapureddy <y.sanjeevreddy@gmail.com>
---
Thank you Akihiko for your review and for providing detailed guidance on
calculating the payload length for e1000e and the relevant sections in
the Intel datasheet.
Changes in v3:
- Architectural redesign per Akihiko Odaki's review: remove the
device-specific ESXi/TSO heuristic from the common
net_tx_pkt_rebuild_payload() code.
- net_tx_pkt_rebuild_payload() now has three explicit cases: (1) valid
ip_len — clamp to it; (2) invalid ip_len and no pre-set value — fall
back to raw_payload_len; (3) invalid ip_len but device pre-set an
authoritative payload_len before parse — preserve it.
- Add net_tx_pkt_set_payload_len() to the public API for devices to
supply a context-derived payload length before net_tx_pkt_parse().
- Move e1000e TSO logic into e1000e_core.c: per 82574 §7.3.4, compute
payload_len = paylen + (hdr_len - tucss) and pre-set it before parse
so the common fallback is never reached for TSO super-packets.
- Link to v2: https://lore.kernel.org/qemu-devel/20260625-net-tx-pkt-ip-length-padding-v2-1-287d0ba59ec1@gmail.com
Changes in v2:
- Fix iov_copy() to use raw_payload_len instead of payload_len so
Ethernet padding is preserved in the TAP wire frame.
- Fix net_tx_pkt_get_total_len() to return iov_size() instead of
hdr_len + payload_len to avoid under-counting short frames.
- Link to v1: https://lore.kernel.org/qemu-devel/20260624-net-tx-pkt-ip-length-padding-v1-1-7a22fe9b4e40@gmail.com
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 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 payload_len reflects the IP-declared length, while the full
padded frame continues to be forwarded to TAP via raw_payload_len.
---
hw/net/e1000e_core.c | 27 ++++++++++++-----
hw/net/net_tx_pkt.c | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++--
hw/net/net_tx_pkt.h | 14 +++++++++
3 files changed, 116 insertions(+), 10 deletions(-)
diff --git a/hw/net/e1000e_core.c b/hw/net/e1000e_core.c
index 46e156a5dd..0f0f18c675 100644
--- a/hw/net/e1000e_core.c
+++ b/hw/net/e1000e_core.c
@@ -717,14 +717,27 @@ e1000e_process_tx_desc(E1000ECore *core,
}
if (eop) {
- if (!tx->skip_cp && net_tx_pkt_parse(tx->tx_pkt)) {
- if (e1000x_vlan_enabled(core->mac) &&
- e1000x_is_vlan_txd(txd_lower)) {
- net_tx_pkt_setup_vlan_header_ex(tx->tx_pkt,
- le16_to_cpu(dp->upper.fields.special), core->mac[VET]);
+ if (!tx->skip_cp) {
+ if (tx->props.tse && tx->cptse) {
+ /*
+ * Per 82574 §7.3.4, ip_len is zero in TSO super-packets.
+ * Pre-set payload_len before parse so
+ * net_tx_pkt_rebuild_payload() preserves it (case 3).
+ * L4 header length = hdr_len − tucss
+ * L4 data = paylen
+ */
+ net_tx_pkt_set_payload_len(tx->tx_pkt,
+ tx->props.paylen + tx->props.hdr_len - tx->props.tucss);
}
- if (e1000e_tx_pkt_send(core, tx, queue_index)) {
- e1000e_on_tx_done_update_stats(core, tx->tx_pkt);
+ if (net_tx_pkt_parse(tx->tx_pkt)) {
+ if (e1000x_vlan_enabled(core->mac) &&
+ e1000x_is_vlan_txd(txd_lower)) {
+ net_tx_pkt_setup_vlan_header_ex(tx->tx_pkt,
+ le16_to_cpu(dp->upper.fields.special), core->mac[VET]);
+ }
+ if (e1000e_tx_pkt_send(core, tx, queue_index)) {
+ e1000e_on_tx_done_update_stats(core, tx->tx_pkt);
+ }
}
}
diff --git a/hw/net/net_tx_pkt.c b/hw/net/net_tx_pkt.c
index 903238dca2..be09727ad6 100644
--- a/hw/net/net_tx_pkt.c
+++ b/hw/net/net_tx_pkt.c
@@ -274,11 +274,77 @@ 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;
+ if (ip_total_len > l3_hdr_len) {
+ /*
+ * ip_len is valid — clamp to the IP-declared payload length to
+ * strip any Ethernet minimum-frame padding appended by the NIC.
+ */
+ pkt->payload_len = MIN(raw_payload_len, ip_total_len - l3_hdr_len);
+ } else if (!pkt->payload_len) {
+ /* No valid ip_len and no device-specific value was pre-set. */
+ pkt->payload_len = raw_payload_len;
+ }
+ /* else: device pre-set payload_len before parse; keep it. */
+ } 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);
+ if (ip6_payload_len > ext_hdr_len) {
+ /*
+ * Clamp to the IPv6-declared payload length minus any extension
+ * headers already parsed into l3_hdr_len.
+ */
+ pkt->payload_len = MIN(raw_payload_len,
+ ip6_payload_len - ext_hdr_len);
+ } else if (!pkt->payload_len) {
+ /* ip6_plen is zero (jumbogram) and no pre-set value. */
+ pkt->payload_len = raw_payload_len;
+ }
+ /* else: device pre-set payload_len before parse — keep it. */
+ } else {
+ if (!pkt->payload_len) {
+ 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 +494,20 @@ 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_set_payload_len(struct NetTxPkt *pkt, uint32_t payload_len)
+{
+ assert(pkt);
+ pkt->payload_len = payload_len;
}
void net_tx_pkt_dump(struct NetTxPkt *pkt)
diff --git a/hw/net/net_tx_pkt.h b/hw/net/net_tx_pkt.h
index 0a716e74a5..1a1c5edc2d 100644
--- a/hw/net/net_tx_pkt.h
+++ b/hw/net/net_tx_pkt.h
@@ -133,6 +133,20 @@ bool net_tx_pkt_update_sctp_checksum(struct NetTxPkt *pkt);
*/
size_t net_tx_pkt_get_total_len(struct NetTxPkt *pkt);
+/**
+ * pre-set the payload length before net_tx_pkt_parse()
+ *
+ * Devices that carry an authoritative payload length in a hardware context
+ * descriptor (e.g. e1000e PAYLEN for TSO) should call this before
+ * net_tx_pkt_parse(). net_tx_pkt_rebuild_payload() will then skip its
+ * ip_len=0 fallback and preserve this value.
+ *
+ * @pkt: packet
+ * @payload_len: IP payload length (L4 header + L4 data)
+ *
+ */
+void net_tx_pkt_set_payload_len(struct NetTxPkt *pkt, uint32_t payload_len);
+
/**
* get packet type
*
---
base-commit: b83371668192a705b878e909c5ae9c1233cbd5fb
change-id: 20260624-net-tx-pkt-ip-length-padding-5a8f358933fc
Best regards,
--
Sanjeeva Yerrapureddy <y.sanjeevreddy@gmail.com>
On 2026/06/27 22:18, 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.
>
> net_tx_pkt_rebuild_payload() now handles three cases:
>
> 1. ip_len is valid (ip_total_len > l3_hdr_len): clamp payload_len to
> the IP-declared payload length, stripping Ethernet padding.
>
> 2. ip_len is invalid and pkt->payload_len == 0: no authoritative source
> is available; fall back to raw_payload_len as a safe default.
>
> 3. ip_len is invalid and pkt->payload_len != 0: a device pre-set an
> authoritative value via net_tx_pkt_set_payload_len() before calling
> net_tx_pkt_parse(); leave it unchanged.
>
> Add net_tx_pkt_set_payload_len() to the public API so devices that
> carry the payload length in a hardware context descriptor can provide
> it before net_tx_pkt_parse() is called (case 3 above).
>
> For e1000e, TSO super-packets have ip_len=0 as a placeholder per the
> 82574 datasheet §7.3.4. Use the descriptor fields to compute the
> correct IP payload length before net_tx_pkt_parse():
>
> payload_len = paylen + (hdr_len - tucss)
> = L4 data + L4 header length
>
> net_tx_pkt_rebuild_payload() then sees a non-zero payload_len and
> preserves it, never reaching the raw_payload_len fallback.
>
> Signed-off-by: Sanjeeva Yerrapureddy <y.sanjeevreddy@gmail.com>
> ---
> Thank you Akihiko for your review and for providing detailed guidance on
> calculating the payload length for e1000e and the relevant sections in
> the Intel datasheet.
>
> Changes in v3:
> - Architectural redesign per Akihiko Odaki's review: remove the
> device-specific ESXi/TSO heuristic from the common
> net_tx_pkt_rebuild_payload() code.
> - net_tx_pkt_rebuild_payload() now has three explicit cases: (1) valid
> ip_len — clamp to it; (2) invalid ip_len and no pre-set value — fall
> back to raw_payload_len; (3) invalid ip_len but device pre-set an
> authoritative payload_len before parse — preserve it.
> - Add net_tx_pkt_set_payload_len() to the public API for devices to
> supply a context-derived payload length before net_tx_pkt_parse().
> - Move e1000e TSO logic into e1000e_core.c: per 82574 §7.3.4, compute
> payload_len = paylen + (hdr_len - tucss) and pre-set it before parse
> so the common fallback is never reached for TSO super-packets.
> - Link to v2: https://lore.kernel.org/qemu-devel/20260625-net-tx-pkt-ip-length-padding-v2-1-287d0ba59ec1@gmail.com
>
> Changes in v2:
> - Fix iov_copy() to use raw_payload_len instead of payload_len so
> Ethernet padding is preserved in the TAP wire frame.
> - Fix net_tx_pkt_get_total_len() to return iov_size() instead of
> hdr_len + payload_len to avoid under-counting short frames.
> - Link to v1: https://lore.kernel.org/qemu-devel/20260624-net-tx-pkt-ip-length-padding-v1-1-7a22fe9b4e40@gmail.com
>
> 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 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.
The padded bytes are already present in the buffer supplied by the
guest; e1000e is not the component adding them here.
More generally, the packet passed through net_tx_pkt is still in an
intermediate form. If it were already a fully defined wire packet, QEMU
would not need to fix up headers and checksums later. Since that
intermediate form is not defined generically, the common code cannot
reliably decide whether the authoritative payload length is the raw
buffer size or the IP header field for every device and offload mode.
So I do not think net_tx_pkt_rebuild_payload() should grow a generic
algorithm for choosing payload_len from the IP header. That choice
belongs in the device-specific code that understands the descriptor
format. For e1000e, please determine the payload length from the e1000e
context descriptor before calling net_tx_pkt_parse(), and keep the
common net_tx_pkt code unchanged.
Regards,
Akihiko Odaki
>
> This fix does not change when or where padding is applied. It only
> ensures payload_len reflects the IP-declared length, while the full
> padded frame continues to be forwarded to TAP via raw_payload_len.
> ---
> hw/net/e1000e_core.c | 27 ++++++++++++-----
> hw/net/net_tx_pkt.c | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++--
> hw/net/net_tx_pkt.h | 14 +++++++++
> 3 files changed, 116 insertions(+), 10 deletions(-)
>
> diff --git a/hw/net/e1000e_core.c b/hw/net/e1000e_core.c
> index 46e156a5dd..0f0f18c675 100644
> --- a/hw/net/e1000e_core.c
> +++ b/hw/net/e1000e_core.c
> @@ -717,14 +717,27 @@ e1000e_process_tx_desc(E1000ECore *core,
> }
>
> if (eop) {
> - if (!tx->skip_cp && net_tx_pkt_parse(tx->tx_pkt)) {
> - if (e1000x_vlan_enabled(core->mac) &&
> - e1000x_is_vlan_txd(txd_lower)) {
> - net_tx_pkt_setup_vlan_header_ex(tx->tx_pkt,
> - le16_to_cpu(dp->upper.fields.special), core->mac[VET]);
> + if (!tx->skip_cp) {
> + if (tx->props.tse && tx->cptse) {
> + /*
> + * Per 82574 §7.3.4, ip_len is zero in TSO super-packets.
> + * Pre-set payload_len before parse so
> + * net_tx_pkt_rebuild_payload() preserves it (case 3).
> + * L4 header length = hdr_len − tucss
> + * L4 data = paylen
> + */
> + net_tx_pkt_set_payload_len(tx->tx_pkt,
> + tx->props.paylen + tx->props.hdr_len - tx->props.tucss);
> }
> - if (e1000e_tx_pkt_send(core, tx, queue_index)) {
> - e1000e_on_tx_done_update_stats(core, tx->tx_pkt);
> + if (net_tx_pkt_parse(tx->tx_pkt)) {
> + if (e1000x_vlan_enabled(core->mac) &&
> + e1000x_is_vlan_txd(txd_lower)) {
> + net_tx_pkt_setup_vlan_header_ex(tx->tx_pkt,
> + le16_to_cpu(dp->upper.fields.special), core->mac[VET]);
> + }
> + if (e1000e_tx_pkt_send(core, tx, queue_index)) {
> + e1000e_on_tx_done_update_stats(core, tx->tx_pkt);
> + }
> }
> }
>
> diff --git a/hw/net/net_tx_pkt.c b/hw/net/net_tx_pkt.c
> index 903238dca2..be09727ad6 100644
> --- a/hw/net/net_tx_pkt.c
> +++ b/hw/net/net_tx_pkt.c
> @@ -274,11 +274,77 @@ 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;
> + if (ip_total_len > l3_hdr_len) {
> + /*
> + * ip_len is valid — clamp to the IP-declared payload length to
> + * strip any Ethernet minimum-frame padding appended by the NIC.
> + */
> + pkt->payload_len = MIN(raw_payload_len, ip_total_len - l3_hdr_len);
> + } else if (!pkt->payload_len) {
> + /* No valid ip_len and no device-specific value was pre-set. */
> + pkt->payload_len = raw_payload_len;
> + }
> + /* else: device pre-set payload_len before parse; keep it. */
> + } 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);
> + if (ip6_payload_len > ext_hdr_len) {
> + /*
> + * Clamp to the IPv6-declared payload length minus any extension
> + * headers already parsed into l3_hdr_len.
> + */
> + pkt->payload_len = MIN(raw_payload_len,
> + ip6_payload_len - ext_hdr_len);
> + } else if (!pkt->payload_len) {
> + /* ip6_plen is zero (jumbogram) and no pre-set value. */
> + pkt->payload_len = raw_payload_len;
> + }
> + /* else: device pre-set payload_len before parse — keep it. */
> + } else {
> + if (!pkt->payload_len) {
> + 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 +494,20 @@ 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_set_payload_len(struct NetTxPkt *pkt, uint32_t payload_len)
> +{
> + assert(pkt);
> + pkt->payload_len = payload_len;
> }
>
> void net_tx_pkt_dump(struct NetTxPkt *pkt)
> diff --git a/hw/net/net_tx_pkt.h b/hw/net/net_tx_pkt.h
> index 0a716e74a5..1a1c5edc2d 100644
> --- a/hw/net/net_tx_pkt.h
> +++ b/hw/net/net_tx_pkt.h
> @@ -133,6 +133,20 @@ bool net_tx_pkt_update_sctp_checksum(struct NetTxPkt *pkt);
> */
> size_t net_tx_pkt_get_total_len(struct NetTxPkt *pkt);
>
> +/**
> + * pre-set the payload length before net_tx_pkt_parse()
> + *
> + * Devices that carry an authoritative payload length in a hardware context
> + * descriptor (e.g. e1000e PAYLEN for TSO) should call this before
> + * net_tx_pkt_parse(). net_tx_pkt_rebuild_payload() will then skip its
> + * ip_len=0 fallback and preserve this value.
> + *
> + * @pkt: packet
> + * @payload_len: IP payload length (L4 header + L4 data)
> + *
> + */
> +void net_tx_pkt_set_payload_len(struct NetTxPkt *pkt, uint32_t payload_len);
> +
> /**
> * get packet type
> *
>
> ---
> 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.