Skip to content

zircon: fix checksum end-around carry folding in IP TX deparse - #38

Open
john-kearney wants to merge 1 commit into
fpganinja:masterfrom
john-kearney:zircon-tx-checksum-fold
Open

john-kearney wants to merge 1 commit into
fpganinja:masterfrom
john-kearney:zircon-tx-checksum-fold

Conversation

@john-kearney

@john-kearney john-kearney commented Sep 1, 2026 •

Copy link
Copy Markdown

Problem

zircon_ip_tx_deparse accumulates the L4 checksum into a 21-bit register
(meta_l4_cksum_reg). With maximal header field values (all-ones IPv4/IPv6
addresses, ports, TCP sequence/acknowledge numbers, window/urgent pointer —
all software-supplied TX metadata) the accumulated sum reaches ~0x9510F, far
beyond 17 bits.

At the start of packet generation the accumulator was folded back ONCE:

pkt_l4_cksum_next = ~21'(meta_l4_cksum_reg[15:0] + 16'(meta_l4_cksum_reg[20:16]));

and the checksum is truncated to 16 bits on output (TCP :877, UDP :902).
The single fold can itself produce a carry (e.g. S = 0x8FFFE -> T = 0x10006),
and that carry is silently dropped by the truncation, so the transmitted
checksum is wrong by exactly the dropped carry (off by one in every reachable
case).

Measured (all-ones TCP/IPv6 metadata, 2-byte payload 0xAEEB): sum S = 0x8FFFE,
DUT transmits checksum 0xFFF9 while the correct value is 0xFFF8. UDP/IPv6 with
payload 0xFFDB: S = 0x2FFFE, DUT transmits 0xFFFF, correct is 0xFFFE. The same
values reproduce for TCP/IPv4 and UDP/IPv4 (identical accumulated sums).

The existing tests never push the accumulator past 0x10000, so this was not
caught by CI. For dense/jumbo traffic the failure region is hit roughly once
per ~14k packets (silent, receiver drops the segment); flows with maximal
metadata field values can hit it deterministically.

The IPv4 header checksum capture had the same shape for its own accumulator
(pkt_l3_cksum_next = ~meta_l3_cksum_reg with a 21-bit register, truncated to
16 bits at output): fully-ingested records already fold it twice during
metadata ingest, but the fold was not repeated at capture time.

Change

Fold the end-around carry TWICE before complementing, for both the L3 and the
L4 checksums:

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_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]});

Two folds always suffice: the first fold of a 21-bit sum is at most
0x1001E, so the second fold can never carry again. The double fold is
idempotent for values that are already fully folded (the L3 accumulator after
complete 16-word ingest), so behavior for existing traffic is unchanged.

Test

Added run_test_l4_cksum_endaround_carry to tb/zircon_ip_tx_deparse: drives
TCP and UDP over IPv4 and IPv6 with maximal header field values and 2-byte
payloads chosen so the accumulated sum sits exactly at the carry-fold boundary
(S[15:0] = 0xFFFE with S >> 16 >= 1). Asserts the emitted checksum matches
the scapy reference packet. This test fails on the unfixed RTL (TCP checksum
0xFFF9 vs 0xFFF8) and passes with the fix; the pre-existing tests are
unchanged and still pass.

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.
@john-kearney

Copy link
Copy Markdown
Author

/gha run Regression Tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants