]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
rtase: Workaround for TX hang caused by hardware packet parsing
authorJustin Lai <justinlai0215@realtek.com>
Thu, 9 Jul 2026 10:34:56 +0000 (18:34 +0800)
committerJakub Kicinski <kuba@kernel.org>
Tue, 21 Jul 2026 18:30:20 +0000 (11:30 -0700)
The hardware performs packet parsing before packet transmission.
Parsing incomplete IPv4, IPv6, TCP, or UDP headers may trigger a TX
hang because the hardware parser expects additional protocol header
data that is not present in the packet.

The hardware performs additional PTP parsing on UDP packets identified
by destination ports 319/320 at the expected UDP destination port
offset.

If such a packet has transport data smaller than RTASE_MIN_PAD_LEN,
the hardware parser expects additional packet data and may trigger a
TX hang.

To avoid these hardware issues, the driver applies the following
workarounds.

Drop malformed packets that may trigger this hardware issue before
transmission.

For IPv4 non-initial fragments, the hardware does not check the
fragment offset before parsing the expected transport header location.
As a result, these packets are still subject to transport header
parsing even though they do not contain a transport header. If the
transport data is shorter than the minimum transport header required
by the hardware parser, pad the transport data to the minimum
transport header length required by the hardware parser. Packets that
also match the hardware PTP parsing conditions continue to follow the
corresponding workaround.

For IPv6 fragmented packets, neither of the above hardware issues
occurs because the hardware only continues packet parsing when the
IPv6 Base Header Next Header field directly indicates UDP. Packets
carrying a Fragment Header do not continue through the subsequent
packet parsing stages.

For packets identified for hardware PTP parsing, pad the transport
data so it reaches RTASE_MIN_PAD_LEN before transmission.

Fixes: d6e882b89fdf ("rtase: Implement .ndo_start_xmit function")
Cc: stable@vger.kernel.org
Signed-off-by: Justin Lai <justinlai0215@realtek.com>
Reviewed-by: Simon Horman <horms@kernel.org>
Link: https://patch.msgid.link/20260709103456.83789-1-justinlai0215@realtek.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
drivers/net/ethernet/realtek/rtase/rtase.h
drivers/net/ethernet/realtek/rtase/rtase_main.c

index 9bd6872474c11bd927a0938c0243cebdf3c7cda6..03b12d83f6e9dfb48f0767d7222bea4848c0f3d0 100644 (file)
@@ -192,6 +192,12 @@ enum rtase_sw_flag_content {
        RTASE_SWF_MSIX_ENABLED = BIT(2),
 };
 
