Skip to content

Commit fd2a99e

Browse files
author
Qiwei Huang
committed
Merge remote-tracking branch 'origin/main' into fix/parquet-mask-sparse-pages
2 parents 9ed6dbb + fed7862 commit fd2a99e

10 files changed

Lines changed: 177 additions & 68 deletions

File tree

.github/workflows/dev_pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
github.event_name == 'pull_request_target' &&
4545
(github.event.action == 'opened' ||
4646
github.event.action == 'synchronize')
47-
uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
47+
uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
4848
with:
4949
repo-token: ${{ secrets.GITHUB_TOKEN }}
5050
configuration-path: .github/workflows/dev_pr/labeler.yml

Cargo.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,20 +91,10 @@ Planned Release Schedule
9191

9292
| Approximate Date | Version | Notes |
9393
| ---------------- | ---------- | --------------------------------------- |
94-
| May 2026 | [`58.3.0`] | Minor, NO breaking API changes |
95-
| May 2026 | [`57.3.1`] | Patch, NO breaking API changes |
96-
| May 2026 | [`56.2.1`] | Patch, NO breaking API changes |
97-
| May 2026 | [`59.0.0`] | Major, potentially breaking API changes |
9894
| June 2026 | [`59.1.0`] | Minor, NO breaking API changes |
9995
| July 2026 | [`59.2.0`] | Minor, NO breaking API changes |
10096
| August 2026 | [`60.0.0`] | Major, potentially breaking API changes |
10197

102-
[`58.1.0`]: https://github.com/apache/arrow-rs/issues/9108
103-
[`58.2.0`]: https://github.com/apache/arrow-rs/issues/9109
104-
[`58.3.0`]: https://github.com/apache/arrow-rs/issues/9859
105-
[`57.3.1`]: https://github.com/apache/arrow-rs/issues/9858
106-
[`56.2.1`]: https://github.com/apache/arrow-rs/issues/9857
107-
[`59.0.0`]: https://github.com/apache/arrow-rs/issues/9110
10898
[`59.1.0`]: https://github.com/apache/arrow-rs/issues/9878
10999
[`59.2.0`]: https://github.com/apache/arrow-rs/issues/9879
110100
[`60.0.0`]: https://github.com/apache/arrow-rs/issues/9880

arrow-avro/src/reader/block.rs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,13 @@ impl BlockDecoder {
9696
AvroError::ParseError(format!("Block size cannot be negative, got {c}"))
9797
})?;
9898

99-
self.in_progress.data.reserve(self.bytes_remaining);
99+
// Only reserve what the current input backs: the block size is
100+
// input specified so could be an extreme value (e.g. i64::MAX)
101+
// in case of corrupted/malicious input. The rest is reserved
102+
// lazily by `extend_from_slice` below as data arrives.
103+
self.in_progress
104+
.data
105+
.reserve(self.bytes_remaining.min(buf.len()));
100106
self.state = BlockDecoderState::Data;
101107
}
102108
}
@@ -148,3 +154,72 @@ impl BlockDecoder {
148154
self.bytes_remaining
149155
}
150156
}
157+
158+
#[cfg(test)]
159+
mod tests {
160+
use super::*;
161+
162+
/// Zig-zag encode `value` as an Avro `long` (variable-length integer).
163+
fn encode_long(value: i64, out: &mut Vec<u8>) {
164+
let mut n = ((value << 1) ^ (value >> 63)) as u64;
165+
while n >= 0x80 {
166+
out.push((n as u8) | 0x80);
167+
n >>= 7;
168+
}
169+
out.push(n as u8);
170+
}
171+
172+
#[test]
173+
fn test_oversized_block_size_bounds_reserve() {
174+
// A block advertising `i64::MAX` bytes must not reserve that up front when only
175+
// a few payload bytes are present, or a crafted OCF aborts on a huge alloc (#10234).
176+
let mut buf = Vec::new();
177+
encode_long(1, &mut buf); // object count
178+
encode_long(i64::MAX, &mut buf); // attacker-controlled block size
179+
buf.extend_from_slice(&[0u8; 8]); // a handful of real bytes
180+
181+
let mut decoder = BlockDecoder::default();
182+
let read = decoder.decode(&buf).unwrap();
183+
184+
assert_eq!(read, buf.len(), "all available input should be consumed");
185+
assert!(
186+
decoder.in_progress.data.capacity() <= buf.len(),
187+
"capacity {} must stay bounded by available input {}, not the advertised i64::MAX",
188+
decoder.in_progress.data.capacity(),
189+
buf.len(),
190+
);
191+
}
192+
193+
#[test]
194+
fn test_negative_block_size_errors() {
195+
let mut buf = Vec::new();
196+
encode_long(1, &mut buf); // object count
197+
encode_long(-1, &mut buf); // invalid (negative) block size
198+
199+
let mut decoder = BlockDecoder::default();
200+
let err = decoder.decode(&buf).unwrap_err();
201+
assert!(
202+
err.to_string().contains("Block size cannot be negative"),
203+
"unexpected error: {err}",
204+
);
205+
}
206+
207+
#[test]
208+
fn test_well_formed_block_round_trips() {
209+
// The capped reserve must not change decoding of a normal block.
210+
let payload = [1u8, 2, 3, 4];
211+
let sync = [7u8; 16];
212+
let mut buf = Vec::new();
213+
encode_long(2, &mut buf); // object count
214+
encode_long(payload.len() as i64, &mut buf); // block size
215+
buf.extend_from_slice(&payload);
216+
buf.extend_from_slice(&sync);
217+
218+
let mut decoder = BlockDecoder::default();
219+
assert_eq!(decoder.decode(&buf).unwrap(), buf.len());
220+
let block = decoder.flush().expect("a complete block");
221+
assert_eq!(block.count, 2);
222+
assert_eq!(block.data, payload);
223+
assert_eq!(block.sync, sync);
224+
}
225+
}

