Skip to content

fix: harden vortex.zoned metadata parsing against malformed input; decimal sum fallback (#197)#224

Merged
dfa1 merged 1 commit into
mainfrom
fix/zoned-proto-bounds
Jul 7, 2026
Merged

fix: harden vortex.zoned metadata parsing against malformed input; decimal sum fallback (#197)#224
dfa1 merged 1 commit into
mainfrom
fix/zoned-proto-bounds

Conversation

@dfa1

@dfa1 dfa1 commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Post-merge review follow-up to #197. The hand-rolled protobuf cursor added there had two defects:

  1. Crash on untrusted input (ADR 0003). The bounds guards used pos + len > bound, which integer-overflows when a malformed varint length is near Long.MAX_VALUE — the guard passes and the code then does new byte[(int) len] (NegativeArraySizeException) or advances pos negative (IndexOutOfBoundsException). Empirically confirmed by the reviewer against crafted metadata. Rewritten overflow-safe as len < 0 || len > bound - pos (invariant pos <= bound holds, so bound - pos never overflows) across readAggregateSpecId, readString, and advance/skipField.

  2. Latent wrong answer for decimal columns. Rust's zoned schema emits a sum column whenever Sum::return_dtype is Some — which is Some(Decimal) for a decimal column — but our statDtype(SUM, decimal) returns null, so we silently dropped the field and the positional decode read null_count out of the sum buffer. Now a null-resolving mapped aggregate that Rust would still emit (decimal sum) makes the whole column fall back to per-chunk stats (correct, unpruned); only aggregates Rust also omits (nan_count on non-float, min/max on unsupported types) are skipped, via a new rustAlsoOmits helper. Drop-vs-keep enumeration verified against vortex-layout/src/layouts/zoned/writer.rs + the aggregate return_dtype/state_dtype impls.

Malformed-metadata contract: null → per-chunk fallback (consistent with the existing bailsOn* cases and the decodeZoneTable caller), never a raw JDK exception.

Validation: ZonedStatsSchemaTest 26 (new MalformedAggregateMetadata group covering truncated varint, oversized string/skip length, unknown wire type, message past end; plus decimal-sum fallback); reader 1150 green; RustWritesJavaReadsIntegrationTest 13/13 (zone-map ground truth still green). docs/explanation.md zoned row updated.

Refs #197

🤖 Generated with Claude Code

…cimal sum fallback (#197)

The aggregate-spec zone-map decoder added in #197 (commit 9e20edc) had two
post-merge review findings.

Overflow class: ProtoCursor guarded reads with `pos + len > bound`. A varint
length up to Long.MAX_VALUE overflows `pos + len` to negative, so the `> bound`
check is false and the guard passes — then `new byte[(int) len]` throws
NegativeArraySizeException, or `pos` advances negative and the next segment read
throws IndexOutOfBoundsException. This is untrusted file metadata (ADR 0003), so
it must never surface a raw JDK exception. Every length/offset check
(readAggregateSpecId message descent, readString, advance/skipField) is rewritten
overflow-safe: the invariant pos <= bound holds throughout, so `bound - pos >= 0`
never overflows and the guard becomes `len < 0 || len > bound - pos`. Malformed
metadata now falls back to per-chunk stats.

Decimal divergence: Rust's default_zoned_aggregate_fns (vortex-layout
zoned/writer.rs) emits a `sum` column whenever Sum.return_dtype is Some — which is
Some(Decimal(precision + 10)) for a Decimal column (sum/mod.rs) — while our
sumDtype returns null for Decimal. Dropping that field with `continue` reconstructs
{max,min,null_count} for an encoded {max,min,sum,null_count} table, so the
positional decode reads null_count out of the sum buffer. The reconstructor now
distinguishes "Rust also omits this" (nan_count over non-float per nan_count/mod.rs,
min/max over an unsupported column per min_max minmax_supported_dtype — safe skip)
from "Rust keeps a field we cannot map" (decimal sum, bounded_max/min nested state
— return null so the whole column falls back to correct-but-unpruned per-chunk
stats).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dfa1 dfa1 force-pushed the fix/zoned-proto-bounds branch from febfcc1 to a70b1e2 Compare July 7, 2026 06:05
@dfa1 dfa1 merged commit d5684cf into main Jul 7, 2026
6 checks passed
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