Skip to content

feat(core): add safe decoding helpers for Parquet footers - #423

Open
prudhvimaharishi wants to merge 2 commits into
GoogleCloudPlatform:mainfrom
prudhvimaharishi:parquet_footer_decoding
Open

prudhvimaharishi wants to merge 2 commits into
GoogleCloudPlatform:mainfrom
prudhvimaharishi:parquet_footer_decoding

Conversation

@prudhvimaharishi

@prudhvimaharishi prudhvimaharishi commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Type of Change

  • feat: A new feature
  • fix: A bug fix
  • docs: Documentation only changes
  • style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
  • refactor: A code change that neither fixes a bug nor adds a feature
  • perf: A code change that improves performance
  • test: Adding missing tests or correcting existing tests
  • chore: Changes to the build process or auxiliary tools and libraries such as documentation generation

Description

What?

Adds three package-private helpers for decoding Parquet footers safely:

  • BoundedCompactProtocol caps every Thrift string, binary value, and container at the message length, and caps how deep an unknown field can nest while it is skipped at 64 levels. Without it, a 7-byte list header makes the generated code allocate a two-billion-element list (OutOfMemoryError), a binary value can declare up to 100 MB or a negative length, and about 100 KB of nested unknown fields overflows the stack (StackOverflowError); the errors escape ordinary exception handling.
  • ParquetDictionaryEncoding decides whether a chunk's dictionary page can filter the chunk on its own, from page encoding stats or, like parquet-mr's DictionaryFilter, from the encodings when stats are missing. Where it differs from parquet-mr, it is stricter.
  • ParquetSchemaFingerprint fingerprints the column tree (names, child counts, and physical types) with a 64-bit FarmHash and ignores the writer-specific root name, so a{b}, c and a{b, c} get different fingerprints.

Packaging

  • Adds parquet-format-structures, which bundles a minimal Thrift runtime and does not depend on Hadoop, without javax.annotation-api, which none of its classes reference.
  • Shades it into the core jar and relocates org.apache.parquet.format and its Thrift runtime under com.google.cloud.gcs.analyticscore.internal.shaded, so consumers never see these classes. The dependency is optional, so it is not passed on transitively. The core jar grows from about 48 KB to about 890 KB.
  • Leaves out Util and the event package, which decode footers without per-value limits, plus the dependency's manifest and Maven metadata.
  • Declares slf4j-api directly, because the bundled Thrift transport logs through it. Core already got it through client.

Why?

Footers come from untrusted objects, so decoding them must never exhaust the heap or the stack, and decisions taken from them must be conservative.

This PR is the second of three that replace the original, larger version of #420:

  1. feat(core): add a byte-range layout model for Parquet files #420: layout model (merged)
  2. feat(core): add safe decoding helpers for Parquet footers #423: safe decoding helpers (this PR)
  3. feat(core): parse Parquet footers into a byte-range layout #424: the footer parser, which builds on both

Checklist

  • PR title follows Conventional Commits (e.g., feat(core): ...)
  • All files include the Apache License 2.0 header
  • Documentation has been updated to reflect changes

Generated/Assisted by Agent? Yes

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces robust, safe decoding helpers for Parquet footers to improve security and reliability when processing untrusted data. By implementing strict bounds on Thrift message structures and providing utilities for dictionary encoding validation and schema fingerprinting, the changes ensure that the system can safely handle potentially malicious or malformed Parquet files without risking memory or stack overflows.

Highlights

  • BoundedCompactProtocol: Introduced a new Thrift protocol that enforces limits on nesting depth and container sizes to prevent heap and stack exhaustion when parsing untrusted Parquet footers.
  • ParquetDictionaryEncoding: Added a utility to determine if a Parquet column chunk can be filtered using dictionary pages by verifying if all data pages are dictionary-encoded.
  • ParquetSchemaFingerprint: Implemented a 64-bit FarmHash-based fingerprinting mechanism for Parquet schemas to identify column tree structures independently of writer-specific root names.
  • Dependency Management: Added the parquet-format-structures dependency to the project, excluding unnecessary javax.annotation-api artifacts.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@codecov

codecov Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.36364% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.44%. Comparing base (f425f88) to head (3b51073).

Files with missing lines Patch % Lines
...cscore/core/prefetch/ParquetSchemaFingerprint.java 88.23% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #423      +/-   ##
==========================================
- Coverage   98.47%   98.44%   -0.03%     
==========================================
  Files          79       82       +3     
  Lines        3922     3977      +55     
  Branches      457      472      +15     
==========================================
+ Hits         3862     3915      +53     
  Misses         32       32              
- Partials       28       30       +2     
Files with missing lines Coverage Δ
...ticscore/core/prefetch/BoundedCompactProtocol.java 100.00% <100.00%> (ø)
...score/core/prefetch/ParquetDictionaryEncoding.java 100.00% <100.00%> (ø)
...cscore/core/prefetch/ParquetSchemaFingerprint.java 88.23% <88.23%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several new utility classes to safely handle Parquet metadata and protocols, including BoundedCompactProtocol to prevent stack/heap exhaustion, ParquetDictionaryEncoding to check dictionary encoding, and ParquetSchemaFingerprint to compute stable schema fingerprints, along with their respective unit tests. The review feedback is highly constructive, focusing on aligning the new code with the repository's style guide by strictly applying @nonnull annotations, using Guava's Preconditions.checkNotNull for boundary input validation, and adding defensive checks to prevent nesting depth underflow or null pointer exceptions.