arrow-avro/src/reader/record.rs

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2286,35 +2286,56 @@ fn process_blockwise(
22862286
match block_count.cmp(&0) {
22872287
Ordering::Equal => break,
22882288
Ordering::Less => {
2289-
let count = (-block_count) as usize;
2289+
// `unsigned_abs` avoids overflowing `-block_count` for `i64::MIN` (#10235)
2290+
let count = block_count.unsigned_abs() as usize;
22902291
// A negative count is followed by a long of the size in bytes
2291-
let size_in_bytes = buf.get_long()? as usize;
2292+
let raw_size = buf.get_long()?;
2293+
let size_in_bytes = usize::try_from(raw_size).map_err(|_| {
2294+
AvroError::ParseError(format!("Block size cannot be negative, got {raw_size}"))
2295+
})?;
22922296
match negative_behavior {
22932297
NegativeBlockBehavior::ProcessItems => {
22942298
// Process items one-by-one after reading size
2295-
for _ in 0..count {
2296-
on_item(buf)?;
2297-
}
2299+
total = process_block_items(buf, count, total, &mut on_item)?;
22982300
}
22992301
NegativeBlockBehavior::SkipBySize => {
23002302
// Skip the entire block payload at once
23012303
let _ = buf.get_fixed(size_in_bytes)?;
2304+
total = total.saturating_add(count);
23022305
}
23032306
}
2304-
total += count;
23052307
}
23062308
Ordering::Greater => {
23072309
let count = block_count as usize;
2308-
for _ in 0..count {
2309-
on_item(buf)?;
2310-
}
2311-
total += count;
2310+
total = process_block_items(buf, count, total, &mut on_item)?;
23122311
}
23132312
}
23142313
}
23152314
Ok(total)
23162315
}
23172316

