AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/08/07 11:22 flow patch-triage
0m Args:
null

Results:
{
  "EnableConfigs": null,
  "FocusSymbols": [],
  "KMSANReasoning": "",
  "NeedsKMSAN": false,
  "Reasoning": "The patch only modifies the Airoha SoC ethernet driver (drivers/net/ethernet/airoha/*), which is a platform-specific driver for Airoha hardware. This hardware is not emulated in standard virtualized environments like QEMU or GCE, making the modified code paths unreachable during fuzzing.",
  "WorthFuzzing": false
}

1/1 2026/08/07 11:22 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit 3c6242a987ae4fb8eb6f627bb5f2c4a8137566a7\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Fri Aug 7 11:22:34 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/drivers/net/ethernet/airoha/airoha_eth.c b/drivers/net/ethernet/airoha/airoha_eth.c\nindex 64619e9a704dd..e0d74d0cb36bb 100644\n--- a/drivers/net/ethernet/airoha/airoha_eth.c\n+++ b/drivers/net/ethernet/airoha/airoha_eth.c\n@@ -10,8 +10,10 @@\n #include \u003clinux/tcp.h\u003e\n #include \u003clinux/u64_stats_sync.h\u003e\n #include \u003cnet/dst_metadata.h\u003e\n+#include \u003cnet/ip6_checksum.h\u003e\n #include \u003cnet/page_pool/helpers.h\u003e\n #include \u003cnet/pkt_cls.h\u003e\n+#include \u003cnet/tcp.h\u003e\n #include \u003cuapi/linux/ppp_defs.h\u003e\n \n #include \"airoha_regs.h\"\n@@ -491,6 +493,88 @@ static void airoha_fe_crsn_qsel_init(struct airoha_eth *eth)\n \t\t\t\t CDM_CRSN_QSEL_Q1));\n }\n \n+static void airoha_fe_lro_rxq_enable(struct airoha_eth *eth, int qdma_id,\n+\t\t\t\t     int lro_queue_index, int qid,\n+\t\t\t\t     int buf_size)\n+{\n+\tint id = qdma_id + 1;\n+\n+\tairoha_fe_rmw(eth, REG_CDM_LRO_LIMIT(id),\n+\t\t      CDM_LRO_AGG_NUM_MASK | CDM_LRO_AGG_SIZE_MASK,\n+\t\t      FIELD_PREP(CDM_LRO_AGG_SIZE_MASK, buf_size) |\n+\t\t      FIELD_PREP(CDM_LRO_AGG_NUM_MASK,\n+\t\t\t\t AIROHA_RXQ_LRO_MAX_AGG_COUNT));\n+\tairoha_fe_rmw(eth, REG_CDM_LRO_AGE_TIME(id),\n+\t\t      CDM_LRO_AGE_TIME_MASK | CDM_LRO_AGG_TIME_MASK,\n+\t\t      FIELD_PREP(CDM_LRO_AGE_TIME_MASK,\n+\t\t\t\t AIROHA_RXQ_LRO_MAX_AGE_TIME) |\n+\t\t      FIELD_PREP(CDM_LRO_AGG_TIME_MASK,\n+\t\t\t\t AIROHA_RXQ_LRO_MAX_AGG_TIME));\n+\tairoha_fe_rmw(eth, REG_CDM_LRO_RXQ(id, lro_queue_index),\n+\t\t      LRO_RXQ_MASK(lro_queue_index),\n+\t\t      __field_prep(LRO_RXQ_MASK(lro_queue_index), qid));\n+\tairoha_fe_set(eth, REG_CDM_LRO_EN(id), BIT(lro_queue_index));\n+}\n+\n+static void airoha_fe_lro_disable(struct airoha_eth *eth, int qdma_id)\n+{\n+\tint i, id = qdma_id + 1;\n+\n+\tairoha_fe_clear(eth, REG_CDM_LRO_EN(id), LRO_RXQ_EN_MASK);\n+\tairoha_fe_clear(eth, REG_CDM_LRO_LIMIT(id),\n+\t\t\tCDM_LRO_AGG_NUM_MASK | CDM_LRO_AGG_SIZE_MASK);\n+\tairoha_fe_clear(eth, REG_CDM_LRO_AGE_TIME(id),\n+\t\t\tCDM_LRO_AGE_TIME_MASK | CDM_LRO_AGG_TIME_MASK);\n+\tfor (i = 0; i \u003c AIROHA_MAX_NUM_LRO_QUEUES; i++)\n+\t\tairoha_fe_clear(eth, REG_CDM_LRO_RXQ(id, i), LRO_RXQ_MASK(i));\n+}\n+\n+static bool airoha_fe_lro_is_enabled(struct airoha_eth *eth, int qdma_id)\n+{\n+\treturn airoha_fe_get(eth, REG_CDM_LRO_EN(qdma_id + 1),\n+\t\t\t     LRO_RXQ_EN_MASK);\n+}\n+\n+static void airoha_dev_lro_enable(struct airoha_gdm_dev *dev)\n+{\n+\tstruct airoha_qdma *qdma = airoha_qdma_deref(dev);\n+\tstruct airoha_eth *eth = qdma-\u003eeth;\n+\tint qdma_id = qdma - \u0026eth-\u003eqdma[0];\n+\tint i, lro_queue_index = 0;\n+\n+\tif (airoha_fe_lro_is_enabled(eth, qdma_id))\n+\t\treturn;\n+\n+\tfor (i = 0; i \u003c ARRAY_SIZE(qdma-\u003eq_rx); i++) {\n+\t\tstruct airoha_queue *q = \u0026qdma-\u003eq_rx[i];\n+\t\tu32 size;\n+\n+\t\tif (!q-\u003endesc)\n+\t\t\tcontinue;\n+\n+\t\tif (!airoha_qdma_is_lro_queue(q))\n+\t\t\tcontinue;\n+\n+\t\tsize = SKB_WITH_OVERHEAD(AIROHA_RX_LEN(q-\u003ebuf_size));\n+\t\tsize = min_t(u32, size, AIROHA_MAX_RX_SIZE);\n+\t\tairoha_fe_lro_rxq_enable(eth, qdma_id, lro_queue_index, i,\n+\t\t\t\t\t size);\n+\t\tlro_queue_index++;\n+\t}\n+}\n+\n+static void airoha_dev_lro_disable(struct airoha_gdm_dev *dev)\n+{\n+\tstruct airoha_qdma *qdma = airoha_qdma_deref(dev);\n+\tstruct airoha_eth *eth = qdma-\u003eeth;\n+\tint qdma_id = qdma - \u0026eth-\u003eqdma[0];\n+\n+\tif (!airoha_fe_lro_is_enabled(eth, qdma_id))\n+\t\treturn;\n+\n+\tairoha_fe_lro_disable(eth, qdma_id);\n+}\n+\n static int airoha_fe_init(struct airoha_eth *eth)\n {\n \tairoha_fe_maccr_init(eth);\n@@ -616,6 +700,7 @@ static int airoha_qdma_fill_rx_queue(struct airoha_queue *q)\n \t\te-\u003edma_addr = page_pool_get_dma_addr(page) + offset;\n \t\te-\u003edma_len = SKB_WITH_OVERHEAD(AIROHA_RX_LEN(q-\u003ebuf_size));\n \n+\t\tWRITE_ONCE(desc-\u003etcp_ts_reply, 0);\n \t\tval = FIELD_PREP(QDMA_DESC_LEN_MASK, e-\u003edma_len);\n \t\tWRITE_ONCE(desc-\u003ectrl, cpu_to_le32(val));\n \t\tWRITE_ONCE(desc-\u003eaddr, cpu_to_le32(e-\u003edma_addr));\n@@ -657,18 +742,188 @@ airoha_qdma_get_gdm_dev(struct airoha_eth *eth, struct airoha_qdma_desc *desc)\n \treturn port-\u003edevs[d] ? port-\u003edevs[d] : ERR_PTR(-ENODEV);\n }\n \n+static struct sk_buff *airoha_qdma_lro_rx_skb(struct airoha_queue *q,\n+\t\t\t\t\t      struct airoha_qdma_desc *desc,\n+\t\t\t\t\t      struct airoha_queue_entry *e,\n+\t\t\t\t\t      u32 len, u32 agg_count)\n+{\n+\tu32 msg1 = le32_to_cpu(READ_ONCE(desc-\u003emsg1));\n+\tu32 msg2 = le32_to_cpu(READ_ONCE(desc-\u003emsg2));\n+\tu32 msg3 = le32_to_cpu(READ_ONCE(desc-\u003emsg3));\n+\tu32 th_off, tcp_ack_seq, data_off, data_len;\n+\tstruct skb_shared_info *shinfo;\n+\tu16 tcp_win, l2_len;\n+\tstruct sk_buff *skb;\n+\tstruct tcphdr *th;\n+\tstruct page *page;\n+\tbool ipv4, ipv6;\n+\n+\tipv4 = FIELD_GET(QDMA_ETH_RXMSG_IP4_MASK, msg1);\n+\tipv6 = FIELD_GET(QDMA_ETH_RXMSG_IP6_MASK, msg1);\n+\tif (!ipv4 \u0026\u0026 !ipv6)\n+\t\treturn NULL;\n+\n+\tl2_len = FIELD_GET(QDMA_ETH_RXMSG_L2_LEN_MASK, msg2);\n+\n+\tif (ipv4) {\n+\t\tstruct iphdr *iph;\n+\n+\t\tif (len \u003c l2_len + sizeof(*iph))\n+\t\t\treturn NULL;\n+\n+\t\tiph = (struct iphdr *)(e-\u003ebuf + l2_len);\n+\t\tif (iph-\u003eprotocol != IPPROTO_TCP)\n+\t\t\treturn NULL;\n+\n+\t\tif (iph-\u003eihl \u003c 5)\n+\t\t\treturn NULL;\n+\n+\t\tth_off = l2_len + (iph-\u003eihl \u003c\u003c 2);\n+\t\tif (len \u003c th_off)\n+\t\t\treturn NULL;\n+\n+\t\tiph-\u003etot_len = cpu_to_be16(len - l2_len);\n+\t\tiph-\u003echeck = 0;\n+\t\tiph-\u003echeck = ip_fast_csum((void *)iph, iph-\u003eihl);\n+\t} else {\n+\t\tstruct ipv6hdr *ip6h;\n+\n+\t\tth_off = l2_len + sizeof(*ip6h);\n+\t\tif (len \u003c th_off)\n+\t\t\treturn NULL;\n+\n+\t\tip6h = (struct ipv6hdr *)(e-\u003ebuf + l2_len);\n+\t\tif (ip6h-\u003enexthdr != NEXTHDR_TCP)\n+\t\t\treturn NULL;\n+\n+\t\tip6h-\u003epayload_len = cpu_to_be16(len - th_off);\n+\t}\n+\n+\tif (len \u003c th_off + sizeof(*th))\n+\t\treturn NULL;\n+\n+\tth = (struct tcphdr *)(e-\u003ebuf + th_off);\n+\tif (th-\u003edoff \u003c 5)\n+\t\treturn NULL;\n+\n+\tdata_off = th_off + (th-\u003edoff \u003c\u003c 2);\n+\tif (len \u003c= data_off)\n+\t\treturn NULL;\n+\n+\ttcp_win = FIELD_GET(QDMA_ETH_RXMSG_TCP_WIN_MASK, msg3);\n+\ttcp_ack_seq = le32_to_cpu(READ_ONCE(desc-\u003edata));\n+\tth-\u003eack_seq = cpu_to_be32(tcp_ack_seq);\n+\tth-\u003ewindow = cpu_to_be16(tcp_win);\n+\n+\t/* Check tcp timestamp option */\n+\tif (th-\u003edoff == (sizeof(*th) + TCPOLEN_TSTAMP_ALIGNED) / 4) {\n+\t\tu32 topt = get_unaligned_be32(th + 1);\n+\n+\t\tif (topt == ((TCPOPT_NOP \u003c\u003c 24) | (TCPOPT_NOP \u003c\u003c 16) |\n+\t\t\t     (TCPOPT_TIMESTAMP \u003c\u003c 8) | TCPOLEN_TIMESTAMP)) {\n+\t\t\tu8 *ptr = (u8 *)th + sizeof(*th) + 2 * sizeof(__be32);\n+\t\t\t__le32 tcp_ts_reply = READ_ONCE(desc-\u003etcp_ts_reply);\n+\n+\t\t\t/* The field is pre-zeroed in the posted descriptor,\n+\t\t\t * so a non-zero value is the hardware confirming the\n+\t\t\t * timestamp echo reply is valid. Leave TSecr\n+\t\t\t * untouched otherwise, since the hardware may not\n+\t\t\t * populate it for every aggregate.\n+\t\t\t */\n+\t\t\tif (tcp_ts_reply)\n+\t\t\t\tput_unaligned_be32(le32_to_cpu(tcp_ts_reply),\n+\t\t\t\t\t\t   ptr);\n+\t\t}\n+\t}\n+\n+\tif (ipv4) {\n+\t\tstruct iphdr *iph = (struct iphdr *)(e-\u003ebuf + l2_len);\n+\n+\t\tth-\u003echeck = ~tcp_v4_check(len - th_off, iph-\u003esaddr,\n+\t\t\t\t\t  iph-\u003edaddr, 0);\n+\t} else {\n+\t\tstruct ipv6hdr *ip6h = (struct ipv6hdr *)(e-\u003ebuf + l2_len);\n+\n+\t\tth-\u003echeck = ~tcp_v6_check(len - th_off, \u0026ip6h-\u003esaddr,\n+\t\t\t\t\t  \u0026ip6h-\u003edaddr, 0);\n+\t}\n+\n+\t/* Split network headers and payload to rely on GRO.\n+\t * We need to do it in the driver since the NIC does\n+\t * not support it.\n+\t */\n+\tskb = napi_alloc_skb(\u0026q-\u003enapi, data_off);\n+\tif (!skb)\n+\t\treturn NULL;\n+\n+\t__skb_put(skb, data_off);\n+\tmemcpy(skb-\u003edata, e-\u003ebuf, data_off);\n+\n+\tpage = virt_to_head_page(e-\u003ebuf);\n+\tdata_len = len - data_off;\n+\tshinfo = skb_shinfo(skb);\n+\tskb_add_rx_frag(skb, shinfo-\u003enr_frags, page,\n+\t\t\te-\u003ebuf + data_off - page_address(page), data_len,\n+\t\t\tq-\u003ebuf_size);\n+\n+\tshinfo-\u003egso_type = ipv4 ? SKB_GSO_TCPV4 : SKB_GSO_TCPV6;\n+\t/* HW does not report the per-segment MSS, so approximate it as\n+\t * the average segment size. For a stream of MSS-sized segments\n+\t * with a shorter tail this still re-segments into exactly\n+\t * agg_count segments, keeping gso_size/gso_segs and the packet\n+\t * accounting coherent, while never overestimating the MSS.\n+\t */\n+\tshinfo-\u003egso_size = DIV_ROUND_UP(data_len, agg_count);\n+\tshinfo-\u003egso_segs = agg_count;\n+\n+\tskb-\u003ecsum_start = skb_headroom(skb) + th_off;\n+\tskb-\u003ecsum_offset = offsetof(struct tcphdr, check);\n+\tskb-\u003eip_summed = CHECKSUM_PARTIAL;\n+\n+\treturn skb;\n+}\n+\n+static struct sk_buff *airoha_qdma_build_rx_skb(struct airoha_queue *q,\n+\t\t\t\t\t\tstruct airoha_qdma_desc *desc,\n+\t\t\t\t\t\tstruct airoha_queue_entry *e,\n+\t\t\t\t\t\tstruct net_device *dev,\n+\t\t\t\t\t\tu32 len, u32 agg_count)\n+{\n+\tint qid = q - \u0026q-\u003eqdma-\u003eq_rx[0];\n+\tstruct sk_buff *skb;\n+\n+\tif (agg_count \u003e 1 \u0026\u0026 airoha_qdma_is_lro_queue(q)) { /* LRO */\n+\t\tskb = airoha_qdma_lro_rx_skb(q, desc, e, len, agg_count);\n+\t\tif (!skb)\n+\t\t\treturn NULL;\n+\t} else {\n+\t\tskb = napi_build_skb(e-\u003ebuf - AIROHA_RX_HEADROOM, q-\u003ebuf_size);\n+\t\tif (!skb)\n+\t\t\treturn NULL;\n+\n+\t\tskb_reserve(skb, AIROHA_RX_HEADROOM);\n+\t\t__skb_put(skb, len);\n+\t\tskb-\u003eip_summed = CHECKSUM_UNNECESSARY;\n+\t}\n+\n+\tskb_mark_for_recycle(skb);\n+\tskb-\u003edev = dev;\n+\tskb_record_rx_queue(skb, qid);\n+\tskb-\u003eprotocol = eth_type_trans(skb, dev);\n+\n+\treturn skb;\n+}\n+\n static int airoha_qdma_rx_process(struct airoha_queue *q, int budget)\n {\n \tenum dma_data_direction dir = page_pool_get_dma_dir(q-\u003epage_pool);\n-\tstruct airoha_qdma *qdma = q-\u003eqdma;\n-\tstruct airoha_eth *eth = qdma-\u003eeth;\n-\tint qid = q - \u0026qdma-\u003eq_rx[0];\n+\tstruct airoha_eth *eth = q-\u003eqdma-\u003eeth;\n \tint done = 0;\n \n \twhile (done \u003c budget) {\n+\t\tu32 hash, reason, msg1, msg2, desc_ctrl, agg_count;\n \t\tstruct airoha_queue_entry *e = \u0026q-\u003eentry[q-\u003etail];\n \t\tstruct airoha_qdma_desc *desc = \u0026q-\u003edesc[q-\u003etail];\n-\t\tu32 hash, reason, msg1, desc_ctrl;\n \t\tstruct airoha_gdm_dev *dev;\n \t\tstruct net_device *netdev;\n \t\tint data_len, len;\n@@ -692,24 +947,21 @@ static int airoha_qdma_rx_process(struct airoha_queue *q, int budget)\n \t\tif (!len || data_len \u003c len)\n \t\t\tgoto free_frag;\n \n+\t\tmsg2 = le32_to_cpu(READ_ONCE(desc-\u003emsg2));\n+\t\tagg_count = FIELD_GET(QDMA_ETH_RXMSG_AGG_COUNT_MASK, msg2);\n+\t\tif (agg_count \u003e AIROHA_RXQ_LRO_MAX_AGG_COUNT)\n+\t\t\tgoto free_frag;\n+\n \t\tdev = airoha_qdma_get_gdm_dev(eth, desc);\n \t\tif (IS_ERR(dev))\n \t\t\tgoto free_frag;\n \n \t\tnetdev = netdev_from_priv(dev);\n \t\tif (!q-\u003eskb) { /* first buffer */\n-\t\t\tq-\u003eskb = napi_build_skb(e-\u003ebuf - AIROHA_RX_HEADROOM,\n-\t\t\t\t\t\tq-\u003ebuf_size);\n+\t\t\tq-\u003eskb = airoha_qdma_build_rx_skb(q, desc, e, netdev,\n+\t\t\t\t\t\t\t  len, agg_count);\n \t\t\tif (!q-\u003eskb)\n \t\t\t\tgoto free_frag;\n-\n-\t\t\tskb_reserve(q-\u003eskb, AIROHA_RX_HEADROOM);\n-\t\t\t__skb_put(q-\u003eskb, len);\n-\t\t\tskb_mark_for_recycle(q-\u003eskb);\n-\t\t\tq-\u003eskb-\u003edev = netdev;\n-\t\t\tq-\u003eskb-\u003eprotocol = eth_type_trans(q-\u003eskb, netdev);\n-\t\t\tq-\u003eskb-\u003eip_summed = CHECKSUM_UNNECESSARY;\n-\t\t\tskb_record_rx_queue(q-\u003eskb, qid);\n \t\t} else { /* scattered frame */\n \t\t\tstruct skb_shared_info *shinfo = skb_shinfo(q-\u003eskb);\n \t\t\tint nr_frags = shinfo-\u003enr_frags;\n@@ -800,12 +1052,10 @@ static int airoha_qdma_rx_napi_poll(struct napi_struct *napi, int budget)\n static int airoha_qdma_init_rx_queue(struct airoha_queue *q,\n \t\t\t\t     struct airoha_qdma *qdma, int ndesc)\n {\n-\tconst struct page_pool_params pp_params = {\n-\t\t.order = 0,\n+\tstruct page_pool_params pp_params = {\n \t\t.pool_size = 256,\n \t\t.flags = PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV,\n \t\t.dma_dir = DMA_FROM_DEVICE,\n-\t\t.max_len = PAGE_SIZE,\n \t\t.nid = NUMA_NO_NODE,\n \t\t.dev = qdma-\u003eeth-\u003edev,\n \t\t.napi = \u0026q-\u003enapi,\n@@ -813,9 +1063,10 @@ static int airoha_qdma_init_rx_queue(struct airoha_queue *q,\n \tstruct airoha_eth *eth = qdma-\u003eeth;\n \tint qid = q - \u0026qdma-\u003eq_rx[0], thr;\n \tdma_addr_t dma_addr;\n+\tbool lro_q;\n \n-\tq-\u003ebuf_size = PAGE_SIZE / 2;\n \tq-\u003eqdma = qdma;\n+\tlro_q = airoha_qdma_is_lro_queue(q);\n \n \tq-\u003eentry = devm_kzalloc(eth-\u003edev, ndesc * sizeof(*q-\u003eentry),\n \t\t\t\tGFP_KERNEL);\n@@ -827,6 +1078,9 @@ static int airoha_qdma_init_rx_queue(struct airoha_queue *q,\n \tif (!q-\u003edesc)\n \t\treturn -ENOMEM;\n \n+\tpp_params.order = lro_q ? AIROHA_LRO_PAGE_ORDER : 0;\n+\tpp_params.max_len = PAGE_SIZE \u003c\u003c pp_params.order;\n+\n \tq-\u003epage_pool = page_pool_create(\u0026pp_params);\n \tif (IS_ERR(q-\u003epage_pool)) {\n \t\tint err = PTR_ERR(q-\u003epage_pool);\n@@ -835,6 +1089,7 @@ static int airoha_qdma_init_rx_queue(struct airoha_queue *q,\n \t\treturn err;\n \t}\n \n+\tq-\u003ebuf_size = lro_q ? pp_params.max_len : pp_params.max_len / 2;\n \tq-\u003endesc = ndesc;\n \tnetif_napi_add(eth-\u003enapi_dev, \u0026q-\u003enapi, airoha_qdma_rx_napi_poll);\n \n@@ -848,7 +1103,12 @@ static int airoha_qdma_init_rx_queue(struct airoha_queue *q,\n \t\t\tFIELD_PREP(RX_RING_THR_MASK, thr));\n \tairoha_qdma_rmw(qdma, REG_RX_DMA_IDX(qid), RX_RING_DMA_IDX_MASK,\n \t\t\tFIELD_PREP(RX_RING_DMA_IDX_MASK, q-\u003ehead));\n-\tairoha_qdma_set(qdma, REG_RX_SCATTER_CFG(qid), RX_RING_SG_EN_MASK);\n+\tif (lro_q)\n+\t\tairoha_qdma_clear(qdma, REG_RX_SCATTER_CFG(qid),\n+\t\t\t\t  RX_RING_SG_EN_MASK);\n+\telse\n+\t\tairoha_qdma_set(qdma, REG_RX_SCATTER_CFG(qid),\n+\t\t\t\tRX_RING_SG_EN_MASK);\n \n \tairoha_qdma_fill_rx_queue(q);\n \n@@ -870,6 +1130,7 @@ static void airoha_qdma_cleanup_rx_queue(struct airoha_queue *q)\n \t\t\t\t\tpage_pool_get_dma_dir(q-\u003epage_pool));\n \t\tpage_pool_put_full_page(q-\u003epage_pool, page, false);\n \t\t/* Reset DMA descriptor */\n+\t\tWRITE_ONCE(desc-\u003etcp_ts_reply, 0);\n \t\tWRITE_ONCE(desc-\u003ectrl, 0);\n \t\tWRITE_ONCE(desc-\u003eaddr, 0);\n \t\tWRITE_ONCE(desc-\u003edata, 0);\n@@ -1896,6 +2157,34 @@ static void airoha_update_hw_stats(struct airoha_gdm_dev *dev)\n \tspin_unlock(\u0026port-\u003estats_lock);\n }\n \n+static void airoha_update_netdev_features(struct airoha_gdm_dev *dev)\n+{\n+\tstruct airoha_eth *eth = dev-\u003eeth;\n+\tint i;\n+\n+\tfor (i = 0; i \u003c ARRAY_SIZE(eth-\u003eports); i++) {\n+\t\tstruct airoha_gdm_port *port = eth-\u003eports[i];\n+\t\tint j;\n+\n+\t\tif (!port)\n+\t\t\tcontinue;\n+\n+\t\tfor (j = 0; j \u003c ARRAY_SIZE(port-\u003edevs); j++) {\n+\t\t\tstruct airoha_gdm_dev *iter_dev = port-\u003edevs[j];\n+\t\t\tstruct net_device *netdev;\n+\n+\t\t\tif (!iter_dev)\n+\t\t\t\tcontinue;\n+\n+\t\t\tnetdev = netdev_from_priv(iter_dev);\n+\t\t\tif (netdev-\u003ereg_state != NETREG_REGISTERED)\n+\t\t\t\tcontinue;\n+\n+\t\t\tnetdev_update_features(netdev);\n+\t\t}\n+\t}\n+}\n+\n static void airoha_dev_set_xmit_frame_size(struct net_device *netdev)\n {\n \tstruct airoha_gdm_dev *dev = netdev_priv(netdev);\n@@ -1913,14 +2202,28 @@ static int airoha_dev_open(struct net_device *netdev)\n \tstruct airoha_gdm_port *port = dev-\u003eport;\n \tu32 pse_port = FE_PSE_PORT_PPE1;\n \tstruct airoha_qdma *qdma;\n-\tint err;\n+\tint qdma_id, err;\n+\n+\t/* HW GRO is configured on the QDMA and it is shared between\n+\t * all the devices using it. Refuse to open a second device on\n+\t * the same QDMA if HW GRO is enabled on any device sharing it\n+\t * or if this device has HW GRO enabled in its feature set.\n+\t */\n+\tqdma = airoha_qdma_deref(dev);\n+\tqdma_id = qdma - \u0026qdma-\u003eeth-\u003eqdma[0];\n+\n+\tif (qdma-\u003eusers \u003e 1 \u0026\u0026 (airoha_fe_lro_is_enabled(qdma-\u003eeth, qdma_id) ||\n+\t\t\t\t(netdev-\u003efeatures \u0026 NETIF_F_GRO_HW))) {\n+\t\tnetdev_warn(netdev, \"required to disable HW GRO on QDMA%d\\n\",\n+\t\t\t    qdma_id);\n+\t\treturn -EBUSY;\n+\t}\n \n \tnetif_tx_start_all_queues(netdev);\n \terr = airoha_set_vip_for_gdm_port(dev, true);\n \tif (err)\n \t\treturn err;\n \n-\tqdma = airoha_qdma_deref(dev);\n \tif (netdev_uses_dsa(netdev))\n \t\tairoha_fe_set(qdma-\u003eeth, REG_GDM_INGRESS_CFG(port-\u003eid),\n \t\t\t      GDM_STAG_EN_MASK);\n@@ -1937,6 +2240,9 @@ static int airoha_dev_open(struct net_device *netdev)\n \tairoha_set_gdm_port_fwd_cfg(qdma-\u003eeth, REG_GDM_FWD_CFG(port-\u003eid),\n \t\t\t\t    pse_port);\n \n+\tif (netdev-\u003efeatures \u0026 NETIF_F_GRO_HW)\n+\t\tairoha_dev_lro_enable(dev);\n+\n \treturn 0;\n }\n \n@@ -1954,6 +2260,10 @@ static int airoha_dev_stop(struct net_device *netdev)\n \t\tairoha_set_gdm_port_fwd_cfg(dev-\u003eeth,\n \t\t\t\t\t    REG_GDM_FWD_CFG(port-\u003eid),\n \t\t\t\t\t    FE_PSE_PORT_DROP);\n+\n+\tif (netdev-\u003efeatures \u0026 NETIF_F_GRO_HW)\n+\t\tairoha_dev_lro_disable(dev);\n+\n \treturn 0;\n }\n \n@@ -2104,6 +2414,34 @@ airoha_get_wan_gdm_dev(struct airoha_eth *eth)\n \treturn NULL;\n }\n \n+static int airoha_dev_check_hw_gro(struct airoha_gdm_dev *dev)\n+{\n+\tstruct airoha_qdma *qdma = airoha_qdma_deref(dev);\n+\tstruct airoha_eth *eth = qdma-\u003eeth;\n+\tint qdma_id = qdma - \u0026eth-\u003eqdma[0];\n+\n+\tif (airoha_fe_lro_is_enabled(eth, qdma_id)) {\n+\t\tstruct net_device *netdev = netdev_from_priv(dev);\n+\n+\t\tnetdev_warn(netdev, \"HW GRO is enabled on QDMA%d\\n\", qdma_id);\n+\t\treturn -EBUSY;\n+\t}\n+\n+\tqdma = \u0026eth-\u003eqdma[!qdma_id];\n+\tqdma_id = qdma - \u0026eth-\u003eqdma[0];\n+\n+\tif (airoha_fe_lro_is_enabled(eth, qdma_id)) {\n+\t\tstruct net_device *netdev = netdev_from_priv(dev);\n+\n+\t\tnetdev_warn(netdev,\n+\t\t\t    \"HW GRO is enabled on destination QDMA%d\\n\",\n+\t\t\t    qdma_id);\n+\t\treturn -EBUSY;\n+\t}\n+\n+\treturn 0;\n+}\n+\n static void airoha_dev_set_qdma(struct airoha_gdm_dev *dev)\n {\n \tstruct net_device *netdev = netdev_from_priv(dev);\n@@ -2120,6 +2458,7 @@ static void airoha_dev_set_qdma(struct airoha_gdm_dev *dev)\n \n \trcu_assign_pointer(dev-\u003eqdma, qdma);\n \tnetdev-\u003eirq = qdma-\u003eirq_banks[0].irq;\n+\tqdma-\u003eusers++;\n \tsynchronize_rcu();\n \n \tppe_id = !airoha_is_lan_gdm_dev(dev) \u0026\u0026 airoha_ppe_is_enabled(eth, 1);\n@@ -2136,8 +2475,12 @@ static void airoha_dev_set_qdma(struct airoha_gdm_dev *dev)\n \t\t\tairoha_qdma_rr(qdma, REG_CNTR_VAL((i \u003c\u003c 1) + 1));\n \t}\n \n-\tif (cur_qdma)\n+\tif (cur_qdma) {\n+\t\tcur_qdma-\u003eusers--;\n \t\tnetif_tx_wake_all_queues(netdev);\n+\t}\n+\n+\tairoha_update_netdev_features(dev);\n }\n \n static int airoha_dev_init(struct net_device *netdev)\n@@ -2288,6 +2631,36 @@ int airoha_get_fe_port(struct airoha_gdm_dev *dev)\n \t}\n }\n \n+static netdev_features_t airoha_dev_fix_features(struct net_device *netdev,\n+\t\t\t\t\t\t netdev_features_t features)\n+{\n+\tstruct airoha_gdm_dev *dev = netdev_priv(netdev);\n+\tstruct airoha_qdma *qdma;\n+\n+\tqdma = airoha_qdma_deref(dev);\n+\tif (qdma-\u003eusers \u003e 1)\n+\t\tfeatures \u0026= ~NETIF_F_GRO_HW;\n+\n+\treturn features;\n+}\n+\n+static int airoha_dev_set_features(struct net_device *netdev,\n+\t\t\t\t   netdev_features_t features)\n+{\n+\tnetdev_features_t diff = netdev-\u003efeatures ^ features;\n+\tstruct airoha_gdm_dev *dev = netdev_priv(netdev);\n+\n+\tif (!(diff \u0026 NETIF_F_GRO_HW))\n+\t\treturn 0;\n+\n+\tif (features \u0026 NETIF_F_GRO_HW)\n+\t\tairoha_dev_lro_enable(dev);\n+\telse\n+\t\tairoha_dev_lro_disable(dev);\n+\n+\treturn 0;\n+}\n+\n static netdev_tx_t airoha_dev_xmit(struct sk_buff *skb,\n \t\t\t\t   struct net_device *netdev)\n {\n@@ -3199,7 +3572,7 @@ static int airoha_enable_qos_for_gdm34(struct net_device *netdev,\n \tstruct airoha_gdm_dev *wan_dev, *dev = netdev_priv(netdev);\n \tstruct airoha_gdm_port *port = dev-\u003eport;\n \tstruct airoha_eth *eth = dev-\u003eeth;\n-\tint err = -EBUSY;\n+\tint err;\n \n \tif (port-\u003eid != AIROHA_GDM3_IDX \u0026\u0026\n \t    port-\u003eid != AIROHA_GDM4_IDX) {\n@@ -3210,6 +3583,10 @@ static int airoha_enable_qos_for_gdm34(struct net_device *netdev,\n \tif (!airoha_is_lan_gdm_dev(dev)) /* Already enabled */\n \t\treturn 0;\n \n+\terr = airoha_dev_check_hw_gro(dev);\n+\tif (err)\n+\t\treturn err;\n+\n \tmutex_lock(\u0026flow_offload_mutex);\n \n \twan_dev = airoha_get_wan_gdm_dev(eth);\n@@ -3218,6 +3595,7 @@ static int airoha_enable_qos_for_gdm34(struct net_device *netdev,\n \t\t    wan_dev-\u003eport-\u003eid == AIROHA_GDM2_IDX) {\n \t\t\tNL_SET_ERR_MSG_MOD(extack,\n \t\t\t\t\t   \"QoS configured for WAN device\");\n+\t\t\terr = -EBUSY;\n \t\t\tgoto error_unlock;\n \t\t}\n \t\tairoha_disable_qos_for_gdm34(netdev_from_priv(wan_dev));\n@@ -3343,6 +3721,8 @@ static const struct net_device_ops airoha_netdev_ops = {\n \t.ndo_stop\t\t= airoha_dev_stop,\n \t.ndo_change_mtu\t\t= airoha_dev_change_mtu,\n \t.ndo_select_queue\t= airoha_dev_select_queue,\n+\t.ndo_fix_features\t= airoha_dev_fix_features,\n+\t.ndo_set_features\t= airoha_dev_set_features,\n \t.ndo_start_xmit\t\t= airoha_dev_xmit,\n \t.ndo_get_stats64        = airoha_dev_get_stats64,\n \t.ndo_set_mac_address\t= airoha_dev_set_macaddr,\n@@ -3430,11 +3810,9 @@ static int airoha_alloc_gdm_device(struct airoha_eth *eth,\n \tnetdev-\u003eethtool_ops = \u0026airoha_ethtool_ops;\n \tnetdev-\u003emax_mtu = AIROHA_MAX_MTU;\n \tnetdev-\u003ewatchdog_timeo = 5 * HZ;\n-\tnetdev-\u003ehw_features = NETIF_F_IP_CSUM | NETIF_F_RXCSUM | NETIF_F_TSO6 |\n-\t\t\t      NETIF_F_IPV6_CSUM | NETIF_F_SG | NETIF_F_TSO |\n-\t\t\t      NETIF_F_HW_TC;\n-\tnetdev-\u003efeatures |= netdev-\u003ehw_features;\n-\tnetdev-\u003evlan_features = netdev-\u003ehw_features;\n+\tnetdev-\u003ehw_features = AIROHA_HW_FEATURES | NETIF_F_GRO_HW;\n+\tnetdev-\u003efeatures |= AIROHA_HW_FEATURES;\n+\tnetdev-\u003evlan_features = AIROHA_HW_FEATURES;\n \tSET_NETDEV_DEV(netdev, eth-\u003edev);\n \n \t/* reserve hw queues for HTB offloading */\ndiff --git a/drivers/net/ethernet/airoha/airoha_eth.h b/drivers/net/ethernet/airoha/airoha_eth.h\nindex fa9a8edce22ff..9b55abc630484 100644\n--- a/drivers/net/ethernet/airoha/airoha_eth.h\n+++ b/drivers/net/ethernet/airoha/airoha_eth.h\n@@ -45,6 +45,18 @@\n \t (_n) == 15 ? 128 :\t\t\\\n \t (_n) ==  0 ? 1024 : 16)\n \n+#define AIROHA_LRO_PAGE_ORDER\t\tget_order(SZ_16K)\n+#define AIROHA_MAX_NUM_LRO_QUEUES\t8\n+#define AIROHA_RXQ_LRO_EN_MASK\t\tGENMASK(31, 24)\n+#define AIROHA_RXQ_LRO_MAX_AGG_COUNT\t64\n+#define AIROHA_RXQ_LRO_MAX_AGG_TIME\t100\n+#define AIROHA_RXQ_LRO_MAX_AGE_TIME\t2000\n+\n+#define AIROHA_HW_FEATURES\t\t\t\\\n+\t(NETIF_F_IP_CSUM | NETIF_F_RXCSUM |\t\\\n+\t NETIF_F_TSO6 | NETIF_F_IPV6_CSUM |\t\\\n+\t NETIF_F_SG | NETIF_F_TSO | NETIF_F_HW_TC)\n+\n #define PSE_RSV_PAGES\t\t\t128\n #define PSE_QUEUE_RSV_PAGES\t\t64\n \n@@ -538,7 +550,7 @@ struct airoha_wdma_info {\n \n /* RX queue to IRQ mapping: BIT(q) in IRQ(n) */\n #define RX_IRQ0_BANK_PIN_MASK\t\t\t0x839f\n-#define RX_IRQ1_BANK_PIN_MASK\t\t\t0x7fe00000\n+#define RX_IRQ1_BANK_PIN_MASK\t\t\t0xffe00000\n #define RX_IRQ2_BANK_PIN_MASK\t\t\t0x20\n #define RX_IRQ3_BANK_PIN_MASK\t\t\t0x40\n #define RX_IRQ_BANK_PIN_MASK(_n)\t\t\\\n@@ -560,6 +572,8 @@ struct airoha_qdma {\n \tstruct airoha_eth *eth;\n \tvoid __iomem *regs;\n \n+\tint users;\n+\n \tstruct airoha_irq_bank irq_banks[AIROHA_MAX_NUM_IRQ_BANKS];\n \n \tstruct airoha_tx_irq_queue q_tx_irq[AIROHA_NUM_TX_IRQ];\n@@ -714,6 +728,18 @@ static inline bool airoha_is_7583(struct airoha_eth *eth)\n \treturn eth-\u003esoc-\u003eversion == 0x7583;\n }\n \n+static inline bool airoha_qdma_is_lro_queue(struct airoha_queue *q)\n+{\n+\tstruct airoha_qdma *qdma = q-\u003eqdma;\n+\tint qid = q - \u0026qdma-\u003eq_rx[0];\n+\n+\t/* EN7581 SoC supports at most 8 LRO rx queues */\n+\tBUILD_BUG_ON(hweight32(AIROHA_RXQ_LRO_EN_MASK) \u003e\n+\t\t     AIROHA_MAX_NUM_LRO_QUEUES);\n+\n+\treturn !!(AIROHA_RXQ_LRO_EN_MASK \u0026 BIT(qid));\n+}\n+\n int airoha_get_fe_port(struct airoha_gdm_dev *dev);\n bool airoha_is_valid_gdm_dev(struct airoha_eth *eth,\n \t\t\t     struct airoha_gdm_dev *dev);\ndiff --git a/drivers/net/ethernet/airoha/airoha_regs.h b/drivers/net/ethernet/airoha/airoha_regs.h\nindex 442b48c9b991e..0ec41c3b4c066 100644\n--- a/drivers/net/ethernet/airoha/airoha_regs.h\n+++ b/drivers/net/ethernet/airoha/airoha_regs.h\n@@ -122,6 +122,20 @@\n #define CDM_CRSN_QSEL_REASON_MASK(_n)\t\\\n \tGENMASK(4 + (((_n) % 4) \u003c\u003c 3),\t(((_n) % 4) \u003c\u003c 3))\n \n+#define REG_CDM_LRO_RXQ(_n, _m)\t\t(CDM_BASE(_n) + 0x78 + ((_m) \u0026 0x4))\n+#define LRO_RXQ_MASK(_n)\t\tGENMASK(4 + (((_n) \u0026 0x3) \u003c\u003c 3), ((_n) \u0026 0x3) \u003c\u003c 3)\n+\n+#define REG_CDM_LRO_EN(_n)\t\t(CDM_BASE(_n) + 0x80)\n+#define LRO_RXQ_EN_MASK\t\t\tGENMASK(7, 0)\n+\n+#define REG_CDM_LRO_LIMIT(_n)\t\t(CDM_BASE(_n) + 0x84)\n+#define CDM_LRO_AGG_NUM_MASK\t\tGENMASK(23, 16)\n+#define CDM_LRO_AGG_SIZE_MASK\t\tGENMASK(15, 0)\n+\n+#define REG_CDM_LRO_AGE_TIME(_n)\t(CDM_BASE(_n) + 0x88)\n+#define CDM_LRO_AGE_TIME_MASK\t\tGENMASK(31, 16)\n+#define CDM_LRO_AGG_TIME_MASK\t\tGENMASK(15, 0)\n+\n #define REG_GDM_FWD_CFG(_n)\t\tGDM_BASE(_n)\n #define GDM_PAD_EN_MASK\t\t\tBIT(28)\n #define GDM_DROP_CRC_ERR_MASK\t\tBIT(23)\n@@ -887,9 +901,15 @@\n #define QDMA_ETH_RXMSG_SPORT_MASK\tGENMASK(25, 21)\n #define QDMA_ETH_RXMSG_CRSN_MASK\tGENMASK(20, 16)\n #define QDMA_ETH_RXMSG_PPE_ENTRY_MASK\tGENMASK(15, 0)\n+/* RX MSG2 */\n+#define QDMA_ETH_RXMSG_AGG_COUNT_MASK\tGENMASK(31, 24)\n+#define QDMA_ETH_RXMSG_L2_LEN_MASK\tGENMASK(6, 0)\n+/* RX MSG3 */\n+#define QDMA_ETH_RXMSG_AGG_LEN_MASK\tGENMASK(31, 16)\n+#define QDMA_ETH_RXMSG_TCP_WIN_MASK\tGENMASK(15, 0)\n \n struct airoha_qdma_desc {\n-\t__le32 rsv;\n+\t__le32 tcp_ts_reply;\n \t__le32 ctrl;\n \t__le32 addr;\n \t__le32 data;\n"
}

