This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new a2f3034ffa fix(parquet): bound row-group count by remaining input 
before reserving (#11419)
a2f3034ffa is described below

commit a2f3034ffaf107c6b2afb13371d128e892551ac5
Author: Panagiotis Moustafellos <[email protected]>
AuthorDate: Wed Oct 7 19:52:11 2026 +0000

    fix(parquet): bound row-group count by remaining input before reserving 
(#11419)
    
    # Which issue does this PR close?
    
    - Closes #11418.
    
    # Rationale for this change
    
    Decoding a Parquet footer reserves a `Vec` sized from the declared
    row-group count before reading any row group:
    
    ```rust
    let mut rg_vec = Vec::with_capacity(list_ident.size as usize);
    ```
    
    `list_ident.size` comes straight off the wire and `RowGroupMetaData` is
    a
    large struct, so a footer of a few bytes that declares a huge row-group
    count asks for `count * size_of::<RowGroupMetaData>()` up front, before
    a
    single row group is read. On a host under memory pressure that
    allocation
    aborts the process.
    
    PR #10979 bounded `read_thrift_vec` by the remaining input. The
    row-group
    list is decoded inline in `parquet_metadata_from_bytes` and does not go
    through that helper, so the bound did not reach it. The other footer
    lists
    (schema elements, column chunks, sorting columns, key/values, column
    orders) do go through `read_thrift_vec`. This is the one that did not.
    
    # What changes are included in this PR?
    
    - Apply the same remaining-input bound inline at the row-group list:
    each
      row group is at least one byte on the wire, so a declared count larger
      than the remaining input is malformed and is rejected before the
      reserve.
    - A test (`row_group_count_exceeding_remaining_input_is_rejected`)
    builds a
      valid footer prefix and declares an over-sized `row_groups` list, then
      asserts the decode reports the bound. Without the bound the same input
      instead fails later with `Required field columns is missing`, so the
      test is shown failing against the unfixed code.
    
    # Are these changes tested?
    
    Yes. The new test passes with the bound and fails without it, as
    described
    above.
    
    # Are there any user-facing changes?
    
    A malformed footer that declares more row groups than its bytes can hold
    now returns a `Parquet error` describing the bound, rather than
    attempting
    a large allocation. This was found while hardening the Parquet
    external-table reader in [Ravel](https://github.com/NOFireAI/ravel).
    
    ---------
    
    Signed-off-by: Panagiotis Moustafellos <[email protected]>
    Co-authored-by: Andrew Lamb <[email protected]>
    Co-authored-by: Ed Seidl <[email protected]>
---
 parquet/src/file/metadata/thrift/mod.rs | 50 +++++++++++++++++++++++++++++++--
 parquet/src/parquet_thrift.rs           | 29 +++++++++++--------
 2 files changed, 65 insertions(+), 14 deletions(-)

diff --git a/parquet/src/file/metadata/thrift/mod.rs 
b/parquet/src/file/metadata/thrift/mod.rs
index 8bc0cbbd0b..dffaacf440 100644
--- a/parquet/src/file/metadata/thrift/mod.rs
+++ b/parquet/src/file/metadata/thrift/mod.rs
@@ -51,7 +51,7 @@ use crate::{
     parquet_thrift::{
         ElementType, FieldType, ReadThrift, ThriftCompactInputProtocol,
         ThriftCompactOutputProtocol, ThriftSliceInputProtocol, WriteThrift, 
WriteThriftField,
-        read_thrift_vec, validate_list_type,
+        check_list_size, read_thrift_vec, validate_list_type,
     },
     schema::types::{
         ColumnDescriptor, SchemaDescriptor, TypePtr, num_nodes, 
parquet_schema_from_array,
@@ -820,7 +820,10 @@ pub(crate) fn parquet_metadata_from_bytes(
                 let list_ident = prot.read_list_begin()?;
                 // check for list of struct
                 validate_list_type(ElementType::Struct, &list_ident)?;
-                let mut rg_vec = Vec::with_capacity(list_ident.size as usize);
+                let size = list_ident.size as usize;
+                // Validate list size.
+                check_list_size(prot.remaining_bytes(), size)?;
+                let mut rg_vec = Vec::with_capacity(size);
 
                 for _ in 0..list_ident.size {
                     rg_vec.push(read_row_group(&mut prot, schema_descr, 
options)?);
@@ -2095,4 +2098,47 @@ pub(crate) mod tests {
             vec![None, Some(1), Some(2)],
         );
     }
+
+    /// Test row group on-wire size validation.
+    #[test]
+    fn row_group_count_exceeding_remaining_input_is_rejected() {
+        use crate::file::metadata::thrift::parquet_metadata_from_bytes;
+        use crate::parquet_thrift::{FieldType, WriteThriftField};
+        use crate::schema::types::Type as SchemaType;
+
+        // Minimal valid schema: a group with one INT32 column.
+        let field = SchemaType::primitive_type_builder("c", 
PhysicalType::INT32)
+            .build()
+            .unwrap();
+        let schema = SchemaType::group_type_builder("schema")
+            .with_fields(vec![Arc::new(field)])
+            .build()
+            .unwrap();
+        let root: TypePtr = Arc::new(schema);
+        let schema_len = num_nodes(&root).unwrap();
+
+        let mut buf = Vec::new();
+        let mut w = ThriftCompactOutputProtocol::new(&mut buf);
+        // FileMetaData field 1: version.
+        1i32.write_thrift_field(&mut w, 1, 0).unwrap();
+        // field 2: schema (required before row groups are read).
+        w.write_field_begin(FieldType::List, 2, 1).unwrap();
+        w.write_list_begin(ElementType::Struct, schema_len).unwrap();
+        write_schema(&root, &mut w).unwrap();
+        // field 3: num_rows.
+        0i64.write_thrift_field(&mut w, 3, 2).unwrap();
+        // field 4: row_groups, declaring 1024 structs with no element bytes
+        // after the header. Only the struct-stop byte remains.
+        w.write_field_begin(FieldType::List, 4, 3).unwrap();
+        w.write_list_begin(ElementType::Struct, 1024).unwrap();
+        w.write_struct_end().unwrap();
+
+        let err = parquet_metadata_from_bytes(&buf, None)
+            .expect_err("over-declared row-group count must be rejected")
+            .to_string();
+        assert!(
+            err.contains("exceeds remaining input length"),
+            "expected a remaining-input bound error, got: {err}"
+        );
+    }
 }
diff --git a/parquet/src/parquet_thrift.rs b/parquet/src/parquet_thrift.rs
index 9e69fc382b..39d0128f14 100644
--- a/parquet/src/parquet_thrift.rs
+++ b/parquet/src/parquet_thrift.rs
@@ -734,18 +734,7 @@ where
     let list_ident = prot.read_list_begin()?;
     validate_list_type(T::ELEMENT_TYPE, &list_ident)?;
     let size = list_ident.size as usize;
-    // Each list element occupies at least one byte on the wire. Bound the
-    // declared count by remaining input before reserving, so a malformed
-    // header cannot abort the process with a huge allocation.
-    if let Some(remaining) = prot.remaining_bytes()
-        && size > remaining
-    {
-        return Err(general_err!(
-            "Thrift list size {} exceeds remaining input length {}",
-            size,
-            remaining
-        ));
-    }
+    check_list_size(prot.remaining_bytes(), size)?;
     let mut res = Vec::with_capacity(size);
     for _ in 0..list_ident.size {
         let val = T::read_thrift(prot)?;
@@ -765,6 +754,22 @@ pub(crate) fn validate_list_type(expected: ElementType, 
got: &ListIdentifier) ->
     Ok(())
 }
 
+/// Reject a declared list size that cannot fit the remaining input. Each list
+/// element is at least one byte on the wire, so a size past the remaining
+/// bytes is a malformed header; bounding it before reserving stops a huge
+/// allocation. `remaining` is `None` when the protocol is not backed by a
+/// finite buffer, in which case there is nothing to check against.
+pub(crate) fn check_list_size(remaining: Option<usize>, size: usize) -> 
Result<()> {
+    if let Some(remaining) = remaining
+        && size > remaining
+    {
+        return Err(general_err!(
+            "Thrift list size {size} exceeds remaining input length 
{remaining}"
+        ));
+    }
+    Ok(())
+}
+
 /////////////////////////
 // thrift compact output
 

Reply via email to