Skip to content

Conversation

@erickcestari
Copy link

@erickcestari erickcestari commented Dec 11, 2025

The unwrapPacket function passes the full hopInfo buffer to DecodeHopPayload for parsing. However, this buffer contains both the actual routing information and zero-padding used for XOR decryption.
The routing info occupies the first portion of the buffer (determined by the routingInfoLen parameter), while the remainder is padding.

When a malformed packet contains an oversized payload length field, the decoder could read beyond the routing info boundary into the padding area.

This PR constrains DecodeHopPayload to read only from the routing info portion by slicing hopInfo to routingInfoLen bytes. This fix is compatible with both:

  • HTLC onion packets (update_add_htlc): 1300-byte routing info
  • Onion messages: variable size up to ~32KB routing info

The boundary is determined dynamically by the routingInfoLen parameter passed to unwrapPacket, so the fix works correctly regardless of the onion packet type being processed.

A malformed packet that attempts to read beyond its routing info boundary will now fail with an EOF error during payload decoding, rather than silently reading from the padding area.

Found through differential fuzzing (bitcoinfuzz) where Core Lightning and rust-lightning rejected the malformed onion packet.

@gemini-code-assist
Copy link

Summary of Changes

Hello @erickcestari, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a critical fix to how onion packet payloads are processed, specifically preventing DecodeHopPayload from reading beyond the intended routing information boundary. Previously, a malformed packet could exploit this by specifying an oversized payload, causing the decoder to read into the zero-padding area. The change ensures that only the valid routing information portion of the hopInfo buffer is processed, enhancing parsing consistency and robustness against malformed inputs. Additionally, new tests have been added to validate this boundary enforcement, alongside minor test function name corrections.

Highlights

  • Payload Boundary Enforcement: The unwrapPacket function now explicitly limits the hopInfo buffer passed to DecodeHopPayload to routingInfoLen (1300 bytes). This prevents the decoder from reading into the zero-padding area of the buffer, addressing a potential parsing inconsistency with malformed packets.
  • New Test Case for Malformed Packets: A new test, TestUnwrapPacketBeyondRoutingInfoBoundary, has been added to verify that unwrapPacket correctly handles malformed onion packets where the payload size attempts to exceed the routing information boundary, ensuring it fails as expected.
  • Test Function Renames: Several test functions in sphinx_test.go were renamed to correct a typo, changing 'Relpay' to 'Replay' (e.g., TestSphinxNodeRelpay is now TestSphinxNodeReplay).
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request addresses a potential vulnerability in unwrapPacket where a malformed packet could cause the decoder to read beyond the intended routing information boundary. The fix correctly restricts the data passed to DecodeHopPayload by slicing the buffer to the appropriate length. This is a solid and important correction. The addition of a specific test case, TestUnwrapPacketBeyondRoutingInfoBoundary, is excellent as it directly verifies the fix against the described vulnerability. I've also noted the correction of typos in other test function names. My review includes a couple of minor suggestions for the new test code to align with the repository's style guide concerning line length.

@erickcestari erickcestari force-pushed the fix-hop-payload-bounds branch from a8eb498 to 3fad506 Compare December 11, 2025 11:53
@morehouse
Copy link

Maybe we should also change DecodeHopPayload to enforce the correct size, rather than UINT16_MAX.

@erickcestari erickcestari force-pushed the fix-hop-payload-bounds branch from 3fad506 to ace6bdb Compare December 15, 2025 18:47
@erickcestari
Copy link
Author

Maybe we should also change DecodeHopPayload to enforce the correct size, rather than UINT16_MAX.

Updated! DecodeHopPayload now validates that the payload size does not exceed MaxHopPayloadSize (1265 bytes) rather than allowing sizes up to UINT16_MAX.

I have created a new constant MaxHopPayloadSize which is defined as MaxRoutingPayloadSize - BigSizeLenMaxBytes - HMACSize, which accounts for the 3-byte BigSize length prefix and 32-byte HMAC within the 1300-byte routing info boundary. If the encoded payload size exceeds this limit, DecodeHopPayload returns ErrMaxHopPayloadSizeExceeded.

@Roasbeef Roasbeef requested a review from gijswijs December 18, 2025 00:27
@Roasbeef
Copy link
Member

A malformed packet with a large payload size field can cause the decoder to read beyond the routing info into the padding area. This creates parsing inconsistencies with other implementations that enforce the 1300-byte boundary.

IIUC it still results in the malformed packet being rejected? Or which inconsistencies are you referring to here?

Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

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

LGTM 📻

