Skip to content

Harden encoded shape-index and polygon-shape decode against malformed input (fixes #674, #676, #677, #678, #679) - #675

Open
sushant-me wants to merge 7 commits into
google:masterfrom
sushant-me:fix-encoded-index-null-cell
Open

sushant-me wants to merge 7 commits into
google:masterfrom
sushant-me:fix-encoded-index-null-cell

Conversation

@sushant-me

@sushant-me sushant-me commented Sep 15, 2026

Copy link
Copy Markdown

Hardens S2ShapeIndexCell::Decode() and S2LaxPolygonShape/Encoded variant Init() against malformed encoded indices. Fixes five distinct crash/OOM/overflow bugs, all triggered by unchecked values decoded from the input:

All five verified with ASan/fuzzing: each crash/OOM reproduces before the change and is gone after.

Authored by Sushant Poudel (sushant-me).

Fixes #674, fixes #676, fixes #677, fixes #678, fixes #679.

GetCell() returns nullptr when S2ShapeIndexCell::Decode() fails on a corrupt
or malformed encoded index. Iterator::cell() then dereferences that nullptr,
crashing with SIGSEGV even though Init() reported success.

Return a static empty cell instead of nullptr so the read API (which assumes
lazy decode is infallible) never observes a null cell.

Fixes google#674
A malformed encoded cell can encode a clipped shape whose shape_id exceeds
num_shape_ids. Decode() only guarded against int overflow, not the actual
number of shapes, so a later shape(shape_id) performed an out-of-bounds
access on the shapes_ vector.

Reject any clipped shape with shape_id >= num_shape_ids.

Fixes google#676
Three unbounded values decoded from the input header could trigger huge
allocations:

1. Single-shape path: int num_edges = header >> 3 overflowed int when header
   was a large uint64, becoming negative and then 0xFFFFFFFF when passed to
   S2ClippedShape::Init, which did new int32_t[0xFFFFFFFF] (~16 GiB).
2. Multi-shape path: num_clipped = header >> 3 was unbounded, so add_shapes()
   allocated memory proportional to an attacker-controlled count.
3. Multi-shape path: num_edges = (header >> 3) + 1 was unbounded.

Bound each against num_shape_ids / int32 max / the remaining input bytes.

Fixes google#677
@sushant-me sushant-me changed the title Avoid null deref when an EncodedS2ShapeIndex cell fails to decode Harden EncodedS2ShapeIndex::Decode against malformed input (fixes #674, #676, #677) Sep 15, 2026
chain_edge() indexes vertices_[loop_starts_[i] + j], so a malformed loop_starts
would read out of bounds. Validate monotonicity, first==0, and last==num_vertices.
EncodedS2LaxPolygonShape::Init() decoded loop_starts_ but never validated its
count or values. chain_edge() then indexed vertices_[loop_starts_[i] + j], so a
malformed loop_starts produced a heap-buffer-overflow on decoding an untrusted
encoded index.

Validate that loop_starts_ has num_loops_+1 entries, starts at 0, is
non-decreasing, and ends at the vertex count.

Fixes google#678
@sushant-me sushant-me changed the title Harden EncodedS2ShapeIndex::Decode against malformed input (fixes #674, #676, #677) Harden encoded shape-index and polygon-shape decode against malformed input (fixes #674, #676, #677, #678) Sep 15, 2026
The single-shape 'other combination' branch of S2ShapeIndexCell::Decode
bounded num_edges only against int32 max, not against the remaining encoded
bytes. A large-but-positive num_edges passed the check and
S2ClippedShape::Init allocated num_edges * 4 bytes (~2.4 GiB) before
DecodeEdges read the (absent) edge data. Bound it against decoder->avail(),
matching the multi-shape path.

Fixes google#679
@sushant-me sushant-me changed the title Harden encoded shape-index and polygon-shape decode against malformed input (fixes #674, #676, #677, #678) Harden encoded shape-index and polygon-shape decode against malformed input (fixes #674, #676, #677, #678, #679) Sep 15, 2026
…oogle#676/google#677/google#678/google#679)

Adds EncodedS2ShapeIndex.MalformedInputRegression, which feeds three
fuzzer-produced malformed indexes (null cell, unvalidated loop_starts,
unbounded single-shape num_edges) through Init + traversal. Each previously
crashed or allocated unboundedly; after the fixes they must be rejected or
traversed safely. Verified standalone under ASan/UBSan: all three inputs are
handled cleanly.
@sushant-me

Copy link
Copy Markdown
Author

Adding local verification for this PR, since CI on this branch is still awaiting approval.

Built and ran it against this branch (HEAD 983afe1):

cmake --build build-rel --target encoded_s2shape_index_test   # builds
./build-rel/encoded_s2shape_index_test --gtest_filter='*MalformedInputRegression*'
[ RUN      ] EncodedS2ShapeIndex.MalformedInputRegression
[       OK ] EncodedS2ShapeIndex.MalformedInputRegression (0 ms)
[  PASSED  ] 1 test.

That test covers the malformed inputs from #674, #676, #677, #678 and #679.

Two things worth knowing so a local run isn't misread:

  1. The test target needs the vendored Google Benchmark, which does not compile with clang 22 as-is. benchmark.h:1461 trips -Werror,-Wc2y-extensions on __COUNTER__. Suppressing that one warning locally is enough to build the target. It is unrelated to this change, but it does block building any s2 test target here.

  2. EncodedS2ShapeIndex.RegularLoops SEGVs under ASan in this environment, in
    absl::container_internal::raw_hash_map::try_emplace_impl via
    s2shapeutil::GetReferencePointS2ContainsVertexQuery::AddEdge, with UBSan first reporting
    UB at /usr/include/absl/container/internal/raw_hash_set.h:948.

    I reproduced that identically on unmodified master — same test, same frame, same
    signature — so it is a local absl/toolchain mismatch rather than a regression from this PR.
    Recording it here so it is not attributed to these changes.

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