@gcs-analytics-core-robot

gcs-analytics-core-robot commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

📊 Hermetic JMH Benchmark Summary (PR vs Main)

PR: #423 · Commit: 3b51073c9114 · Kokoro Build: c9472006-559a-4be8-9057-c96ad28b13ef

Published Assets: 🌐 Interactive HTML Report | 📄 Markdown Report | 📦 Raw JMH JSON | 🔬 Kokoro / Sponge Artifacts

Suite-Level Executive Summary

Suite Cases GeoMean Latency Δ Latency Δ Range GCS I/O Counters (opens/op, bytes/op)
FooterPrefetchBenchmark 4 +1.99% -0.50% to +4.04% 0 Δ (invariants held)
GcsReadChannelOpenBenchmark 2 -4.41% -13.57% to +5.73% 0 Δ (invariants held)
HermeticInputStreamBenchmark 8 +4.21% -6.11% to +9.20% 0 Δ (invariants held)
ReadStrategyBenchmark 12 +2.98% -4.37% to +12.53% 0 Δ (invariants held)
SmallObjectCacheBenchmark 2 +2.21% +1.18% to +3.25% 0 Δ (invariants held)
TelemetryOverheadBenchmark 8 +2.37% -0.75% to +10.71% CPU-only
VectoredRangeCoalescingBenchmark 9 +0.18% -3.81% to +7.48% CPU-only
📋 Full Hermetic Benchmark Breakdown (45 cases)
Benchmark Params Unit Main PR Latency Δ Opens/op Bytes/op Alloc (B/op)
FooterPrefetchBenchmark.readFooterUnknownSize prefetchEnabled=true us/op 42.175 43.880 +4.04% 1 128.0 KiB 395146
FooterPrefetchBenchmark.readFooterUnknownSize prefetchEnabled=false us/op 3.173 3.252 +2.47% 2 64.0 KiB 1880
FooterPrefetchBenchmark.readTail prefetchEnabled=true us/op 17.068 16.982 -0.50% 1 128 KiB 133623
FooterPrefetchBenchmark.readTail prefetchEnabled=false us/op 3.209 3.272 +1.99% 1 64 KiB 2128
GcsReadChannelOpenBenchmark.openAndReadOneByte_withFileInfo - us/op 0.266 0.230 -13.57% 1 1 B 712
GcsReadChannelOpenBenchmark.openAndReadOneByte_withItemId - us/op 0.654 0.691 +5.73% 1 1 B 1504
HermeticInputStreamBenchmark.positionedReads objectSize=1048576 us/op 121.274 126.211 +4.07% 64 1.47 MiB 718665
HermeticInputStreamBenchmark.positionedReads objectSize=16777216 us/op 59.259 64.711 +9.20% 64 508 KiB 326184
HermeticInputStreamBenchmark.readTailWithFooterPrefetch objectSize=1048576 us/op 17.164 18.632 +8.55% 1 128 KiB 133640
HermeticInputStreamBenchmark.readTailWithFooterPrefetch objectSize=16777216 us/op 18.076 19.166 +6.03% 1 128 KiB 133456
HermeticInputStreamBenchmark.readVectored objectSize=1048576 us/op 178.011 167.131 -6.11% 1 1 MiB 1053226
HermeticInputStreamBenchmark.readVectored objectSize=16777216 us/op 119.001 127.247 +6.93% 16 1 MiB 1060216
HermeticInputStreamBenchmark.sequentialRead objectSize=1048576 us/op 53.026 56.122 +5.84% 1 1 MiB 133932
HermeticInputStreamBenchmark.sequentialRead objectSize=16777216 us/op 848.268 848.939 +0.08% 1 16 MiB 149416
ReadStrategyBenchmark.seekAndRead forwardGapBytes=0, strategy=SEQUENTIAL us/op 0.937 0.931 -0.57% 1 8 KiB 1448
ReadStrategyBenchmark.seekAndRead forwardGapBytes=0, strategy=RANDOM us/op 0.968 0.926 -4.37% 1 8 KiB 1456
ReadStrategyBenchmark.seekAndRead forwardGapBytes=0, strategy=ADAPTIVE us/op 0.943 1.005 +6.58% 1 8 KiB 1496
ReadStrategyBenchmark.seekAndRead forwardGapBytes=4096, strategy=SEQUENTIAL us/op 1.165 1.140 -2.21% 1 12 KiB 1496
ReadStrategyBenchmark.seekAndRead forwardGapBytes=4096, strategy=RANDOM us/op 1.115 1.120 +0.47% 1 12 KiB 1456
ReadStrategyBenchmark.seekAndRead forwardGapBytes=4096, strategy=ADAPTIVE us/op 1.164 1.182 +1.58% 1 12 KiB 1544
ReadStrategyBenchmark.seekAndRead forwardGapBytes=65536, strategy=SEQUENTIAL us/op 3.357 3.727 +11.03% 1 72 KiB 1522
ReadStrategyBenchmark.seekAndRead forwardGapBytes=65536, strategy=RANDOM us/op 3.322 3.620 +8.97% 1 72 KiB 1496
ReadStrategyBenchmark.seekAndRead forwardGapBytes=65536, strategy=ADAPTIVE us/op 3.304 3.718 +12.53% 1 72 KiB 1496
ReadStrategyBenchmark.seekAndRead forwardGapBytes=1048576, strategy=SEQUENTIAL us/op 0.980 0.976 -0.41% 2 8 KiB 1496
ReadStrategyBenchmark.seekAndRead forwardGapBytes=1048576, strategy=RANDOM us/op 1.097 1.133 +3.23% 2 8 KiB 1736
ReadStrategyBenchmark.seekAndRead forwardGapBytes=1048576, strategy=ADAPTIVE us/op 1.116 1.122 +0.54% 2 8 KiB 1816
SmallObjectCacheBenchmark.coldRead - us/op 48.208 49.773 +3.25% 1 256 KiB 263644
SmallObjectCacheBenchmark.warmRead - us/op 9.713 9.828 +1.18% 0 0 B 1208
TelemetryOverheadBenchmark.caffeineCacheHit - ns/op 16.667 18.452 +10.71% - - 16
TelemetryOverheadBenchmark.measureOperation listenerCount=0 ns/op 2.982 3.057 +2.51% - - 0
TelemetryOverheadBenchmark.measureOperation listenerCount=1 ns/op 155.808 161.980 +3.96% - - 392
TelemetryOverheadBenchmark.measureOperation listenerCount=8 ns/op 200.202 198.708 -0.75% - - 392
TelemetryOverheadBenchmark.noOpCacheLookup - ns/op 3.239 3.250 +0.33% - - 0
TelemetryOverheadBenchmark.recordMetric listenerCount=0 ns/op 3.207 3.218 +0.34% - - 0
TelemetryOverheadBenchmark.recordMetric listenerCount=1 ns/op 25.301 25.452 +0.60% - - 24
TelemetryOverheadBenchmark.recordMetric listenerCount=8 ns/op 39.539 40.206 +1.69% - - 24
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=0, ranges=16 ns/op 214.135 214.369 +0.11% - - 528
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=0, ranges=256 ns/op 3154.765 3183.626 +0.91% - - 6768
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=0, ranges=4096 ns/op 31096.191 30666.888 -1.38% - - 92176
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=1024, ranges=16 ns/op 223.043 214.972 -3.62% - - 528
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=1024, ranges=256 ns/op 3127.981 3016.598 -3.56% - - 6768
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=1024, ranges=4096 ns/op 32469.620 32874.755 +1.25% - - 105184
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=1048576, ranges=16 ns/op 239.575 230.438 -3.81% - - 1176
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=1048576, ranges=256 ns/op 3645.955 3822.250 +4.84% - - 18920
VectoredRangeCoalescingBenchmark.mergeRanges gapBytes=1048576, ranges=4096 ns/op 46358.903 49826.860 +7.48% - - 295640