2/1 2026/08/07 11:22 agent patch-evaluator
0m Model:
best-expensive

Args:
null

Results:
{
  "EnableConfigs": null,
  "FocusSymbols": [],
  "Reasoning": "The patch only modifies the Airoha SoC ethernet driver (drivers/net/ethernet/airoha/*), which is a platform-specific driver for Airoha hardware. This hardware is not emulated in standard virtualized environments like QEMU or GCE, making the modified code paths unreachable during fuzzing.",
  "WorthFuzzing": false
}

Instruction:
You are an expert Linux kernel maintainer.
Your job is to review a provided patch series and determine
if it makes functional changes to the kernel that should be fuzzed.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively
use your code access tools to examine the actual source code and confirm any assumptions.

Return WorthFuzzing=false if the patch only contains:
- Modifications to Documentation/, Kconfig files, or code comments.
- Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints.
- Changes to numeric constants or macros that do not functionally alter execution flow.
- Code paths that are impossible to reach in virtualized environments like GCE or QEMU,
  even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim).
- Code in vendor-specific PCIe switch, SmartNIC, or GPU drivers (e.g., mlxsw, pds_core, qed,
  ionic, amdgpu) that require physical PCIe hardware cards not emulated in standard QEMU.
- Driver .remove, .shutdown, or pci_unregister_driver teardown callbacks (e.g., igb_remove)
  that are executed only during PCI hot-unplug or sysfs driver unbind operations.

