Skip to content

Fix out-of-bounds read in GreLayer optional-field getters on truncated packets - #2189

Open
94xhn wants to merge 1 commit into
seladb:devfrom
94xhn:fix/gre-layer-isdatavalid-oob-read
Open

94xhn wants to merge 1 commit into
seladb:devfrom
94xhn:fix/gre-layer-isdatavalid-oob-read

Conversation

@94xhn

@94xhn 94xhn commented Jul 18, 2026

Copy link
Copy Markdown

Problem

GREv0Layer::isDataValid() and GREv1Layer::isDataValid() only check the
data length against the fixed base header size (sizeof(gre_basic_header)
for v0, sizeof(gre1_header) for v1), regardless of which optional fields
(checksum/routing, key, sequence number, ack sequence number) the packet's
own bit flags claim are present.

GreLayer::getFieldValue() computes each optional field's offset purely
from those bit flags, with no check against the actual buffer length. So a
truncated GRE packet that has an optional bit set but not enough trailing
bytes causes getChecksum() / getKey() / getSequenceNumber() /
getAcknowledgmentNum() / getOffset() to read past the end of the
buffer.

This is reachable through ordinary packet parsing, not just direct API
misuse: IPv4Layer::parseNextLayer() dispatches protocol-47 payloads into
tryConstructNextLayerWithFallback<GREv0Layer/GREv1Layer, PayloadLayer>,
which gates construction solely on isDataValid(). A short/truncated
capture (or a crafted packet) is enough to get a GreLayer constructed
over a buffer too small for the fields it claims to contain, and the next
call to one of the getters above reads out of bounds.

I verified this empirically (VirtualAlloc guard page + AddVectoredExceptionHandler,
since ASan isn't available in this toolchain) by building the real,
unmodified Packet++ library and:

  • confirming GREv0Layer::isDataValid() accepts a 4-byte buffer with
    checksumBit set, and that the subsequent getChecksum()/getKey()
    call reads past the end of the allocation
  • confirming GREv1Layer::isDataValid() accepts an 8-byte buffer with
    keyBit and sequenceNumBit both set even though the sequence-number
    field it computes sits one byte past the 8-byte allocation
  • building a truncated IPv4-in-GRE packet and confirming the real
    IPv4Layer::parseNextLayer()tryConstructNextLayerWithFallback
    chain constructs an (unsafe) GREv0Layer instead of falling back to
    PayloadLayer

Fix

Both isDataValid() implementations now sum the required buffer length
from the same optional-field bits that GreLayer::getHeaderLen() already
uses, so a truncated packet is correctly rejected (falling back to
PayloadLayer during parsing) instead of allowing a GreLayer to be
constructed over an undersized buffer.

After the fix, the same harness confirms:

  • GREv0Layer::isDataValid() now rejects the 4-byte/checksumBit-set case
  • GREv1Layer::isDataValid() now rejects the 8-byte/keyBit+sequenceNumBit-set case
  • the truncated IPv4-in-GRE packet no longer constructs a GREv0Layer (falls back to PayloadLayer)
  • well-formed GRE packets (v0 and v1, with every optional field actually
    present at its correct offset) still parse correctly and all getters
    still return the correct values, so this doesn't regress the normal
    parsing path

Only Packet++/header/GreLayer.h is touched; no behavior changes for
correctly-sized packets.

…d packets

GREv0Layer::isDataValid() and GREv1Layer::isDataValid() only checked the
data length against the fixed base header size (sizeof(gre_basic_header)
for v0, sizeof(gre1_header) for v1), regardless of which optional fields
(checksum/routing, key, sequence number, ack sequence number) the packet's
bit flags claim are present.

GreLayer::getFieldValue() computes each optional field's offset purely
from those bit flags with no bounds check against the actual buffer
length, so a truncated GRE packet with an optional bit set but not enough
trailing bytes causes getChecksum()/getKey()/getSequenceNumber()/
getAcknowledgmentNum()/getOffset() to read past the end of the buffer.

This is reachable via ordinary packet parsing: IPv4Layer::parseNextLayer()
dispatches protocol-47 payloads into
tryConstructNextLayerWithFallback<GREv0Layer/GREv1Layer, PayloadLayer>,
which only gates construction on isDataValid(), so a short/truncated
capture (or a crafted packet) is enough to trigger the out-of-bounds read
once a caller reads one of the optional fields.

Fix both isDataValid() implementations to sum the required length from
the same optional-field bits that GreLayer::getHeaderLen() already uses,
so truncated packets are rejected up front (falling back to PayloadLayer)
instead of allowing a GreLayer to be constructed over a buffer too short
for the fields it claims to contain.
@94xhn
94xhn requested a review from seladb as a code owner July 18, 2026 18:47
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.16667% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.66%. Comparing base (3aae85b) to head (87b061c).
⚠️ Report is 4 commits behind head on dev.

Files with missing lines Patch % Lines
Packet++/header/GreLayer.h 79.16% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #2189      +/-   ##
==========================================
- Coverage   82.66%   82.66%   -0.01%     
==========================================
  Files         332      332              
  Lines       60155    60169      +14     
  Branches    12768    12547     -221     
==========================================
+ Hits        49727    49736       +9     
- Misses       9019     9023       +4     
- Partials     1409     1410       +1     
Flag Coverage Δ
23.11.6 7.26% <0.00%> (-0.04%) ⬇️
24.11.5 7.28% <0.00%> (-0.02%) ⬇️
25.11.1 7.26% <0.00%> (-0.04%) ⬇️
alpine320 76.86% <53.33%> (-0.01%) ⬇️
fedora42 76.44% <50.00%> (-0.02%) ⬇️
macos-15 82.26% <54.16%> (-0.02%) ⬇️
macos-26 82.26% <54.16%> (-0.02%) ⬇️
macos-26-intel 82.19% <54.16%> (-0.02%) ⬇️
mingw32 70.97% <50.00%> (-0.03%) ⬇️
mingw64 70.95% <50.00%> (+0.07%) ⬆️
npcap ?
rhel94 76.23% <50.00%> (-0.02%) ⬇️
ubuntu2204 76.27% <50.00%> (-0.03%) ⬇️
ubuntu2404 76.58% <53.33%> (-0.02%) ⬇️
ubuntu2604 76.53% <50.00%> (-0.02%) ⬇️
ubuntu2604-arm64 76.34% <50.00%> (-0.03%) ⬇️
ubuntu2604-icpx 59.05% <54.16%> (-0.03%) ⬇️
unittest 82.66% <79.16%> (-0.01%) ⬇️
windows-2022 85.80% <79.16%> (+0.10%) ⬆️
windows-2025 85.54% <79.16%> (+0.11%) ⬆️
winpcap 85.83% <79.16%> (+0.19%) ⬆️
xdp 52.90% <50.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

// end of the buffer
auto* header = reinterpret_cast<const gre_basic_header*>(data);
size_t requiredLen = sizeof(gre_basic_header);
if (header->checksumBit == 1 || header->routingBit == 1)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

nit: I think the checks with == 1 could just be: if header->checksumBit || header->routingBit etc.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Can we add tests for these fixes?

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