2317+
/// Decode `count` items, capping the running total at `i32::MAX` (the largest index
2318+
/// an Arrow list/map offset holds). Otherwise a crafted `i64::MAX` count of a zero-byte
2319+
/// item like `null` spins the loop forever (#10235); byte-consuming items self-terminate
2320+
/// on cursor exhaustion, so valid blocks (including `array<null>`) are unaffected.
2321+
#[inline]
2322+
fn process_block_items(
2323+
buf: &mut AvroCursor,
2324+
count: usize,
2325+
total: usize,
2326+
on_item: &mut impl FnMut(&mut AvroCursor) -> Result<(), AvroError>,
2327+
) -> Result<usize, AvroError> {
2328+
let Some(new_total) = total.checked_add(count).filter(|&t| t <= i32::MAX as usize) else {
2329+
return Err(AvroError::ParseError(
2330+
"Capacity overflow when decoding array/map item blocks".to_string(),
2331+
));
2332+
};
2333+
for _ in 0..count {
2334+
on_item(buf)?;
2335+
}
2336+
Ok(new_total)
2337+
}
2338+
23182339
#[inline]
23192340
fn flush_values<T>(values: &mut Vec<T>) -> Vec<T> {
23202341
std::mem::replace(values, Vec::with_capacity(DEFAULT_CAPACITY))
@@ -3434,6 +3455,60 @@ mod tests {
34343455
assert_eq!(values.value(2), 3);
34353456
}
34363457

3458+
/// Zig-zag + unsigned-LEB128 encode, correct for all `i64` including `MIN`/`MAX`
3459+
/// (`encode_avro_long` loops forever on those two values).
3460+
fn encode_avro_long_extreme(value: i64) -> Vec<u8> {
3461+
let mut n = ((value << 1) ^ (value >> 63)) as u64;
3462+
let mut out = Vec::new();
3463+
while n >= 0x80 {
3464+
out.push((n as u8) | 0x80);
3465+
n >>= 7;
3466+
}
3467+
out.push(n as u8);
3468+
out
3469+
}
3470+
3471+
// `array<null>` is the worst case: items consume no bytes, so an unbounded
3472+
// `block_count` spins the item loop without ever advancing the cursor (#10235).
3473+
fn array_of_null_decoder() -> Decoder {
3474+
let list_dt = avro_from_codec(Codec::List(Arc::new(avro_from_codec(Codec::Null))));
3475+
Decoder::try_new(&list_dt).unwrap()
3476+
}
3477+
3478+
#[test]
3479+
fn test_array_of_null_decodes() {
3480+
let mut decoder = array_of_null_decoder();
3481+
let mut data = encode_avro_long(3); // three null items
3482+
data.extend_from_slice(&encode_avro_long(0)); // empty-block terminator
3483+
decoder.decode(&mut AvroCursor::new(&data)).unwrap();
3484+
}
3485+
3486+
#[test]
3487+
fn test_array_block_count_i64_max_errors() {
3488+
// A positive `i64::MAX` block count must error rather than spin the item loop.
3489+
let mut decoder = array_of_null_decoder();
3490+
let mut data = encode_avro_long_extreme(i64::MAX); // item count
3491+
data.extend_from_slice(&encode_avro_long(0)); // empty-block terminator
3492+
let err = decoder.decode(&mut AvroCursor::new(&data)).unwrap_err();
3493+
assert!(
3494+
err.to_string().contains("Capacity overflow"),
3495+
"unexpected error: {err}",
3496+
);
3497+
}
3498+
3499+
#[test]
3500+
fn test_array_block_count_i64_min_errors() {
3501+
// `i64::MIN` previously overflowed `-block_count` before spinning the loop.
3502+
let mut decoder = array_of_null_decoder();
3503+
let mut data = encode_avro_long_extreme(i64::MIN); // negative item count
3504+
data.extend_from_slice(&encode_avro_long(0)); // block size in bytes
3505+
let err = decoder.decode(&mut AvroCursor::new(&data)).unwrap_err();
3506+
assert!(
3507+
err.to_string().contains("Capacity overflow"),
3508+
"unexpected error: {err}",
3509+
);
3510+
}
3511+
34373512
#[test]
34383513
fn test_nested_array_decoding() {
34393514
let inner_ty = avro_from_codec(Codec::List(Arc::new(avro_from_codec(Codec::Int32))));

arrow-flight/src/decode.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,12 +308,14 @@ impl FlightDataDecoder {
308308
)
309309
})?;
310310