+enum rtase_parse_result {
+       RTASE_PARSE_OK,
+       RTASE_PARSE_SKIP,
+       RTASE_PARSE_DROP,
+};
+
 #define RSVD_MASK 0x3FFFC000
 
 struct rtase_tx_desc {
@@ -363,4 +369,6 @@ struct rtase_private {
 
 #define RTASE_MSS_MASK GENMASK(28, 18)
 
+#define RTASE_MIN_PAD_LEN 47
+
 #endif /* RTASE_H */
index 255667775f0e8b9c1c34535728984fa7c7ad7c22..4168ad9e48eacacb4be1b0bd692c462d4fd7af78 100644 (file)
@@ -61,6 +61,7 @@
 #include <linux/pci.h>
 #include <linux/pm_runtime.h>
 #include <linux/prefetch.h>
+#include <linux/ptp_classify.h>
 #include <linux/rtnetlink.h>
 #include <linux/tcp.h>
 #include <asm/irq.h>
@@ -1252,6 +1253,199 @@ static u32 rtase_tx_csum(struct sk_buff *skb, const struct net_device *dev)
        return csum_cmd;
 }
 
+static enum rtase_parse_result rtase_get_l3_proto(struct sk_buff *skb,
+                                                 __be16 *proto,
+                                                 u32 *network_offset)
+{
+       struct vlan_hdr *vh, _vh;
+       struct ethhdr *eh, _eh;
+       u32 offset = ETH_HLEN;
+
+       eh = skb_header_pointer(skb, 0, sizeof(_eh), &_eh);
+       if (!eh)
+               return RTASE_PARSE_DROP;
+
+       *proto = eh->h_proto;
+
+       while (eth_type_vlan(*proto)) {
+               vh = skb_header_pointer(skb, offset, sizeof(_vh), &_vh);
+               if (!vh)
+                       return RTASE_PARSE_DROP;
+
+               *proto = vh->h_vlan_encapsulated_proto;
+               offset += VLAN_HLEN;
+       }
+
+       *network_offset = offset;
+
+       return RTASE_PARSE_OK;
+}
+
+static bool rtase_pad_to_transport_len(struct sk_buff *skb,
+                                      u32 transport_offset,
+                                      u32 pad_to_len)
+{
+       u32 trans_data_len;
+       u32 pad_len;
+
+       trans_data_len = skb->len - transport_offset;
+       if (trans_data_len >= pad_to_len)
+               return true;
+
+       if (skb_is_nonlinear(skb)) {
+               if (skb_linearize(skb))
+                       return false;
+       }
+
+       pad_len = pad_to_len - trans_data_len;
+       if (__skb_put_padto(skb, skb->len + pad_len, false))
+               return false;
+
+       return true;
+}
+
+static enum rtase_parse_result rtase_get_transport_offset(struct sk_buff *skb,
+                                                         u32 *transport_offset,
+                                                         u8 *transport_proto,
+                                                         u32 *pad_to_len)
+{
+       enum rtase_parse_result ret;
+       struct ipv6hdr *i6h, _i6h;
+       struct iphdr *ih, _ih;
+       bool non_first_frag;
+       __be16 proto;
+       u32 offset;
+       u32 no;
+
+       ret = rtase_get_l3_proto(skb, &proto, &no);
+       if (ret != RTASE_PARSE_OK)
+               return ret;
+
+       switch (proto) {
+       case htons(ETH_P_IP):
+               ih = skb_header_pointer(skb, no, sizeof(_ih), &_ih);
+               if (!ih)
+                       return RTASE_PARSE_DROP;
+
+               if (ih->ihl < 5)
+                       return RTASE_PARSE_DROP;
+
+               offset = no + ih->ihl * 4;
+               if (offset > skb->len)
+                       return RTASE_PARSE_DROP;
+
+               non_first_frag = ntohs(ih->frag_off) & IP_OFFSET;
+
+               if (ih->protocol == IPPROTO_TCP) {
+                       if (skb->len - offset < sizeof(struct tcphdr)) {
+                               if (non_first_frag) {
+                                       *transport_offset = offset;
+                                       *transport_proto = IPPROTO_TCP;
+                                       *pad_to_len = sizeof(struct tcphdr);
+
+                                       return RTASE_PARSE_OK;
+                               }
+
+                               return RTASE_PARSE_DROP;
+                       }
+
+                       return RTASE_PARSE_SKIP;
+               }
+
+               if (ih->protocol != IPPROTO_UDP)
+                       return RTASE_PARSE_SKIP;
+
+               *transport_offset = offset;
+               *transport_proto = IPPROTO_UDP;
+
+               if (skb->len - offset < sizeof(struct udphdr)) {
+                       if (non_first_frag) {
+                               *pad_to_len = sizeof(struct udphdr);
+
+                               return RTASE_PARSE_OK;
+                       }
+
+                       return RTASE_PARSE_DROP;
+               }
+
+               return RTASE_PARSE_OK;
+
+       case htons(ETH_P_IPV6):
+               i6h = skb_header_pointer(skb, no, sizeof(_i6h), &_i6h);
+               if (!i6h)
+                       return RTASE_PARSE_DROP;
+
+               offset = no + sizeof(*i6h);
+
+               if (i6h->nexthdr == IPPROTO_TCP) {
+                       if (skb->len - offset < sizeof(struct tcphdr))
+                               return RTASE_PARSE_DROP;
+
+                       return RTASE_PARSE_SKIP;
+               }
+
+               if (i6h->nexthdr != IPPROTO_UDP)
+                       return RTASE_PARSE_SKIP;
+
+               if (skb->len - offset < sizeof(struct udphdr))
+                       return RTASE_PARSE_DROP;
+
+               *transport_offset = offset;
+               *transport_proto = IPPROTO_UDP;
+
+               return RTASE_PARSE_OK;
+
+       default:
+               return RTASE_PARSE_SKIP;
+       }
+}
+
+static bool rtase_skb_pad(struct sk_buff *skb)
+{
+       enum rtase_parse_result ret;
+       u32 transport_offset;
+       __be16 *dest, _dest;
+       u32 trans_data_len;
+       u32 pad_to_len = 0;
+       u8 transport_proto;
+       u16 dest_port;
+
+       ret = rtase_get_transport_offset(skb, &transport_offset,
+                                        &transport_proto, &pad_to_len);
+       if (ret == RTASE_PARSE_SKIP) {
+               return true;
+       } else if (ret == RTASE_PARSE_DROP) {
+               netdev_dbg(skb->dev, "drop malformed packet\n");
+               return false;
+       }
+
+       if (pad_to_len &&
+           !rtase_pad_to_transport_len(skb, transport_offset, pad_to_len))
+               return false;
+
+       if (transport_proto != IPPROTO_UDP)
+               return true;
+
+       trans_data_len = skb->len - transport_offset;
+       if (trans_data_len < offsetof(struct udphdr, len) ||
+           trans_data_len >= RTASE_MIN_PAD_LEN)
+               return true;
+
+       dest = skb_header_pointer(skb,
+                                 transport_offset +
+                                 offsetof(struct udphdr, dest),
+                                 sizeof(_dest), &_dest);
+       if (!dest)
+               return true;
+
+       dest_port = ntohs(*dest);
+       if (dest_port != PTP_EV_PORT && dest_port != PTP_GEN_PORT)
+               return true;
+
+       return rtase_pad_to_transport_len(skb, transport_offset,
+                                         RTASE_MIN_PAD_LEN);
+}
+
 static int rtase_xmit_frags(struct rtase_ring *ring, struct sk_buff *skb,
                            u32 opts1, u32 opts2)
 {
@@ -1365,6 +1559,9 @@ static netdev_tx_t rtase_start_xmit(struct sk_buff *skb,
                opts2 |= rtase_tx_csum(skb, dev);
        }
 
+       if (!rtase_skb_pad(skb))
+               goto err_dma_0;
+
        frags = rtase_xmit_frags(ring, skb, opts1, opts2);
        if (unlikely(frags < 0))
                goto err_dma_0;