Skip to content

Expose IPC decompressed size and bound LZ4 output - #10959

Open
azecevic000 wants to merge 3 commits into
apache:mainfrom
azecevic000:ipc-decompression-size-upper-bound
Open

Expose IPC decompressed size and bound LZ4 output#10959
azecevic000 wants to merge 3 commits into
apache:mainfrom
azecevic000:ipc-decompression-size-upper-bound

Conversation

@azecevic000

Copy link
Copy Markdown

Rationale for this change

Compressed Arrow IPC buffers advertise their uncompressed length in an 8-byte prefix. decompress_to_buffer interprets 0 as empty, -1 as uncompressed, and a positive value as the expected decompressed length. For a positive value, IPC decompression already requires an exact match: the common codec path rejects a returned buffer whose length differs from the advertised length.

The codecs previously differed in how they used this value while decompressing. ZSTD passes it as the output capacity to an API that returns an error if the decompressed data exceeds that capacity. LZ4, however, used it only as the initial Vec capacity before an unbounded read_to_end, allowing Arrow's output Vec to grow beyond the advertised length before the final check.

This change gives both codecs the same output-size invariants:

  • No more than the advertised number of decompressed bytes are accumulated in the output Vec.
  • Successful decompression returns exactly the advertised number of bytes.

Without the LZ4 bound, an untrusted compressed stream could cause Arrow's output Vec to grow beyond an accepted advertised length before decompression eventually failed. With the bound, the potentially unbounded output accumulation is limited to the advertised length.

Downstream IPC consumers may also need to inspect the advertised length before decompression so that they can reject an unreasonable value before allocating the output buffer. Exposing read_uncompressed_size lets them do so without duplicating Arrow's interpretation of the IPC prefix.

What changes are included in this PR?

  • Publicly re-export read_uncompressed_size from arrow-ipc and document the -1, 0, and positive-length semantics.
  • Limit the Arrow-owned LZ4 output buffer to the advertised length while decompressing, and reject output that exceeds it without growing that buffer.
  • Correct the existing length-mismatch error message to refer to the decompressed length.

The advertised length remains the exact required output length on successful return and bounds the number of decompressed bytes accumulated in Arrow's output Vec. It does not bound the compressed input, allocator overhead, or codec working memory.

Are these changes tested?

No new tests are introduced. Existing tests cover successful LZ4 and ZSTD round trips and rejection of prefixes shorter than 8 bytes. The new LZ4 over-limit rejection is not directly covered.

Are there any user-facing changes?

Yes. arrow_ipc::read_uncompressed_size is a new public, non-breaking API that lets callers inspect the IPC compression prefix before decompression.

Malformed LZ4 input whose decompressed output exceeds its advertised length is now rejected before Arrow's output buffer grows beyond that length. Valid IPC input is unaffected.

@github-actions github-actions Bot added arrow Changes to the arrow crate arrow-ipc labels Sep 2, 2026
@Rich-T-kid

Copy link
Copy Markdown
Contributor

run benchmark arrow-ipc

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5513518595-2087-kkkx4 6.12.94+ #1 SMP Fri Jul 17 09:42:57 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing ipc-decompression-size-upper-bound (eb92502) to b84fc5c (merge-base) diff

Run configuration
run benchmark arrow-ipc

BENCH_COMMAND=cargo bench --features=arrow,async,test_common,experimental,object_store --bench arrow-ipc
Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

Benchmark for this request failed before finishing (Kubernetes reason: BackoffLimitExceeded).

Benchmarks requested: arrow-ipc

Kubernetes message
Job has reached the specified backoff limit

File an issue against this benchmark runner

@Rich-T-kid Rich-T-kid 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.

thank you for the in depth PR description.
can you add test for this if it doesn't already exist
'
Without the LZ4 bound, an untrusted compressed stream could cause Arrow's output Vec to grow beyond an accepted advertised length before decompression eventually failed.
'

