Skip to content

net/packet: don't panic in Payload when dataofs is past length - #21232

Merged
jentfoo merged 1 commit into
tailscale:mainfrom
Dev-next-gen:fix/packet-payload-bounds
Sep 15, 2026
Merged

jentfoo merged 1 commit into
tailscale:mainfrom
Dev-next-gen:fix/packet-payload-bounds

Conversation

@Dev-next-gen

Copy link
Copy Markdown
Contributor

Fixes #21231

(*Parsed).Payload says it returns nothing rather than crashing on a truncated packet, but its guard compares both length and dataofs against len(b) while the slice it returns is b[dataofs:length]. What actually has to hold is dataofs <= length, and those two are independent: length is the IPv4 total length header field, dataofs is derived from the sub-protocol header, and decode4 never checks that the declared total length covers the transport header.

A 28-byte IPv4/UDP packet declaring a total length of 20 decodes to length=20 dataofs=28 len(b)=28, and Payload then evaluates b[28:20]:

panic: runtime error: slice bounds out of range [28:20]

tailscale.com/net/packet.(*Parsed).Payload(...)
	net/packet/packet.go:472

ICMPv4 and TCP get there the same way, with 24- and 40-byte packets. The test I added covers all three; it panics on main and passes with the extra condition.

I found this by fuzzing Decode and then calling the accessors on the parsed result. The net/packet fuzz target in #21123 only calls Decode, which is why it did not surface.

I went looking for a caller that can be driven into this from the network and could not prove one. filterPacketInboundFromWireGuard reaches Payload through isSelfDisco for every inbound UDP packet, but wireguard-go truncates decrypted packets to the declared IP length first, so on that path dataofs > length implies dataofs > len(b) and the existing guard already catches it. So I am sending this as a robustness fix for an exported parser whose documented contract is that it will not crash, not as a fix for a reachable crash. If you would rather have the invariant enforced in decode4 (rejecting the packet as Unknown when the declared length does not cover the sub-protocol header) than repaired at the accessor, I am happy to redo it that way.

While I was there I also noticed that (*Parsed).Transport can panic the same way, because decode4 assigns q.subofs before validating it against q.length, so the rejection path leaves subofs > len(b) with IPVersion still set. I left that out of this change to keep it to one thing; tell me if you want it here or in its own issue.

AI tools used

@jentfoo jentfoo self-assigned this Sep 14, 2026

@jentfoo jentfoo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the report and PR @Dev-next-gen

Now that #21123 has been merged, can you please change (or add to) your testing to validate through fuzz targets which includes both valid and invalid inputs as the seeds. Also include adding the new fuzz target to fuzz/oss-fuzz.sh.

The goal is for these protocol issues to be validated through fuzzing.

(FWIW, the only reason this was not included is that I intended to include fuzz targets that needed code changes in a separate PR like this, you just beat me to it)

Payload guards a truncated packet by comparing both length and dataofs
against len(b), but the slice it returns is b[dataofs:length], so what
actually has to hold is dataofs <= length. Those are independent:
length comes from the IPv4 total length header field, while dataofs is
derived from the sub-protocol header, and decode4 never checks that the
declared total length covers the transport header.

A 28-byte IPv4/UDP packet declaring a total length of 20 decodes to
length=20, dataofs=28, len(b)=28, and Payload then evaluates b[28:20].
ICMPv4 and TCP reach the same state with 24- and 40-byte packets.

I found this by fuzzing Decode and then calling the accessors on the
result. I did not find a caller that can be driven into it from the
network: wireguard-go truncates decrypted packets to the declared IP
length, so on the inbound path dataofs > length implies dataofs >
len(b) and the existing guard already catches it.

Add a FuzzParsedPayload target that decodes and then calls Payload,
seeded with valid IPv4/IPv6 packets and with invalid ones, including
the three short total length packets above, and build it in
fuzz/oss-fuzz.sh.

Fixes tailscale#21231

Change-Id: Ie5d2100464b79750626b1bfefbe4020c4a42ca91
Signed-off-by: leoca <[email protected]>
@Dev-next-gen
Dev-next-gen force-pushed the fix/packet-payload-bounds branch from cbd95ea to 48d54e2 Compare September 14, 2026 17:19
@Dev-next-gen

Copy link
Copy Markdown
Contributor Author

Thanks @jentfoo. Now that #21123 is in, I rebased onto main and moved the testing into a fuzz target.

FuzzParsedPayload in net/packet/fuzz_test.go decodes the input and then calls Payload. Its seeds are valid IPv4 UDP/TCP/ICMP and IPv6 UDP packets, plus invalid ones: an empty buffer, a declared IPv4 length longer than the buffer, a TCP data offset past the end, and the three packets from the issue whose total length of 20 stops before the ICMPv4, UDP and TCP headers. Those three replace the table test I had added to packet_test.go, which I removed. The target is also registered in fuzz/oss-fuzz.sh as packet_parsed_payload.

With the change reverted, go test ./net/packet -run FuzzParsedPayload fails on the seed corpus with slice bounds out of range [24:20]. With it, the package tests pass and -fuzz FuzzParsedPayload -fuzztime 60s ran about 21M executions without a failure. I did not run the OSS-Fuzz build itself (compile_native_go_fuzzer_v2 needs their base-builder image); I only checked that the script still parses.

I kept the target to Payload. Calling Transport from it would crash right away on the subofs issue I mentioned in the description, which this PR doesn't fix.

@jentfoo
jentfoo merged commit 8b5a870 into tailscale:main Sep 15, 2026
62 of 64 checks passed
@jentfoo

jentfoo commented Sep 15, 2026

Copy link
Copy Markdown
Member

Thank you @Dev-next-gen !

@Dev-next-gen

Copy link
Copy Markdown
Contributor Author

@jentfoo With pleasure, I use Tailscale every day!

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.

net/packet: Payload panics when the IPv4 total length stops before the transport header

2 participants