// instructions.
hopPayload, err := DecodeHopPayload(
bytes.NewReader(hopInfo), tlvPayloadOnly,
bytes.NewReader(hopInfo[:routingInfoLen]), tlvPayloadOnly,
Copy link
Member

Choose a reason for hiding this comment

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

👍

We should restrict to the actual length of the payload, rather than trusting the sender to accurately encode the length within the payload.

The MAC check of the next hop should fail, but then the HTLC fails one hop later than it should actually.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure if I follow. That would require to already peak bytes from the payload, which is what is happening in DecodeHopPayload. Are you suggesting to lift that logic out of that func?

Copy link
Member

Choose a reason for hiding this comment

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

What I mean was that before we assumed the length encoded and the actual length of the packet were the same thing. This PR fixes that.

Copy link
Collaborator

@gijswijs gijswijs left a comment

Choose a reason for hiding this comment

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

sphinx_test_diff.txt
Onion messaging is throwing a spanner in the works. Onion messages allow for all kinds of payload sizes, also way bigger than 1300 bytes. As such this fix will not work. (See also my other comments for more details) I've created a diff file that I'll share here that updates one of the unit tests to make a huge payload that shouldn't err out but currently does because of the added check.

@gijswijs gijswijs self-requested a review December 18, 2025 12:52
// instructions.
hopPayload, err := DecodeHopPayload(
bytes.NewReader(hopInfo), tlvPayloadOnly,
bytes.NewReader(hopInfo[:routingInfoLen]), tlvPayloadOnly,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure if I follow. That would require to already peak bytes from the payload, which is what is happening in DecodeHopPayload. Are you suggesting to lift that logic out of that func?

@erickcestari
Copy link
Author

erickcestari commented Dec 18, 2025

IIUC it still results in the malformed packet being rejected? Or which inconsistencies are you referring to here?

As you said, the packet would be forwarded to the next hop, where it would be rejected as invalid due to its invalid HMAC. This assumes that the packet has the required TLVs to be considered a forwarded packet. If it's a final-hop payload, it would fail locally because of the non-zero HMAC.

@erickcestari erickcestari force-pushed the fix-hop-payload-bounds branch from ace6bdb to cac3ec8 Compare December 18, 2025 14:14
The unwrapPacket function passes the full hopInfo buffer to
DecodeHopPayload for parsing. However, this buffer contains both the
actual routing information and zero-padding used for XOR decryption.
The routing info occupies the first portion of the buffer (determined
by the routingInfoLen parameter), while the remainder is padding.

When a malformed packet contains an oversized payload length field,
the decoder could read beyond the routing info boundary into the
padding area. This creates parsing inconsistencies with other
implementations that correctly enforce size boundaries.

This commit constrains DecodeHopPayload to read only from the routing
info portion by slicing hopInfo to routingInfoLen bytes. This fix is
compatible with both:

  - HTLC onion packets (update_add_htlc): 1300-byte routing info
  - Onion messages: variable size up to ~32KB routing info

The boundary is determined dynamically by the routingInfoLen parameter
passed to unwrapPacket, so the fix works correctly regardless of the
onion packet type being processed.

A malformed packet that attempts to read beyond its routing info
boundary will now fail with an EOF error during payload decoding,
rather than silently reading from the padding area.
Rename TestSphinxNodeRelpay* to TestSphinxNodeReplay*.
@erickcestari erickcestari force-pushed the fix-hop-payload-bounds branch from cac3ec8 to 93404fe Compare December 18, 2025 14:42
@gijswijs
Copy link
Collaborator

The fix doesn't require peeking or changing DecodeHopPayload. It simply limits the input reader:

Yeah, I understood that. My comment was on @Roasbeef earlier comment. Sorry for the confusion.

@gijswijs
Copy link
Collaborator

@erickcestari So with the current solution, a malformed packet could still look into the bytes of the 32-byte HMAC, correct?

@erickcestari
Copy link
Author

The fix doesn't require peeking or changing DecodeHopPayload. It simply limits the input reader:

Yeah, I understood that. My comment was on @Roasbeef earlier comment. Sorry for the confusion.

Yes, I only saw it after I commented 😅.

@erickcestari So with the current solution, a malformed packet could still look into the bytes of the 32-byte HMAC, correct?

No. If a malformed payload tries to read into the HMAC area, the subsequent 32-byte HMAC read will fail with io.ErrUnexpectedEOF due to insufficient remaining bytes.

For example, with a 1300-byte boundary: if a payload claims 1280 bytes, after reading the 3-byte length prefix + 1280-byte payload, only 17 bytes remain. Not enough for the 32-byte HMAC read.

Am I missing something?

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.

4 participants