@prudhvimaharishi
prudhvimaharishi force-pushed the parquet_footer_decoding branch 2 times, most recently from 258b9c3 to 4544d0d Compare November 2, 2026 12:13
Footers come from untrusted objects, so decoding them must never exhaust
the heap or the stack, and decisions taken from them must be conservative.

- BoundedCompactProtocol caps strings, binary values, and containers at the
  message length and limits how deep an unknown value can nest when it is
  skipped, so crafted footers fail with a TException instead of an
  OutOfMemoryError or StackOverflowError. It relies on Thrift's own limits
  and only adds the binary length check that Thrift lacks.
- ParquetDictionaryEncoding decides whether a chunk's dictionary page can
  filter the chunk on its own, from page encoding stats or, like
  parquet-mr's DictionaryFilter, from the encodings when stats are missing.
- ParquetSchemaFingerprint fingerprints the column tree (names, child
  counts, and physical types) with a 64-bit FarmHash and ignores the
  writer-specific root name.

Adds parquet-format-structures, shaded and relocated into this jar so its
Thrift runtime never clashes with the one on a consumer's classpath.
Leave the dependency's manifest, Maven metadata, Util, and event package
out of the shaded jar. Util decodes footers without per-value limits, so
keeping it out leaves BoundedCompactProtocol as the only decoder.

Correct the skip depth Javadoc, which held only for the packaged jar,
note where ParquetDictionaryEncoding is stricter than parquet-mr, explain
the length prefix and the optional dependency accurately, and warn that
integration tests run against the shaded jar.

Add tests for a binary value that runs past the remaining bytes and for a
leaf and an empty group with the same name.
@prudhvimaharishi
prudhvimaharishi marked this pull request as ready for review November 3, 2026 03:34
@prudhvimaharishi
prudhvimaharishi requested a review from a team as a code owner November 3, 2026 03:34
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