Skip to content

feat: IoBounds — type untrusted-segment bounds as VortexException (ADR 0003 Phase E)#82

Merged
dfa1 merged 5 commits into
mainfrom
bounds-typing
Jun 20, 2026
Merged

feat: IoBounds — type untrusted-segment bounds as VortexException (ADR 0003 Phase E)#82
dfa1 merged 5 commits into
mainfrom
bounds-typing

Conversation

@dfa1

@dfa1 dfa1 commented Jun 20, 2026

Copy link
Copy Markdown
Owner

What

Extends ADR 0003 to a second axis: the type of exception thrown on malformed input, not just message sanitization. Parse-side offsets/lengths/counts from untrusted file bytes now surface as VortexException, never a raw JDK IndexOutOfBoundsException / ArithmeticException / NegativeArraySizeException.

Commits

  1. IoBounds helper (core) — slice / checkRange / toIntSize / checkCount. One pure primitive reachable by every layer (reader → core, and ProtoReader lives in core). Uses the current VortexException(String) ctor; migrates to the VortexError catalog when ADR 0003 Phase A lands. + 19 unit tests.
  2. Slice/size migration — routes the untrusted file-structure and decode sites through IoBounds: VortexReader, VortexHttpReader, Trailer, ScanIterator stats, FlatSegmentDecoder, PostscriptParser. FlatSegmentDecoder + ScanIterator stats were the genuinely unguarded paths — a crafted trailing fbLen leaked IndexOutOfBoundsException. Plus Math.toIntExactIoBounds.toIntSize in the Date/Time/Timestamp/Uuid extension decoders.
  3. Objects.checkIndex sweep — the ~14 hand-rolled getX(i) guards collapse onto the JDK built-in. Consumer-access carve-out: a caller's bad accessor index correctly stays IndexOutOfBoundsException (cf. List.get), distinct from untrusted-parse offsets.
  4. FlatSegmentBoundsSecurityTest — crafted segments (tiny, oversize fbLen, negative fbLen, buffer descriptor past segment end) each assert VortexException.

Scope notes

  • reader.array .limited() re-slices left raw — offset 0, rows < length, bounded by construction, not untrusted input.
  • Deferred follow-ups (ADR 0003 Phase E): checkstyle ban on raw .asSlice(, checkCount guards on new T[(int)n] alloc sites.

Verify

core 242 + reader 683 green; checkstyle + javadoc clean. Existing MalformedTrailer/Footer/ZipBomb security tests unchanged (no behaviour regression on already-validated paths).

🤖 Generated with Claude Code

dfa1 and others added 5 commits June 20, 2026 08:32
…3 Phase E)

Parse-side offsets/lengths/counts from untrusted file bytes must surface as
VortexException, not raw IndexOutOfBoundsException / ArithmeticException /
NegativeArraySizeException. IoBounds wraps the four shapes: slice/checkRange
(asSlice bounds), toIntSize (2 GB ByteBuffer/array cap, replaces
Math.toIntExact), checkCount (new T[n] alloc guard).

Uses the current VortexException(String) constructor — bounds messages carry
only numeric offsets, no attacker strings — and migrates to the VortexError
catalog when ADR 0003 Phase A lands. Extends ADR 0003 to cover the exception
*type* axis alongside message sanitization; records why a static helper beats
the PR #27 BoundedSegment wrapper (no new type on the zero-copy hot path).

Call-site migration + the Objects.checkIndex consumer-access sweep + the
checkstyle ban on raw asSlice follow in subsequent commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… Phase E)

Migrate the file-structure and decode-side bounds operations that take
offsets/lengths from parsed file bytes so a malformed file throws
VortexException, not a raw JDK exception:

- asSlice → IoBounds.slice: VortexReader (trailer/postscript/stats/segment),
  VortexHttpReader, Trailer magic, ScanIterator stats, FlatSegmentDecoder
  buffer descriptors, PostscriptParser blob slice.
- FlatSegmentDecoder and ScanIterator stats now guard the trailing u32 fbLen
  read (checkRange) and segLen narrowing (toIntSize) — both were previously
  unguarded; a crafted fbLen leaked IndexOutOfBoundsException.
- Math.toIntExact(storage.length()) → IoBounds.toIntSize in the Date/Time/
  Timestamp/Uuid extension decoders (ArithmeticException → VortexException
  on a > 2 GB declared length).

reader.array .limited() re-slices are left raw: offset 0, rows < length,
bounded by construction — not untrusted input (ADR 0003 Phase E item 5).

Existing MalformedTrailer/Footer/ZipBomb security tests stay green (no
behaviour change on already-validated paths). Follow-ups: checkstyle ban on
raw asSlice, checkCount guards on new T[(int)n] alloc sites, and the
Objects.checkIndex sweep of consumer-access getters.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Index (ADR 0003 Phase E)

The ~14 consumer-access getters in the Lazy*/Generic array families and
ExtensionStorage hand-rolled the same bounds check:

    if (i < 0 || i >= length) {
        throw new IndexOutOfBoundsException("index " + i + " out of bounds for length " + length);
    }

Collapse each onto the JDK built-in Objects.checkIndex(i, length). This is the
consumer-access carve-out from ADR 0003 Phase E: a caller's bad accessor index
is consumer misuse and correctly stays IndexOutOfBoundsException (cf. List.get),
distinct from the untrusted-parse offsets that route through IoBounds and throw
VortexException. Objects.checkIndex is an @IntrinsicCandidate, so the JIT inlines
it to the same check — no regression on these scalar accessors.

12 files, 14 guards. reader 679 green; checkstyle + javadoc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…se E)

End-to-end coverage of the FlatSegmentDecoder hardening: drives the public
decode() with crafted segments and asserts VortexException, never a raw JDK
exception. Four cases:

- segment smaller than the trailing 4-byte length field
- declared fbLen larger than the segment (fbStart goes negative)
- negative fbLen (0xFFFFFFFF read as signed -1)
- a well-formed Array FlatBuffer whose single buffer descriptor claims a
  1 000 000-byte payload past the segment end (exercises IoBounds.slice in
  the buffer-collection loop)

Builds the Array FlatBuffer with FlatBufferBuilder so the buffer-descriptor
case reaches the real decode path. reader 683 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Phase E)

Three follow-ups from PR review:

- ScanIterator.readFlatStats: degrade to ArrayStats.empty() on a malformed
  stats segment instead of throwing VortexException, matching
  VortexReader.readFlatStats. Stats are an optional zone-map pruning
  optimization — a corrupt stats segment must not abort the scan. Also adds
  the missing segIdx bounds guard the other reader already had.
- FlatSegmentDecoder: route the buffer-count allocation through
  IoBounds.checkCount before new MemorySegment[numBuffers].
- Date/Time/Timestamp/Uuid extension decoders: the decodeAll loops counted
  with a long index over an int bound (n); narrow to int i.

reader 705 green; checkstyle + javadoc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dfa1 dfa1 merged commit 2ca96f8 into main Jun 20, 2026
6 checks passed
@dfa1 dfa1 deleted the bounds-typing branch June 20, 2026 11:44
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.

1 participant