If it modifies reachable core kernel logic, drivers, or architectures, use your code search
tools to verify the code can be executed, then return WorthFuzzing=true.

When returning WorthFuzzing=true, you MUST ALSO:
1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols.
   Avoid listing generic hot-path functions to prevent skewed test distributions.
   Prefer non-static, non-inlined API entrypoint functions over internal static helper functions
   (which are inlined by the compiler and do not have distinct symbol addresses).
2. Identify any specific CONFIG_ options required to properly test this new/modified feature.
   Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions.
   Also check "depends on" lines in Kconfig to include any non-standard parent subsystem configs
   needed for Kbuild to compile the code statically into vmlinux. List them in the EnableConfigs
   output array, and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit 3c6242a987ae4fb8eb6f627bb5f2c4a8137566a7
Author: syz-cluster <triage@syzkaller.com>
Date:   Fri Aug 7 11:22:34 2026 +0000

    syz-cluster: applied patch under review

diff --git a/drivers/net/ethernet/airoha/airoha_eth.c b/drivers/net/ethernet/airoha/airoha_eth.c
index 64619e9a704dd..e0d74d0cb36bb 100644
--- a/drivers/net/ethernet/airoha/airoha_eth.c
+++ b/drivers/net/ethernet/airoha/airoha_eth.c
@@ -10,8 +10,10 @@
 #include <linux/tcp.h>
 #include <linux/u64_stats_sync.h>
 #include <net/dst_metadata.h>
+#include <net/ip6_checksum.h>
 #include <net/page_pool/helpers.h>
 #include <net/pkt_cls.h>
+#include <net/tcp.h>
 #include <uapi/linux/ppp_defs.h>
 
 #include "airoha_regs.h"
@@ -491,6 +493,88 @@ static void airoha_fe_crsn_qsel_init(struct airoha_eth *eth)
 				 CDM_CRSN_QSEL_Q1));
 }
 
+static void airoha_fe_lro_rxq_enable(struct airoha_eth *eth, int qdma_id,
+				     int lro_queue_index, int qid,
+				     int buf_size)
+{
+	int id = qdma_id + 1;
+
+	airoha_fe_rmw(eth, REG_CDM_LRO_LIMIT(id),
+		      CDM_LRO_AGG_NUM_MASK | CDM_LRO_AGG_SIZE_MASK,
+		      FIELD_PREP(CDM_LRO_AGG_SIZE_MASK, buf_size) |
+		      FIELD_PREP(CDM_LRO_AGG_NUM_MASK,
+				 AIROHA_RXQ_LRO_MAX_AGG_COUNT));
+	airoha_fe_rmw(eth, REG_CDM_LRO_AGE_TIME(id),
+		      CDM_LRO_AGE_TIME_MASK | CDM_LRO_AGG_TIME_MASK,
+		      FIELD_PREP(CDM_LRO_AGE_TIME_MASK,
+				 AIROHA_RXQ_LRO_MAX_AGE_TIME) |
+		      FIELD_PREP(CDM_LRO_AGG_TIME_MASK,
+				 AIROHA_RXQ_LRO_MAX_AGG_TIME));
+	airoha_fe_rmw(eth, REG_CDM_LRO_RXQ(id, lro_queue_index),
+		      LRO_RXQ_MASK(lro_queue_index),
+		      __field_prep(LRO_RXQ_MASK(lro_queue_index), qid));
+	airoha_fe_set(eth, REG_CDM_LRO_EN(id), BIT(lro_queue_index));
+}
+
+static void airoha_fe_lro_disable(struct airoha_eth *eth, int qdma_id)
+{
+	int i, id = qdma_id + 1;
+
+	airoha_fe_clear(eth, REG_CDM_LRO_EN(id), LRO_RXQ_EN_MASK);
+	airoha_fe_clear(eth, REG_CDM_LRO_LIMIT(id),
+			CDM_LRO_AGG_NUM_MASK | CDM_LRO_AGG_SIZE_MASK);
+	airoha_fe_clear(eth, REG_CDM_LRO_AGE_TIME(id),
+			CDM_LRO_AGE_TIME_MASK | CDM_LRO_AGG_TIME_MASK);
+	for (i = 0; i < AIROHA_MAX_NUM_LRO_QUEUES; i++)
+		airoha_fe_clear(eth, REG_CDM_LRO_RXQ(id, i), LRO_RXQ_MASK(i));
+}
+
+static bool airoha_fe_lro_is_enabled(struct airoha_eth *eth, int qdma_id)
+{
+	return airoha_fe_get(eth, REG_CDM_LRO_EN(qdma_id + 1),
+			     LRO_RXQ_EN_MASK);
+}
+
+static void airoha_dev_lro_enable(struct airoha_gdm_dev *dev)
+{
+	struct airoha_qdma *qdma = airoha_qdma_deref(dev);
+	struct airoha_eth *eth = qdma->eth;
+	int qdma_id = qdma - &eth->qdma[0];
+	int i, lro_queue_index = 0;
+
+	if (airoha_fe_lro_is_enabled(eth, qdma_id))
+		return;
+
+	for (i = 0; i < ARRAY_SIZE(qdma->q_rx); i++) {
+		struct airoha_queue *q = &qdma->q_rx[i];
+		u32 size;
+
+		if (!q->ndesc)
+			continue;
+
+		if (!airoha_qdma_is_lro_queue(q))
+			continue;
+
+		size = SKB_WITH_OVERHEAD(AIROHA_RX_LEN(q->buf_size));
+		size = min_t(u32, size, AIROHA_MAX_RX_SIZE);
+		airoha_fe_lro_rxq_enable(eth, qdma_id, lro_queue_index, i,
+					 size);
+		lro_queue_index++;
+	}
+}
+
+static void airoha_dev_lro_disable(struct airoha_gdm_dev *dev)
+{
+	struct airoha_qdma *qdma = airoha_qdma_deref(dev);
+	struct airoha_eth *eth = qdma->eth;
+	int qdma_id = qdma - &eth->qdma[0];
+
+	if (!airoha_fe_lro_is_enabled(eth, qdma_id))
+		return;
+
+	airoha_fe_lro_disable(eth, qdma_id);
+}
+
 static int airoha_fe_init(struct airoha_eth *eth)
 {
 	airoha_fe_maccr_init(eth);
@@ -616,6 +700,7 @@ static int airoha_qdma_fill_rx_queue(struct airoha_queue *q)
 		e->dma_addr = page_pool_get_dma_addr(page) + offset;
 		e->dma_len = SKB_WITH_OVERHEAD(AIROHA_RX_LEN(q->buf_size));
 
+		WRITE_ONCE(desc->tcp_ts_reply, 0);
 		val = FIELD_PREP(QDMA_DESC_LEN_MASK, e->dma_len);
 		WRITE_ONCE(desc->ctrl, cpu_to_le32(val));
 		WRITE_ONCE(desc->addr, cpu_to_le32(e->dma_addr));
@@ -657,18 +742,188 @@ airoha_qdma_get_gdm_dev(struct airoha_eth *eth, struct airoha_qdma_desc *desc)
 	return port->devs[d] ? port->devs[d] : ERR_PTR(-ENODEV);
 }
 
+static struct sk_buff *airoha_qdma_lro_rx_skb(struct airoha_queue *q,
+					      struct airoha_qdma_desc *desc,
+					      struct airoha_queue_entry *e,
+					      u32 len, u32 agg_count)
+{
+	u32 msg1 = le32_to_cpu(READ_ONCE(desc->msg1));
+	u32 msg2 = le32_to_cpu(READ_ONCE(desc->msg2));
+	u32 msg3 = le32_to_cpu(READ_ONCE(desc->msg3));
+	u32 th_off, tcp_ack_seq, data_off, data_len;
+	struct skb_shared_info *shinfo;
+	u16 tcp_win, l2_len;
+	struct sk_buff *skb;
+	struct tcphdr *th;
+	struct page *page;
+	bool ipv4, ipv6;
+
+	ipv4 = FIELD_GET(QDMA_ETH_RXMSG_IP4_MASK, msg1);
+	ipv6 = FIELD_GET(QDMA_ETH_RXMSG_IP6_MASK, msg1);
+	if (!ipv4 && !ipv6)
+		return NULL;
+
+	l2_len = FIELD_GET(QDMA_ETH_RXMSG_L2_LEN_MASK, msg2);
+
+	if (ipv4) {
+		struct iphdr *iph;
+
+		if (len < l2_len + sizeof(*iph))
+			return NULL;
+
+		iph = (struct iphdr *)(e->buf + l2_len);
+		if (iph->protocol != IPPROTO_TCP)
+			return NULL;
+
+		if (iph->ihl < 5)
+			return NULL;
+
+		th_off = l2_len + (iph->ihl << 2);
+		if (len < th_off)
+			return NULL;
+
+		iph->tot_len = cpu_to_be16(len - l2_len);
+		iph->check = 0;
+		iph->check = ip_fast_csum((void *)iph, iph->ihl);
+	} else {
+		struct ipv6hdr *ip6h;
+
+		th_off = l2_len + sizeof(*ip6h);
+		if (len < th_off)
+			return NULL;
+
+		ip6h = (struct ipv6hdr *)(e->buf + l2_len);
+		if (ip6h->nexthdr != NEXTHDR_TCP)
+			return NULL;
+
+		ip6h->payload_len = cpu_to_be16(len - th_off);
+	}
+
+	if (len < th_off + sizeof(*th))
+		return NULL;
+
+	th = (struct tcphdr *)(e->buf + th_off);
+	if (th->doff < 5)
+		return NULL;
+
+	data_off = th_off + (th->doff << 2);
+	if (len <= data_off)
+		return NULL;
+
+	tcp_win = FIELD_GET(QDMA_ETH_RXMSG_TCP_WIN_MASK, msg3);
+	tcp_ack_seq = le32_to_cpu(READ_ONCE(desc->data));
+	th->ack_seq = cpu_to_be32(tcp_ack_seq);
+	th->window = cpu_to_be16(tcp_win);
+
+	/* Check tcp timestamp option */
+	if (th->doff == (sizeof(*th) + TCPOLEN_TSTAMP_ALIGNED) / 4) {
+		u32 topt = get_unaligned_be32(th + 1);
+
+		if (topt == ((TCPOPT_NOP << 24) | (TCPOPT_NOP << 16) |
+			     (TCPOPT_TIMESTAMP << 8) | TCPOLEN_TIMESTAMP)) {
+			u8 *ptr = (u8 *)th + sizeof(*th) + 2 * sizeof(__be32);
+			__le32 tcp_ts_reply = READ_ONCE(desc->tcp_ts_reply);
+
+			/* The field is pre-zeroed in the posted descriptor,
+			 * so a non-zero value is the hardware confirming the
+			 * timestamp echo reply is valid. Leave TSecr
+			 * untouched otherwise, since the hardware may not
+			 * populate it for every aggregate.
+			 */
+			if (tcp_ts_reply)
+				put_unaligned_be32(le32_to_cpu(tcp_ts_reply),
+						   ptr);
+		}
+	}
+
+	if (ipv4) {
+		struct iphdr *iph = (struct iphdr *)(e->buf + l2_len);
+
+		th->check = ~tcp_v4_check(len - th_off, iph->saddr,
+					  iph->daddr, 0);
+	} else {
+		struct ipv6hdr *ip6h = (struct ipv6hdr *)(e->buf + l2_len);
+
+		th->check = ~tcp_v6_check(len - th_off, &ip6h->saddr,
+					  &ip6h->daddr, 0);
+	}
+
+	/* Split network headers and payload to rely on GRO.
+	 * We need to do it in the driver since the NIC does
+	 * not support it.
+	 */
+	skb = napi_alloc_skb(&q->napi, data_off);
+	if (!skb)
+		return NULL;
+
+	__skb_put(skb, data_off);
+	memcpy(skb->data, e->buf, data_off);
+
+	page = virt_to_head_page(e->buf);
+	data_len = len - data_off;
+	shinfo = skb_shinfo(skb);
+	skb_add_rx_frag(skb, shinfo->nr_frags, page,
+			e->buf + data_off - page_address(page), data_len,
+			q->buf_size);
+
+	shinfo->gso_type = ipv4 ? SKB_GSO_TCPV4 : SKB_GSO_TCPV6;
+	/* HW does not report the per-segment MSS, so approximate it as
+	 * the average segment size. For a stream of MSS-sized segments
+	 * with a shorter tail this still re-segments into exactly
+	 * agg_count segments, keeping gso_size/gso_segs and the packet
+	 * accounting coherent, while never overestimating the MSS.
+	 */
+	shinfo->gso_size = DIV_ROUND_UP(data_len, agg_count);
+	shinfo->gso_segs = agg_count;
+
+	skb->csum_start = skb_headroom(skb) + th_off;
+	skb->csum_offset = offsetof(struct tcphdr, check);
+	skb->ip_summed = CHECKSUM_PARTIAL;
+
+	return skb;
+}
+
+static struct sk_buff *airoha_qdma_build_rx_skb(struct airoha_queue *q,
+						struct airoha_qdma_desc *desc,
+						struct airoha_queue_entry *e,
+						struct net_device *dev,
+						u32 len, u32 agg_count)
+{
+	int qid = q - &q->qdma->q_rx[0];
+	struct sk_buff *skb;
+
+	if (agg_count > 1 && airoha_qdma_is_lro_queue(q)) { /* LRO */
+		skb = airoha_qdma_lro_rx_skb(q, desc, e, len, agg_count);
+		if (!skb)
+			return NULL;
+	} else {
+		skb = napi_build_skb(e->buf - AIROHA_RX_HEADROOM, q->buf_size);
+		if (!skb)
+			return NULL;
+
+		skb_reserve(skb, AIROHA_RX_HEADROOM);
+		__skb_put(skb, len);
+		skb->ip_summed = CHECKSUM_UNNECESSARY;
+	}
+
+	skb_mark_for_recycle(skb);
+	skb->dev = dev;
+	skb_record_rx_queue(skb, qid);
+	skb->protocol = eth_type_trans(skb, dev);
+
+	return skb;
+}
+
 static int airoha_qdma_rx_process(struct airoha_queue *q, int budget)
 {
 	enum dma_data_direction dir = page_pool_get_dma_dir(q->page_pool);
-	struct airoha_qdma *qdma = q->qdma;
-	struct airoha_eth *eth = qdma->eth;
-	int qid = q - &qdma->q_rx[0];
+	struct airoha_eth *eth = q->qdma->eth;
 	int done = 0;
 
 	while (done < budget) {
+		u32 hash, reason, msg1, msg2, desc_ctrl, agg_count;
 		struct airoha_queue_entry *e = &q->entry[q->tail];
 		struct airoha_qdma_desc *desc = &q->desc[q->tail];
-		u32 hash, reason, msg1, desc_ctrl;
 		struct airoha_gdm_dev *dev;
 		struct net_device *netdev;
 		int data_len, len;
@@ -692,24 +947,21 @@ static int airoha_qdma_rx_process(struct airoha_queue *q, int budget)
 		if (!len || data_len < len)
 			goto free_frag;
 
+		msg2 = le32_to_cpu(READ_ONCE(desc->msg2));
+		agg_count = FIELD_GET(QDMA_ETH_RXMSG_AGG_COUNT_MASK, msg2);
+		if (agg_count > AIROHA_RXQ_LRO_MAX_AGG_COUNT)
+			goto free_frag;
+
 		dev = airoha_qdma_get_gdm_dev(eth, desc);
 		if (IS_ERR(dev))
 			goto free_frag;
 
 		netdev = netdev_from_priv(dev);
 		if (!q->skb) { /* first buffer */
-			q->skb = napi_build_skb(e->buf - AIROHA_RX_HEADROOM,
-						q->buf_size);
+			q->skb = airoha_qdma_build_rx_skb(q, desc, e, netdev,
+							  len, agg_count);
 			if (!q->skb)
 				goto free_frag;
-
-			skb_reserve(q->skb, AIROHA_RX_HEADROOM);
-			__skb_put(q->skb, len);
-			skb_mark_for_recycle(q->skb);
-			q->skb->dev = netdev;
-			q->skb->protocol = eth_type_trans(q->skb, netdev);
-			q->skb->ip_summed = CHECKSUM_UNNECESSARY;
-			skb_record_rx_queue(q->skb, qid);
 		} else { /* scattered frame */
 			struct skb_shared_info *shinfo = skb_shinfo(q->skb);
 			int nr_frags = shinfo->nr_frags;
@@ -800,12 +1052,10 @@ static int airoha_qdma_rx_napi_poll(struct napi_struct *napi, int budget)
 static int airoha_qdma_init_rx_queue(struct airoha_queue *q,
 				     struct airoha_qdma *qdma, int ndesc)
 {
-	const struct page_pool_params pp_params = {
-		.order = 0,
+	struct page_pool_params pp_params = {
 		.pool_size = 256,
 		.flags = PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV,
 		.dma_dir = DMA_FROM_DEVICE,
-		.max_len = PAGE_SIZE,
 		.nid = NUMA_NO_NODE,
 		.dev = qdma->eth->dev,
 		.napi = &q->napi,
@@ -813,9 +1063,10 @@ static int airoha_qdma_init_rx_queue(struct airoha_queue *q,
 	struct airoha_eth *eth = qdma->eth;
 	int qid = q - &qdma->q_rx[0], thr;
 	dma_addr_t dma_addr;
+	bool lro_q;
 
-	q->buf_size = PAGE_SIZE / 2;
 	q->qdma = qdma;
+	lro_q = airoha_qdma_is_lro_queue(q);
 
 	q->entry = devm_kzalloc(eth->dev, ndesc * sizeof(*q->entry),
 				GFP_KERNEL);
@@ -827,6 +1078,9 @@ static int airoha_qdma_init_rx_queue(struct airoha_queue *q,
 	if (!q->desc)
 		return -ENOMEM;
 
+	pp_params.order = lro_q ? AIROHA_LRO_PAGE_ORDER : 0;
+	pp_params.max_len = PAGE_SIZE << pp_params.order;
+
 	q->page_pool = page_pool_create(&pp_params);
 	if (IS_ERR(q->page_pool)) {
 		int err = PTR_ERR(q->page_pool);
@@ -835,6 +1089,7 @@ static int airoha_qdma_init_rx_queue(struct airoha_queue *q,
 		return err;
 	}
 
+	q->buf_size = lro_q ? pp_params.max_len : pp_params.max_len / 2;
 	q->ndesc = ndesc;
 	netif_napi_add(eth->napi_dev, &q->napi, airoha_qdma_rx_napi_poll);
 
@@ -848,7 +1103,12 @@ static int airoha_qdma_init_rx_queue(struct airoha_queue *q,
 			FIELD_PREP(RX_RING_THR_MASK, thr));
 	airoha_qdma_rmw(qdma, REG_RX_DMA_IDX(qid), RX_RING_DMA_IDX_MASK,
 			FIELD_PREP(RX_RING_DMA_IDX_MASK, q->head));
-	airoha_qdma_set(qdma, REG_RX_SCATTER_CFG(qid), RX_RING_SG_EN_MASK);
+	if (lro_q)
+		airoha_qdma_clear(qdma, REG_RX_SCATTER_CFG(qid),
+				  RX_RING_SG_EN_MASK);
+	else
+		airoha_qdma_set(qdma, REG_RX_SCATTER_CFG(qid),
+				RX_RING_SG_EN_MASK);
 
 	airoha_qdma_fill_rx_queue(q);
 
@@ -870,6 +1130,7 @@ static void airoha_qdma_cleanup_rx_queue(struct airoha_queue *q)
 					page_pool_get_dma_dir(q->page_pool));
 		page_pool_put_full_page(q->page_pool, page, false);
 		/* Reset DMA descriptor */
+		WRITE_ONCE(desc->tcp_ts_reply, 0);
 		WRITE_ONCE(desc->ctrl, 0);
 		WRITE_ONCE(desc->addr, 0);
 		WRITE_ONCE(desc->data, 0);
@@ -1896,6 +2157,34 @@ static void airoha_update_hw_stats(struct airoha_gdm_dev *dev)
 	spin_unlock(&port->stats_lock);
 }
 
+static void airoha_update_netdev_features(struct airoha_gdm_dev *dev)
+{
+	struct airoha_eth *eth = dev->eth;
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(eth->ports); i++) {
+		struct airoha_gdm_port *port = eth->ports[i];
+		int j;
+
+		if (!port)
+			continue;
+
+		for (j = 0; j < ARRAY_SIZE(port->devs); j++) {
+			struct airoha_gdm_dev *iter_dev = port->devs[j];
+			struct net_device *netdev;
+
+			if (!iter_dev)
+				continue;
+
+			netdev = netdev_from_priv(iter_dev);
+			if (netdev->reg_state != NETREG_REGISTERED)
+				continue;
+
+			netdev_update_features(netdev);
+		}
+	}
+}
+
 static void airoha_dev_set_xmit_frame_size(struct net_device *netdev)
 {
 	struct airoha_gdm_dev *dev = netdev_priv(netdev);
@@ -1913,14 +2202,28 @@ static int airoha_dev_open(struct net_device *netdev)
 	struct airoha_gdm_port *port = dev->port;
 	u32 pse_port = FE_PSE_PORT_PPE1;
 	struct airoha_qdma *qdma;
-	int err;
+	int qdma_id, err;
+
+	/* HW GRO is configured on the QDMA and it is shared between
+	 * all the devices using it. Refuse to open a second device on
+	 * the same QDMA if HW GRO is enabled on any device sharing it
+	 * or if this device has HW GRO enabled in its feature set.
+	 */
+	qdma = airoha_qdma_deref(dev);
+	qdma_id = qdma - &qdma->eth->qdma[0];
+
+	if (qdma->users > 1 && (airoha_fe_lro_is_enabled(qdma->eth, qdma_id) ||
+				(netdev->features & NETIF_F_GRO_HW))) {
+		netdev_warn(netdev, "required to disable HW GRO on QDMA%d\n",
+			    qdma_id);
+		return -EBUSY;
+	}
 
 	netif_tx_start_all_queues(netdev);
 	err = airoha_set_vip_for_gdm_port(dev, true);
 	if (err)
 		return err;
 
-	qdma = airoha_qdma_deref(dev);
 	if (netdev_uses_dsa(netdev))
 		airoha_fe_set(qdma->eth, REG_GDM_INGRESS_CFG(port->id),
 			      GDM_STAG_EN_MASK);
@@ -1937,6 +2240,9 @@ static int airoha_dev_open(struct net_device *netdev)
 	airoha_set_gdm_port_fwd_cfg(qdma->eth, REG_GDM_FWD_CFG(port->id),
 				    pse_port);
 
+	if (netdev->features & NETIF_F_GRO_HW)
+		airoha_dev_lro_enable(dev);
+
 	return 0;
 }
 
@@ -1954,6 +2260,10 @@ static int airoha_dev_stop(struct net_device *netdev)
 		airoha_set_gdm_port_fwd_cfg(dev->eth,
 					    REG_GDM_FWD_CFG(port->id),
 					    FE_PSE_PORT_DROP);
+
+	if (netdev->features & NETIF_F_GRO_HW)
+		airoha_dev_lro_disable(dev);
+
 	return 0;
 }
 
@@ -2104,6 +2414,34 @@ airoha_get_wan_gdm_dev(struct airoha_eth *eth)
 	return NULL;
 }
 
+static int airoha_dev_check_hw_gro(struct airoha_gdm_dev *dev)
+{
+	struct airoha_qdma *qdma = airoha_qdma_deref(dev);
+	struct airoha_eth *eth = qdma->eth;
+	int qdma_id = qdma - &eth->qdma[0];
+
+	if (airoha_fe_lro_is_enabled(eth, qdma_id)) {
+		struct net_device *netdev = netdev_from_priv(dev);
+
+		netdev_warn(netdev, "HW GRO is enabled on QDMA%d\n", qdma_id);
+		return -EBUSY;
+	}
+
+	qdma = &eth->qdma[!qdma_id];
+	qdma_id = qdma - &eth->qdma[0];
+
+	if (airoha_fe_lro_is_enabled(eth, qdma_id)) {
+		struct net_device *netdev = netdev_from_priv(dev);
+
+		netdev_warn(netdev,
+			    "HW GRO is enabled on destination QDMA%d\n",
+			    qdma_id);
+		return -EBUSY;
+	}
+
+	return 0;
+}
+
 static void airoha_dev_set_qdma(struct airoha_gdm_dev *dev)
 {
 	struct net_device *netdev = netdev_from_priv(dev);
@@ -2120,6 +2458,7 @@ static void airoha_dev_set_qdma(struct airoha_gdm_dev *dev)
 
 	rcu_assign_pointer(dev->qdma, qdma);
 	netdev->irq = qdma->irq_banks[0].irq;
+	qdma->users++;
 	synchronize_rcu();
 
 	ppe_id = !airoha_is_lan_gdm_dev(dev) && airoha_ppe_is_enabled(eth, 1);
@@ -2136,8 +2475,12 @@ static void airoha_dev_set_qdma(struct airoha_gdm_dev *dev)
 			airoha_qdma_rr(qdma, REG_CNTR_VAL((i << 1) + 1));
 	}
 
-	if (cur_qdma)
+	if (cur_qdma) {
+		cur_qdma->users--;
 		netif_tx_wake_all_queues(netdev);
+	}
+
+	airoha_update_netdev_features(dev);
 }
 
 static int airoha_dev_init(struct net_device *netdev)
@@ -2288,6 +2631,36 @@ int airoha_get_fe_port(struct airoha_gdm_dev *dev)
 	}
 }
 
+static netdev_features_t airoha_dev_fix_features(struct net_device *netdev,
+						 netdev_features_t features)
+{
+	struct airoha_gdm_dev *dev = netdev_priv(netdev);
+	struct airoha_qdma *qdma;
+
+	qdma = airoha_qdma_deref(dev);
+	if (qdma->users > 1)
+		features &= ~NETIF_F_GRO_HW;
+
+	return features;
+}
+
+static int airoha_dev_set_features(struct net_device *netdev,
+				   netdev_features_t features)
+{
+	netdev_features_t diff = netdev->features ^ features;
+	struct airoha_gdm_dev *dev = netdev_priv(netdev);
+
+	if (!(diff & NETIF_F_GRO_HW))
+		return 0;
+
+	if (features & NETIF_F_GRO_HW)
+		airoha_dev_lro_enable(dev);
+	else
+		airoha_dev_lro_disable(dev);
+
+	return 0;
+}
+
 static netdev_tx_t airoha_dev_xmit(struct sk_buff *skb,
 				   struct net_device *netdev)
 {
@@ -3199,7 +3572,7 @@ static int airoha_enable_qos_for_gdm34(struct net_device *netdev,
 	struct airoha_gdm_dev *wan_dev, *dev = netdev_priv(netdev);
 	struct airoha_gdm_port *port = dev->port;
 	struct airoha_eth *eth = dev->eth;
-	int err = -EBUSY;
+	int err;
 
 	if (port->id != AIROHA_GDM3_IDX &&
 	    port->id != AIROHA_GDM4_IDX) {
@@ -3210,6 +3583,10 @@ static int airoha_enable_qos_for_gdm34(struct net_device *netdev,
 	if (!airoha_is_lan_gdm_dev(dev)) /* Already enabled */
 		return 0;
 
+	err = airoha_dev_check_hw_gro(dev);
+	if (err)
+		return err;
+
 	mutex_lock(&flow_offload_mutex);
 
 	wan_dev = airoha_get_wan_gdm_dev(eth);
@@ -3218,6 +3595,7 @@ static int airoha_enable_qos_for_gdm34(struct net_device *netdev,
 		    wan_dev->port->id == AIROHA_GDM2_IDX) {
 			NL_SET_ERR_MSG_MOD(extack,
 					   "QoS configured for WAN device");
+			err = -EBUSY;
 			goto error_unlock;
 		}
 		airoha_disable_qos_for_gdm34(netdev_from_priv(wan_dev));
@@ -3343,6 +3721,8 @@ static const struct net_device_ops airoha_netdev_ops = {
 	.ndo_stop		= airoha_dev_stop,
 	.ndo_change_mtu		= airoha_dev_change_mtu,
 	.ndo_select_queue	= airoha_dev_select_queue,
+	.ndo_fix_features	= airoha_dev_fix_features,
+	.ndo_set_features	= airoha_dev_set_features,
 	.ndo_start_xmit		= airoha_dev_xmit,
 	.ndo_get_stats64        = airoha_dev_get_stats64,
 	.ndo_set_mac_address	= airoha_dev_set_macaddr,
@@ -3430,11 +3810,9 @@ static int airoha_alloc_gdm_device(struct airoha_eth *eth,
 	netdev->ethtool_ops = &airoha_ethtool_ops;
 	netdev->max_mtu = AIROHA_MAX_MTU;
 	netdev->watchdog_timeo = 5 * HZ;
-	netdev->hw_features = NETIF_F_IP_CSUM | NETIF_F_RXCSUM | NETIF_F_TSO6 |
-			      NETIF_F_IPV6_CSUM | NETIF_F_SG | NETIF_F_TSO |
-			      NETIF_F_HW_TC;
-	netdev->features |= netdev->hw_features;
-	netdev->vlan_features = netdev->hw_features;
+	netdev->hw_features = AIROHA_HW_FEATURES | NETIF_F_GRO_HW;
+	netdev->features |= AIROHA_HW_FEATURES;
+	netdev->vlan_features = AIROHA_HW_FEATURES;
 	SET_NETDEV_DEV(netdev, eth->dev);
 
 	/* reserve hw queues for HTB offloading */
diff --git a/drivers/net/ethernet/airoha/airoha_eth.h b/drivers/net/ethernet/airoha/airoha_eth.h
index fa9a8edce22ff..9b55abc630484 100644
--- a/drivers/net/ethernet/airoha/airoha_eth.h
+++ b/drivers/net/ethernet/airoha/airoha_eth.h
@@ -45,6 +45,18 @@
 	 (_n) == 15 ? 128 :		\
 	 (_n) ==  0 ? 1024 : 16)
 
+#define AIROHA_LRO_PAGE_ORDER		get_order(SZ_16K)
+#define AIROHA_MAX_NUM_LRO_QUEUES	8
+#define AIROHA_RXQ_LRO_EN_MASK		GENMASK(31, 24)
+#define AIROHA_RXQ_LRO_MAX_AGG_COUNT	64
+#define AIROHA_RXQ_LRO_MAX_AGG_TIME	100
+#define AIROHA_RXQ_LRO_MAX_AGE_TIME	2000
+
+#define AIROHA_HW_FEATURES			\
+	(NETIF_F_IP_CSUM | NETIF_F_RXCSUM |	\
+	 NETIF_F_TSO6 | NETIF_F_IPV6_CSUM |	\
+	 NETIF_F_SG | NETIF_F_TSO | NETIF_F_HW_TC)
+
 #define PSE_RSV_PAGES			128
 #define PSE_QUEUE_RSV_PAGES		64
 
@@ -538,7 +550,7 @@ struct airoha_wdma_info {
 
 /* RX queue to IRQ mapping: BIT(q) in IRQ(n) */
 #define RX_IRQ0_BANK_PIN_MASK			0x839f
-#define RX_IRQ1_BANK_PIN_MASK			0x7fe00000
+#define RX_IRQ1_BANK_PIN_MASK			0xffe00000
 #define RX_IRQ2_BANK_PIN_MASK			0x20
 #define RX_IRQ3_BANK_PIN_MASK			0x40
 #define RX_IRQ_BANK_PIN_MASK(_n)		\
@@ -560,6 +572,8 @@ struct airoha_qdma {
 	struct airoha_eth *eth;
 	void __iomem *regs;
 
+	int users;
+
 	struct airoha_irq_bank irq_banks[AIROHA_MAX_NUM_IRQ_BANKS];
 
 	struct airoha_tx_irq_queue q_tx_irq[AIROHA_NUM_TX_IRQ];
@@ -714,6 +728,18 @@ static inline bool airoha_is_7583(struct airoha_eth *eth)
 	return eth->soc->version == 0x7583;
 }
 
+static inline bool airoha_qdma_is_lro_queue(struct airoha_queue *q)
+{
+	struct airoha_qdma *qdma = q->qdma;
+	int qid = q - &qdma->q_rx[0];
+
+	/* EN7581 SoC supports at most 8 LRO rx queues */
+	BUILD_BUG_ON(hweight32(AIROHA_RXQ_LRO_EN_MASK) >
+		     AIROHA_MAX_NUM_LRO_QUEUES);
+
+	return !!(AIROHA_RXQ_LRO_EN_MASK & BIT(qid));
+}
+
 int airoha_get_fe_port(struct airoha_gdm_dev *dev);
 bool airoha_is_valid_gdm_dev(struct airoha_eth *eth,
 			     struct airoha_gdm_dev *dev);
diff --git a/drivers/net/ethernet/airoha/airoha_regs.h b/drivers/net/ethernet/airoha/airoha_regs.h
index 442b48c9b991e..0ec41c3b4c066 100644
--- a/drivers/net/ethernet/airoha/airoha_regs.h
+++ b/drivers/net/ethernet/airoha/airoha_regs.h
@@ -122,6 +122,20 @@
 #define CDM_CRSN_QSEL_REASON_MASK(_n)	\
 	GENMASK(4 + (((_n) % 4) << 3),	(((_n) % 4) << 3))
 
+#define REG_CDM_LRO_RXQ(_n, _m)		(CDM_BASE(_n) + 0x78 + ((_m) & 0x4))
+#define LRO_RXQ_MASK(_n)		GENMASK(4 + (((_n) & 0x3) << 3), ((_n) & 0x3) << 3)
+
+#define REG_CDM_LRO_EN(_n)		(CDM_BASE(_n) + 0x80)
+#define LRO_RXQ_EN_MASK			GENMASK(7, 0)
+
+#define REG_CDM_LRO_LIMIT(_n)		(CDM_BASE(_n) + 0x84)
+#define CDM_LRO_AGG_NUM_MASK		GENMASK(23, 16)
+#define CDM_LRO_AGG_SIZE_MASK		GENMASK(15, 0)
+
+#define REG_CDM_LRO_AGE_TIME(_n)	(CDM_BASE(_n) + 0x88)
+#define CDM_LRO_AGE_TIME_MASK		GENMASK(31, 16)
+#define CDM_LRO_AGG_TIME_MASK		GENMASK(15, 0)
+
 #define REG_GDM_FWD_CFG(_n)		GDM_BASE(_n)
 #define GDM_PAD_EN_MASK			BIT(28)
 #define GDM_DROP_CRC_ERR_MASK		BIT(23)
@@ -887,9 +901,15 @@
 #define QDMA_ETH_RXMSG_SPORT_MASK	GENMASK(25, 21)
 #define QDMA_ETH_RXMSG_CRSN_MASK	GENMASK(20, 16)
 #define QDMA_ETH_RXMSG_PPE_ENTRY_MASK	GENMASK(15, 0)
+/* RX MSG2 */
+#define QDMA_ETH_RXMSG_AGG_COUNT_MASK	GENMASK(31, 24)
+#define QDMA_ETH_RXMSG_L2_LEN_MASK	GENMASK(6, 0)
+/* RX MSG3 */
+#define QDMA_ETH_RXMSG_AGG_LEN_MASK	GENMASK(31, 16)
+#define QDMA_ETH_RXMSG_TCP_WIN_MASK	GENMASK(15, 0)
 
 struct airoha_qdma_desc {
-	__le32 rsv;
+	__le32 tcp_ts_reply;
 	__le32 ctrl;
 	__le32 addr;
 	__le32 data;

3/2 2026/08/07 11:22 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 12857
							output: 34
							thoughts: 368
						

Thoughts:
**My Assessment of the Airoha Ethernet Driver Patch**

Alright, let's take a look at this patch series. My initial thought is to determine if these changes to the Airoha ethernet driver, specifically within `drivers/net/ethernet/airoha/`, introduce any functional modifications to the kernel that warrant fuzzing.

It seems we're dealing with Large Receive Offload (LRO), or HW GRO support, being added to the Airoha ethernet driver. The specific files modified – `airoha_eth.c`, `.h`, and `airoha_regs.h` – confirm this. Okay, I see the new functions related to enabling/disabling LRO at the RX queue and device level: `airoha_fe_lro_rxq_enable`, `airoha_fe_lro_disable`, `airoha_dev_lro_enable`, and `airoha_dev_lro_disable`. There are also functions to build and handle the SKBs – `airoha_qdma_lro_rx_skb`, and `airoha_qdma_build_rx_skb`–, and critical modifications to existing functions, specifically `airoha_qdma_rx_process`, `airoha_qdma_init_rx_queue`, `airoha_dev_open`, `airoha_dev_stop`, `airoha_dev_init`, `airoha_dev_set_features`, and `airoha_dev_fix_features`. These are all signs of a significant change to the RX path.

My next concern is reachability. This is the Airoha ethernet driver, and based on my prior knowledge, it's specific to the EN7581 SoC. So, the question becomes: can I actually trigger this driver in a virtualized environment like GCE or QEMU? We need to determine if this is a platform driver or a PCI driver. I can use `grepper` to search for `airoha_eth` or `airoha_probe` to figure this out. If it's a platform driver, reaching it in a standard VM setup might be trickier, but certainly not impossible. If it's a PCI driver, that's better, since it's likely we can assign the emulated device to the VM. If it's not easily reachable, then it drops in priority for this kind of evaluation.



4/2 2026/08/07 11:22 tool grepper
0m Args:
{
  "Expression": "airoha.*probe",
  "PathPrefix": "drivers/net/ethernet/airoha/"
}

Results:
{
  "Output": "drivers/net/ethernet/airoha/airoha_eth.c=3938=static int airoha_register_gdm_devices(struct airoha_eth *eth)\n--\ndrivers/net/ethernet/airoha/airoha_eth.c-3966-\ndrivers/net/ethernet/airoha/airoha_eth.c:3967:static int airoha_probe(struct platform_device *pdev)\ndrivers/net/ethernet/airoha/airoha_eth.c-3968-{\n--\ndrivers/net/ethernet/airoha/airoha_eth.c=4332=static struct platform_driver airoha_driver = {\ndrivers/net/ethernet/airoha/airoha_eth.c:4333:\t.probe = airoha_probe,\ndrivers/net/ethernet/airoha/airoha_eth.c-4334-\t.remove = airoha_remove,\n--\ndrivers/net/ethernet/airoha/airoha_npu.c=685=static const struct regmap_config regmap_config = {\n--\ndrivers/net/ethernet/airoha/airoha_npu.c-692-\ndrivers/net/ethernet/airoha/airoha_npu.c:693:static int airoha_npu_probe(struct platform_device *pdev)\ndrivers/net/ethernet/airoha/airoha_npu.c-694-{\n--\ndrivers/net/ethernet/airoha/airoha_npu.c=813=static struct platform_driver airoha_npu_driver = {\ndrivers/net/ethernet/airoha/airoha_npu.c:814:\t.probe = airoha_npu_probe,\ndrivers/net/ethernet/airoha/airoha_npu.c-815-\t.remove = airoha_npu_remove,\n"
}

5/2 2026/08/07 11:22 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 14259
							output: 38
							thoughts: 68
						

Thoughts:
**Analyzing the Target Driver**

Okay, so I'm looking at a `platform_driver` here. That immediately tells me this is likely going to be SoC-specific, or at least highly integrated. I know from experience that platform drivers for a particular SoC, like this Airoha EN7581, are usually a no-go in standard environments like QEMU or GCE unless there's some custom emulator setup for it, or it's designed to be more generic. Given the likely platform dependence, my next step is clear. I need to dive into the Kconfig. That's where I'll get the real story – see if there are any hints about dependencies, build-time configurations, or any indication of broader support. Let's see what the Kconfig says.



6/2 2026/08/07 11:22 tool grepper
0m Args:
{
  "Expression": "config NET_VENDOR_AIROHA",
  "PathPrefix": "drivers/net/ethernet/airoha/"
}

Results:
{
  "Output": "drivers/net/ethernet/airoha/Kconfig-1-# SPDX-License-Identifier: GPL-2.0-only\ndrivers/net/ethernet/airoha/Kconfig:2:config NET_VENDOR_AIROHA\ndrivers/net/ethernet/airoha/Kconfig-3-\tbool \"Airoha devices\"\n"
}

7/2 2026/08/07 11:22 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 14622
							output: 38
							thoughts: 8
						

Thoughts:
Okay, here's my interpretation of that thought process, tailored for an expert audience, and written in the first person:

**Initial Kconfig Review**

Alright, time to dive in. "Let's read the Kconfig." That's the first step, isn't it? Before any serious work begins, you *always* need to understand the configuration space. I'm thinking, "Where are we even starting from? What are the key features and dependencies defined? Are there any obvious flags I should be immediately aware of?"

My mind's already sifting through possibilities. I'm wondering:

*   Is this a fresh Kconfig, or one I'm familiar with? That informs my approach. If I know it, I'll go for quick specific checks for certain variables.
*   Are there any include directives? It's essential to understand the organizational structure immediately, and determine whether I need to track down other Kconfig files.
*   Are there any obvious architectural constraints defined? Is it for a specific CPU, board, or build configuration? I want to contextualize this immediately.
*   Are there any potential conflicts with other modules, or features, that I need to be aware of? This is about to kick in my internal conflict resolution code for potential problems.
*   Am I looking to change something? Or understand what is available. That helps to focus my initial review.

So, I'm mentally sketching out a plan. It's time to find the scope.



8/2 2026/08/07 11:22 tool read-file
0m Args:
{
  "File": "drivers/net/ethernet/airoha/Kconfig",
  "FirstLine": 1,
  "LineCount": 20
}

Results:
{
  "Contents": "   1:\t# SPDX-License-Identifier: GPL-2.0-only\n   2:\tconfig NET_VENDOR_AIROHA\n   3:\t\tbool \"Airoha devices\"\n   4:\t\tdepends on ARCH_AIROHA || COMPILE_TEST\n   5:\t\thelp\n   6:\t\t  If you have a Airoha SoC with ethernet, say Y.\n   7:\t\n   8:\tif NET_VENDOR_AIROHA\n   9:\t\n  10:\tconfig NET_AIROHA_NPU\n  11:\t\ttristate \"Airoha NPU support\"\n  12:\t\tselect WANT_DEV_COREDUMP\n  13:\t\tselect REGMAP_MMIO\n  14:\t\thelp\n  15:\t\t  This driver supports Airoha Network Processor (NPU) available\n  16:\t\t  on the Airoha Soc family.\n  17:\t\n  18:\tconfig NET_AIROHA\n  19:\t\ttristate \"Airoha SoC Gigabit Ethernet support\"\n  20:\t\tdepends on NET_DSA || !NET_DSA\n"
}

9/2 2026/08/07 11:22 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 15261
							output: 96
							thoughts: 553
						

Thoughts:
**WorthFuzzing=false: Airoha Ethernet Driver**

Okay, let's break this down. This patch is targeting the Airoha Gigabit Ethernet driver (NET_AIROHA), which is a platform driver specifically for the Airoha SoC. Since it's a platform driver, it *needs* a specific device tree node or some kind of platform device registration to probe and, more importantly, execute the code this patch touches.

In a standard virtualized environment like QEMU, there's no Airoha SoC emulation by default. That means the `probe` function for this driver *will not* be called. All these modifications to `airoha_qdma_rx_process`, `airoha_dev_open`, etc., are completely inaccessible.

Could Syzkaller somehow create it? Maybe, using `syz_dev_create` or something similar? Possibly, if it was generically supported, or a virtual device like `mac80211_hwsim` or `veth`. But I don't see anything like that here. Plus, the instructions are clear: if the code paths are impossible to reach in virtualized environments, even with emulated hardware, it's a no-go.

This is a clear-cut case. It's a platform-specific driver, so, it's going to fall into that category of "impossible to reach".

I'll quickly double-check the diff just to be extra sure. It’s all within `drivers/net/ethernet/airoha/`, specifically `airoha_eth.c`, `airoha_eth.h`, and `airoha_regs.h`. Definitely just this Airoha Ethernet driver, nothing generic.

So, the reasoning is solid: the Airoha SoC driver is platform-specific. The code modifications are unreachable in virtualized environments like QEMU.

I'm calling `set-results` with `WorthFuzzing=false`.



10/2 2026/08/07 11:23 tool set-results
0m Args:
{
  "EnableConfigs": [],
  "FocusSymbols": [],
  "Reasoning": "The patch only modifies the Airoha SoC ethernet driver (drivers/net/ethernet/airoha/*), which is a platform-specific driver for Airoha hardware. This hardware is not emulated in standard virtualized environments like QEMU or GCE, making the modified code paths unreachable during fuzzing.",
  "WorthFuzzing": false
}

Results:
{
  "EnableConfigs": null,
  "FocusSymbols": [],
  "Reasoning": "The patch only modifies the Airoha SoC ethernet driver (drivers/net/ethernet/airoha/*), which is a platform-specific driver for Airoha hardware. This hardware is not emulated in standard virtualized environments like QEMU or GCE, making the modified code paths unreachable during fuzzing.",
  "WorthFuzzing": false
}

11/1 2026/08/07 11:23 action If
0m Args:
{
  "WorthFuzzing": false
}

Results:
null

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)