Skip to content

EncodedS2ShapeIndex: OOB access via unchecked shape_id in cell decode #676

Description

@sushant-me

Out-of-bounds access in EncodedS2ShapeIndex::shape() via unchecked shape_id

Related to (but distinct from) #674. A malformed EncodedS2ShapeIndex can encode a
clipped shape whose shape_id >= num_shape_ids(), causing an OOB access on the
shapes_ vector during lazy shape decode.

Root cause

S2ShapeIndexCell::Decode() derives shape_id from the untrusted header
(shape_id_count >> 4, header >> 4, shape_delta) and only guards against int
overflow, never against num_shape_ids:

int64_t shape_id = 0;
for (...) {
  if (shape_id >= std::numeric_limits<int>::max()) return false; // overflow only
  shape_id += <untrusted delta>;       // can exceed num_shape_ids
  clipped->Init(shape_id, num_edges);
}

shape(shape_id) then does shapes_[shape_id] with the precondition
0 <= id < num_shape_ids(), but shape_id is out of range.

Reproduction

24-byte input (base64):

BAgACAAICQkAAAAAAAAAAAAAAAAAAABA

Init() returns true; index.shape(clipped.shape_id()) triggers:

stl_vector.h:1253: std::vector<EncodedS2ShapeIndex::AtomicShape>::operator[]:
    Assertion '__n < this->size()' failed.

Impact

OOB read (shapes_[id].load()) and OOB write (shapes_[id].compare_exchange_strong())
on a std::vector<std::atomic<S2Shape*>>. Under ASan this is a heap-buffer-overflow;
without the bounds assertion it is a silent OOB access.

Fix

In S2ShapeIndexCell::Decode(), reject any clipped shape with shape_id >= num_shape_ids
(return false). num_shape_ids is already a parameter, so the check is local.

Activity

  1. added 2 commits that reference this issue on Sep 15, 2026
    27d0fc0
    983afe1
  2. sushant-me commented on Oct 5, 2026

    @sushant-me
    Author

    Related framing: #686 documents the trusted byte stream assumption on EncodedS2ShapeIndex::Init — that a true return does not mean the encoding is sound, because cells are decoded lazily and a malformed one is not reached until later.

    The behaviour reported here is unchanged; the note only states the contract a caller is operating under when it hands the decoder an encoding that does not satisfy it. Filing it as documentation rather than a fix follows jmr's reading on #674.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions