From 14a5f47562acce44f7488e78309cfc97735335e9 Mon Sep 17 00:00:00 2001 From: John Kearney Date: Mon, 31 Aug 2026 23:33:45 -0700 Subject: [PATCH] zircon: fix checksum end-around carry folding in IP TX deparse The TX checksum accumulators were folded a single time before truncation to 16 bits. When the sum is large enough for a carry to survive one end-around fold (reachable with maximal field values on jumbo or edge-value traffic), the emitted TCP/UDP/IPv4 checksum is off by one from the correct value. Fold a second time before the final truncation so that no carry can survive: for any 21-bit accumulator value, folding twice always yields a value that fits in 16 bits (verified exhaustively over all 2^21 inputs and against 200k randomized records vs RFC 1071 reference vectors). The same fix is applied to the L3 checksum path, where it additionally repairs the truncated short-record path (idempotent for full records). Adds a cocotb regression to the tx_deparse testbench driving maximal checksum inputs on TCP/UDP over IPv4/IPv6; it fails on the previous behaviour (off-by-one) and passes with this change. The pre-existing tests are unchanged and still pass. --- src/zircon/rtl/zircon_ip_tx_deparse.sv | 13 +- .../test_zircon_ip_tx_deparse.py | 132 ++++++++++++++++++ 2 files changed, 143 insertions(+), 2 deletions(-) diff --git a/src/zircon/rtl/zircon_ip_tx_deparse.sv b/src/zircon/rtl/zircon_ip_tx_deparse.sv index bd71d845..4171159b 100644 --- a/src/zircon/rtl/zircon_ip_tx_deparse.sv +++ b/src/zircon/rtl/zircon_ip_tx_deparse.sv @@ -225,6 +225,9 @@ logic [16:0] meta_cksum_1_reg = '0, meta_cksum_1_next; logic [16:0] meta_cksum_2_reg = '0, meta_cksum_2_next; logic [17:0] meta_cksum_3_reg = '0, meta_cksum_3_next; +logic [16:0] meta_l3_cksum_fold; +logic [16:0] meta_l4_cksum_fold; + assign s_axis_meta.tready = s_axis_meta_tready_reg; assign m_axis_pkt.tdata = m_axis_pkt_tdata_reg; @@ -250,6 +253,12 @@ always_comb begin meta_cksum_2_next = meta_cksum_2_reg; meta_cksum_3_next = meta_cksum_3_reg; + // fold end-around carries back into the checksums; the accumulators can + // grow beyond 17 bits, so a single fold can still produce a carry, which + // would be lost when the checksums are truncated to 16 bits + meta_l3_cksum_fold = meta_l3_cksum_reg[15:0] + 16'(meta_l3_cksum_reg[20:16]); + meta_l4_cksum_fold = meta_l4_cksum_reg[15:0] + 16'(meta_l4_cksum_reg[20:16]); + pkt_flag_next = pkt_flag_reg; pkt_l3_len_next = pkt_l3_len_reg; pkt_l4_len_next = pkt_l4_len_reg; @@ -492,8 +501,8 @@ always_comb begin data_valid_next = meta_valid_reg && !meta_empty; pkt_flag_next = meta_flag_reg; - pkt_l3_cksum_next = ~meta_l3_cksum_reg; - pkt_l4_cksum_next = ~21'(meta_l4_cksum_reg[15:0] + 16'(meta_l4_cksum_reg[20:16])); + pkt_l3_cksum_next = ~21'(meta_l3_cksum_fold[15:0] + {16'd0, meta_l3_cksum_fold[16]}); + pkt_l4_cksum_next = ~21'(meta_l4_cksum_fold[15:0] + {16'd0, meta_l4_cksum_fold[16]}); meta_ram_b_rd_addr = {meta_rd_slot_reg[0], 4'd0}; pkt_l4_len_next = meta_ram_b_rd_data[15:0]; diff --git a/src/zircon/tb/zircon_ip_tx_deparse/test_zircon_ip_tx_deparse.py b/src/zircon/tb/zircon_ip_tx_deparse/test_zircon_ip_tx_deparse.py index 98ba8b97..3c9f4736 100644 --- a/src/zircon/tb/zircon_ip_tx_deparse/test_zircon_ip_tx_deparse.py +++ b/src/zircon/tb/zircon_ip_tx_deparse/test_zircon_ip_tx_deparse.py @@ -381,3 +381,135 @@ def test_zircon_ip_tx_deparse(request): sim_build=sim_build, extra_env=extra_env, ) + + +@cocotb.test() +async def run_test_l4_cksum_endaround_carry(dut): + """L4 checksum with maximal header field values. + + The metadata L4 checksum accumulator is 21 bits wide. With maximal header + field values (all-ones addresses, ports, sequence and acknowledge numbers, + window and urgent pointer) the accumulated sum grows well past 17 bits, so + more than one end-around carry fold is required before the checksum is + truncated to 16 bits. The payload is selected to place the accumulated sum + at the boundary of the carry fold. + + Verify the generated TCP/UDP checksum matches the reference packet for + TCP and UDP over IPv4 and IPv6. + """ + + tb = TB(dut) + + await tb.reset() + + # two byte payloads selected so the accumulated checksum sum requires a + # second end-around carry fold + tcp_payload = b'\xae\xeb' + udp_payload = b'\xff\xdb' + + test_pkts = [] + + eth = Ether(src='5A:51:52:53:54:55', dst='DA:D1:D2:D3:D4:D5') + + l3hdrs = [] + + # IPv4 with maximal field values + ip = IP(src='255.255.255.255', dst='255.255.255.255', ttl=255, id=0xffff) + l3hdrs.append(ip) + + # IPv6 with maximal field values + ip6 = IPv6(src='ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', + dst='ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', hlim=255, fl=0xfffff) + l3hdrs.append(ip6) + + for l3hdr in l3hdrs: + + # TCP with maximal field values + tcp = TCP(sport=0xffff, dport=0xffff, seq=0xffffffff, ack=0xffffffff, + window=0xffff, urgptr=0xffff, flags=0xff) + test_pkts.append(eth / l3hdr / tcp / tcp_payload) + + # UDP with maximal field values + udp = UDP(sport=0xffff, dport=0xffff) + test_pkts.append(eth / l3hdr / udp / udp_payload) + + for pkt in test_pkts: + tb.log.info("Packet: %r", pkt) + + pkt_b = pkt.build() + + payload = bytes(pkt[TCP].payload if TCP in pkt else pkt[UDP].payload) + + # assemble metadata + flags = ParserFlags.FLG_EN + meta = bytearray(8*16) + + eth_type = pkt[Ether].type + + # Ethernet header + meta[24:30] = scapy.utils.mac2str(pkt[Ether].dst) + meta[32:38] = scapy.utils.mac2str(pkt[Ether].src) + struct.pack_into('>H', meta, 38, eth_type) + + # IPv4 + if IP in pkt: + flags |= ParserFlags.FLG_IPV4 + + meta[64:68] = scapy.utils.inet_aton(pkt[IP].dst) + meta[80:84] = scapy.utils.inet_aton(pkt[IP].src) + + struct.pack_into('BB', meta, 56, pkt[IP].proto, pkt[IP].ttl) + struct.pack_into('