Skip to content

bounds-check serialized state and rule indices in ATNDeserializer - #4970

Open
abdul7056 wants to merge 1 commit into
antlr:devfrom
abdul7056:atn-deserialize-bounds
Open

abdul7056 wants to merge 1 commit into
antlr:devfrom
abdul7056:atn-deserialize-bounds

Conversation

@abdul7056

Copy link
Copy Markdown

The C++ ATN deserializer indexes its state and rule tables with numbers read straight from the serialized data, using unchecked vector operator[]:

  • a malformed ATN naming a non-greedy state, edge target, or rule index that does not exist reads an ATNState* out of bounds, then dereferences and writes through it (->nonGreedy = true, ->isLeftRecursiveRule = true, ...)
  • ruleToStopState[state->ruleIndex] is an out-of-bounds heap write when a state carries a rule index past the rule count
  • the Java runtime throws IndexOutOfBounds on the same input; only the C++ port has undefined behavior at these sites

Switched those accesses to at() so a corrupt ATN throws std::out_of_range before any out-of-bounds access. Valid grammars deserialize unchanged. Added a gtest under runtime/tests that reproduces the fault (it segfaults under AddressSanitizer against the current code and passes after the change).

Signed-off-by: obsidian_nomad <abdul@sproutxp.com>
@kaby76

kaby76 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Do you have an example of the Antlr4 Tool generating a corrupt ATN?

@abdul7056

Copy link
Copy Markdown
Author

No, the tool itself emits well-formed ATNs, so this isn't about a corrupt blob coming out of the ANTLR tool. The concern is the deserializer treating the serialized stream as trusted input: state, rule, and edge indices are read straight from the bytes and used as unchecked operator[] offsets, so a corrupted or hand-crafted ATN (anything not produced by the tool) drives an out-of-bounds read that then gets dereferenced and written through. Java stays memory-safe on the same bytes because List.get/array access throw IndexOutOfBounds; only the C++ port has UB there.

It's straightforward to trigger with a minimal malformed blob. For example {4,0,0,0,1,100} (version 4, one non-greedy state numbered 100 against an empty states vector) reaches downCast<DecisionState*>(atn->states[100]) and segfaults under AddressSanitizer at ATNDeserializer.cpp:321; a second case with an edge target of 9 hits the same pattern in edgeFactory. The gtest in the PR constructs those blobs directly, so it fails under ASan against the current code and passes after switching those 17 sites to at(). Happy to fold that repro into the PR description if that's useful.

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