Fix hop payload bounds
unwrapPacket passes the full 2600-byte hopInfo buffer to DecodeHopPayload, but only the first 1300 bytes contain routing info. The remaining 1300 bytes are zero-padding used for XOR decryption.
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.
Found through differential fuzzing (bitcoinfuzz) where Core Lightning and rust-lightning rejected the malformed onion packet.
Summary of Changes
Hello @erickcestari, I'm Gemini Code Assist[^1]! 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
unwrapPacketfunction now explicitly limits thehopInfobuffer passed toDecodeHopPayloadtoroutingInfoLen(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 thatunwrapPacketcorrectly 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.gowere renamed to correct a typo, changing 'Relpay' to 'Replay' (e.g.,TestSphinxNodeRelpayis nowTestSphinxNodeReplay).
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 :thumbsup: and :thumbsdown: 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.
[^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.
Maybe we should also change DecodeHopPayload to enforce the correct size, rather than UINT16_MAX.
Maybe we should also change
DecodeHopPayloadto enforce the correct size, rather thanUINT16_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.
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?
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.
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.
@erickcestari So with the current solution, a malformed packet could still look into the bytes of the 32-byte HMAC, correct?
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?