fix: harden vortex.zoned metadata parsing against malformed input; decimal sum fallback (#197)#224
Merged
Merged
Conversation
…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>
febfcc1 to
a70b1e2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Post-merge review follow-up to #197. The hand-rolled protobuf cursor added there had two defects:
Crash on untrusted input (ADR 0003). The bounds guards used
pos + len > bound, which integer-overflows when a malformed varint length is nearLong.MAX_VALUE— the guard passes and the code then doesnew byte[(int) len](NegativeArraySizeException) or advancesposnegative (IndexOutOfBoundsException). Empirically confirmed by the reviewer against crafted metadata. Rewritten overflow-safe aslen < 0 || len > bound - pos(invariantpos <= boundholds, sobound - posnever overflows) acrossreadAggregateSpecId,readString, andadvance/skipField.Latent wrong answer for decimal columns. Rust's zoned schema emits a
sumcolumn wheneverSum::return_dtypeisSome— which isSome(Decimal)for a decimal column — but ourstatDtype(SUM, decimal)returns null, so we silently dropped the field and the positional decode readnull_countout of thesumbuffer. Now anull-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 newrustAlsoOmitshelper. Drop-vs-keep enumeration verified againstvortex-layout/src/layouts/zoned/writer.rs+ the aggregatereturn_dtype/state_dtypeimpls.Malformed-metadata contract: null → per-chunk fallback (consistent with the existing
bailsOn*cases and thedecodeZoneTablecaller), never a raw JDK exception.Validation:
ZonedStatsSchemaTest26 (newMalformedAggregateMetadatagroup covering truncated varint, oversized string/skip length, unknown wire type, message past end; plus decimal-sum fallback); reader 1150 green;RustWritesJavaReadsIntegrationTest13/13 (zone-map ground truth still green). docs/explanation.md zoned row updated.Refs #197
🤖 Generated with Claude Code