311-
arrow_ipc::reader::read_dictionary(
311+
arrow_ipc::reader::read_dictionary_impl(
312312
&buffer,
313313
dictionary_batch,
314314
&state.schema,
315315
&mut state.dictionaries_by_field,
316316
&message.version(),
317+
false,
318+
self.skip_validation.clone(),
317319
)
318320
.map_err(|e| {
319321
FlightError::DecodeError(format!("Error decoding ipc dictionary: {e}"))

arrow-ipc/src/reader.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,8 @@ pub fn read_dictionary(
798798
)
799799
}
800800

801-
fn read_dictionary_impl(
801+
/// Low-level version of [`read_dictionary`] with alignment and validation controls
802+
pub fn read_dictionary_impl(
802803
buf: &Buffer,
803804
batch: crate::DictionaryBatch,
804805
schema: &Schema,

dev/release/README.md

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ This file documents the release process for the "Rust Arrow Crates": `arrow`, `a
2828
The Rust Arrow Crates are interconnected (e.g. `parquet` has an optional dependency on `arrow`) so we increment and release all of them together.
2929

3030
If any code has been merged to main that has a breaking API change, as defined
31-
in [Rust RFC 1105] he major version number is incremented (e.g. `9.0.2` to `10.0.2`).
31+
in [Rust RFC 1105] the major version number is incremented (e.g. `9.0.2` to `10.0.0`).
3232
Otherwise the new minor version incremented (e.g. `9.0.2` to `9.1.0`).
3333

3434
[rust rfc 1105]: https://github.com/rust-lang/rfcs/blob/master/text/1105-api-evolution.md
@@ -145,35 +145,8 @@ The `create-tarball.sh` script
145145

146146
### Vote on Release Candidate tarball
147147

148-
Send an email, based on the output from the script to dev@arrow.apache.org. The email should look like
149-
150-
```
151-
To: dev@arrow.apache.org
152-
Subject: [VOTE][RUST] Release Apache Arrow
153-
154-
Hi,
155-
156-
I would like to propose a release of Apache Arrow Rust
157-
Implementation, version 4.1.0.
158-
159-
This release candidate is based on commit: a5dd428f57e62db20a945e8b1895de91405958c4 [1]
160-
161-
The proposed release tarball and signatures are hosted at [2].
162-
The changelog is located at [3].
163-
164-
Please download, verify checksums and signatures, run the unit tests,
165-
and vote on the release.
166-
167-
The vote will be open for at least 72 hours.
168-
169-
[ ] +1 Release this as Apache Arrow Rust
170-
[ ] +0
171-
[ ] -1 Do not release this as Apache Arrow Rust because...
172-
173-
[1]: https://github.com/apache/arrow-rs/tree/a5dd428f57e62db20a945e8b1895de91405958c4
174-
[2]: https://dist.apache.org/repos/dist/dev/arrow/apache-arrow-rs-4.1.0
175-
[3]: https://github.com/apache/arrow-rs/blob/a5dd428f57e62db20a945e8b1895de91405958c4/CHANGELOG.md
176-
```
148+
Send an email, based on the output from the script to dev@arrow.apache.org.
149+
See an [example of how the email should look](https://lists.apache.org/thread/2vpxdt6n7kzo72sxpr7q8yyby4495gnk).
177150

178151
For the release to become "official" it needs at least three Apache Arrow PMC members to vote +1 on it.
179152

parquet-geospatial/src/lib.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,6 @@
1919
//!
2020
//! [Geometry and Geography Encoding]: https://github.com/apache/parquet-format/blob/master/Geospatial.md
2121
//! [Apache Parquet]: https://parquet.apache.org/
22-
//!
23-
//! ## 🚧 Work In Progress
24-
//!
25-
//! This crate is under active development and is not yet ready for production use.
26-
//! If you are interested in helping, you can find more information on the GitHub [Geometry issue]
27-
//!
28-
//! [Geometry issue]: https://github.com/apache/arrow-rs/issues/8373
2922
3023
pub mod bounding;
3124
pub mod interval;

parquet/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ arrow-cast = { workspace = true }
9393
tokio = { version = "1.0", default-features = false, features = ["macros", "rt-multi-thread", "io-util", "fs"] }
9494
rand = { version = "0.9", default-features = false, features = ["std", "std_rng", "thread_rng"] }
9595
object_store = { workspace = true, features = ["azure", "fs"] }
96-
sysinfo = { version = "0.38.1", default-features = false, features = ["system"] }
96+
sysinfo = { version = "0.39.6", default-features = false, features = ["system"] }
9797

9898
[package.metadata.docs.rs]
9999
all-features = true

0 commit comments

Comments
 (0)