/// Returns an error if the input buffer is shorter than the 8-byte prefix.
#[inline]
fn read_uncompressed_size(buffer: &[u8]) -> Result<i64, ArrowError> {
pub fn read_uncompressed_size(buffer: &[u8]) -> Result<i64, ArrowError> {

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.

I'm not sure that we should make this public.

do you have examples of when it would be useful to use this directly?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@Rich-T-kid Thank you for the prompt review. I have added a test that returns an error on an underreported size for LZ4.

To answer your question, this function can be very useful for downstream users. It allows effective resource governance, where instead of unconditionally doing decompress and possibly getting an OOM issue during the allocation, you check if resources (memory in this case) in your system are available and make a decision based on that. That is also why LZ4 fix is important, since it gives us a much stronger guarantee.

I don't see any other way to achieve this currently that is exposed through the public API.

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.

makes sense, I'm interested in what Jefffrey thinks.

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.

where instead of unconditionally doing decompress and possibly getting an OOM issue during the allocation, you check if resources (memory in this case) in your system are available and make a decision based on that.

this is interesting, you may be interested in this issue #10392

@azecevic000 azecevic000 Sep 3, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for pointing out the issue, though the proposition is more about memory optimization if some kind of projection is done (which I agree can be significant for some workloads), the issue I raised is more concerned with having a guaranteed upper bound on memory allocation and knowing it before allocation happens.

Thanks for the review, I totally understand general hesitation when expansion of the public API surface area is considered. In this case, though, I believe the decompressed size is more than internal detail, as downstream users can generally make decisions based on it.

@Rich-T-kid

Copy link
Copy Markdown
Contributor

run benchmark ipc_reader

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5513931990-2100-2xkzt 6.12.94+ #1 SMP Fri Jul 17 09:42:57 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing ipc-decompression-size-upper-bound (eb92502) to b84fc5c (merge-base) diff

Run configuration
run benchmark ipc_reader

BENCH_COMMAND=cargo bench --features=arrow,async,test_common,experimental,object_store --bench ipc_reader
Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

Comparing ipc-decompression-size-upper-bound (eb92502) to b84fc5c (merge-base) diff

Run configuration
run benchmark ipc_reader
CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group                                                       ipc-decompression-size-upper-bound     main
-----                                                       ----------------------------------     ----
arrow_ipc_reader/FileReader/no_validation/read_10           1.00    118.4±5.47µs        ? ?/sec    1.04    122.6±6.16µs        ? ?/sec
arrow_ipc_reader/FileReader/no_validation/read_10/mmap      1.00     59.0±0.79µs        ? ?/sec    1.00     58.8±0.90µs        ? ?/sec
arrow_ipc_reader/FileReader/read_10                         1.00   423.1±38.69µs        ? ?/sec    1.00   424.5±34.62µs        ? ?/sec
arrow_ipc_reader/FileReader/read_10/mmap                    1.00   482.1±35.99µs        ? ?/sec    1.01   487.1±34.93µs        ? ?/sec
arrow_ipc_reader/StreamReader/no_validation/read_10         1.00    118.3±4.62µs        ? ?/sec    1.02    120.8±4.85µs        ? ?/sec
arrow_ipc_reader/StreamReader/no_validation/read_10/zstd    1.00      2.5±0.02ms        ? ?/sec    1.00      2.4±0.02ms        ? ?/sec
arrow_ipc_reader/StreamReader/read_10                       1.00   426.5±37.64µs        ? ?/sec    1.00   425.1±42.45µs        ? ?/sec
arrow_ipc_reader/StreamReader/read_10/zstd                  1.00      2.7±0.01ms        ? ?/sec    1.00      2.7±0.01ms        ? ?/sec

Resource Usage

base (merge-base)

Metric Value
Wall time 85.0s
Peak memory 16.4 MiB
Avg memory 12.1 MiB
CPU user 71.9s
CPU sys 10.1s
Peak spill 0 B

branch

Metric Value
Wall time 85.0s
Peak memory 15.8 MiB
Avg memory 11.1 MiB
CPU user 69.4s
CPU sys 9.6s
Peak spill 0 B

File an issue against this benchmark runner

@Rich-T-kid Rich-T-kid 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.

Thank you for adding a test. This PR looks good to me!

ideally we keep our api surface as small as possible but this seems small and focused so it looks fine to me.

thanks @azecevic000

Comment thread arrow-ipc/src/compression.rs Outdated

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.

while we're here can we avoid this panic? it should be pretty straightforward to error here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Sure, done.


#[test]
#[cfg(feature = "lz4")]
fn test_lz4_decompression_rejects_output_exceeding_advertised_size() {

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.

nice

/// Returns an error if the input buffer is shorter than the 8-byte prefix.
#[inline]
fn read_uncompressed_size(buffer: &[u8]) -> Result<i64, ArrowError> {
pub fn read_uncompressed_size(buffer: &[u8]) -> Result<i64, ArrowError> {

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.

makes sense, I'm interested in what Jefffrey thinks.

/// Returns an error if the input buffer is shorter than the 8-byte prefix.
#[inline]
fn read_uncompressed_size(buffer: &[u8]) -> Result<i64, ArrowError> {
pub fn read_uncompressed_size(buffer: &[u8]) -> Result<i64, ArrowError> {

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.

where instead of unconditionally doing decompress and possibly getting an OOM issue during the allocation, you check if resources (memory in this case) in your system are available and make a decision based on that.

this is interesting, you may be interested in this issue #10392

@Jefffrey Jefffrey added the bug label Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arrow Changes to the arrow crate arrow-ipc bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants