From 390481493e7db7e17739b1d0a7e4f62ce5bf3dfc Mon Sep 17 00:00:00 2001 From: Ishaan Maheshwari Date: Sun, 25 Oct 2026 07:24:21 +0530 Subject: [PATCH] GH-54957: [Python][Parquet] Translate C++ ParquetException in metadata accessors to prevent process abort Add 'except +' to C++ declarations of statistics(), geo_statistics(), crypto_metadata(), ColumnChunk(), RowGroup(), WriteTo(), and CFileMetaData_Make in libparquet.pxd, and 'except *' to RowGroupMetaData.init and ColumnChunkMetaData.init in _parquet.pxd. Without 'except +', Cython does not generate C++ try/catch wrappers around these methods. When a damaged Parquet footer causes C++ to throw parquet::ParquetException, it triggers std::terminate across the C boundary, aborting the Python interpreter instead of translating into a Python exception. --- python/pyarrow/_parquet.pxd | 4 +- python/pyarrow/includes/libparquet.pxd | 14 +++---- python/pyarrow/tests/parquet/test_metadata.py | 37 +++++++++++++++++++ 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/python/pyarrow/_parquet.pxd b/python/pyarrow/_parquet.pxd index 36fc2ccf2f33..0cd096a62dc2 100644 --- a/python/pyarrow/_parquet.pxd +++ b/python/pyarrow/_parquet.pxd @@ -98,7 +98,7 @@ cdef class RowGroupMetaData(_Weakrefable): CRowGroupMetaData* metadata FileMetaData parent - cdef inline init(self, FileMetaData parent, int index): + cdef inline init(self, FileMetaData parent, int index) except *: if index < 0 or index >= parent.num_row_groups: raise IndexError('{0} out of bounds'.format(index)) self.up_metadata = parent._metadata.RowGroup(index) @@ -113,7 +113,7 @@ cdef class ColumnChunkMetaData(_Weakrefable): CColumnChunkMetaData* metadata RowGroupMetaData parent - cdef inline init(self, RowGroupMetaData parent, int i): + cdef inline init(self, RowGroupMetaData parent, int i) except *: self.up_metadata = parent.metadata.ColumnChunk(i) self.metadata = self.up_metadata.get() self.parent = parent diff --git a/python/pyarrow/includes/libparquet.pxd b/python/pyarrow/includes/libparquet.pxd index 3a5f91716c17..2bb967dcc919 100644 --- a/python/pyarrow/includes/libparquet.pxd +++ b/python/pyarrow/includes/libparquet.pxd @@ -358,9 +358,9 @@ cdef extern from "parquet/api/reader.h" namespace "parquet" nogil: int64_t num_values() const shared_ptr[ColumnPath] path_in_schema() const bint is_stats_set() const - shared_ptr[CStatistics] statistics() const + shared_ptr[CStatistics] statistics() const except + c_bool is_geo_stats_set() const - shared_ptr[CParquetGeoStatistics] geo_statistics() const + shared_ptr[CParquetGeoStatistics] geo_statistics() const except + ParquetCompression compression() const const vector[ParquetEncoding]& encodings() const c_bool Equals(const CColumnChunkMetaData&) const @@ -373,7 +373,7 @@ cdef extern from "parquet/api/reader.h" namespace "parquet" nogil: int64_t total_uncompressed_size() const optional[int64_t] bloom_filter_offset() const optional[int64_t] bloom_filter_length() const - unique_ptr[CColumnCryptoMetaData] crypto_metadata() const + unique_ptr[CColumnCryptoMetaData] crypto_metadata() const except + optional[ParquetIndexLocation] GetColumnIndexLocation() const optional[ParquetIndexLocation] GetOffsetIndexLocation() const shared_ptr[const CKeyValueMetadata] key_value_metadata() const @@ -389,7 +389,7 @@ cdef extern from "parquet/api/reader.h" namespace "parquet" nogil: int64_t num_rows() const int64_t total_byte_size() const vector[CSortingColumn] sorting_columns() const - unique_ptr[CColumnChunkMetaData] ColumnChunk(int i) const + unique_ptr[CColumnChunkMetaData] ColumnChunk(int i) const except + cdef cppclass CFileMetaData" parquet::FileMetaData": c_bool Equals(const CFileMetaData&) const @@ -404,10 +404,10 @@ cdef extern from "parquet/api/reader.h" namespace "parquet" nogil: void set_file_path(const c_string& path) void AppendRowGroups(const CFileMetaData& other) except + - unique_ptr[CRowGroupMetaData] RowGroup(int i) + unique_ptr[CRowGroupMetaData] RowGroup(int i) except + const SchemaDescriptor* schema() shared_ptr[const CKeyValueMetadata] key_value_metadata() const - void WriteTo(COutputStream* dst) const + void WriteTo(COutputStream* dst) const except + inline c_bool is_encryption_algorithm_set() const inline EncryptionAlgorithm encryption_algorithm() const @@ -415,7 +415,7 @@ cdef extern from "parquet/api/reader.h" namespace "parquet" nogil: cdef shared_ptr[CFileMetaData] CFileMetaData_Make \ " parquet::FileMetaData::Make"(const void* serialized_metadata, - uint32_t* metadata_len) + uint32_t* metadata_len) except + cdef cppclass CReaderProperties" parquet::ReaderProperties": CMemoryPool* memory_pool() const diff --git a/python/pyarrow/tests/parquet/test_metadata.py b/python/pyarrow/tests/parquet/test_metadata.py index 9eee70b125e5..d28ab5802435 100644 --- a/python/pyarrow/tests/parquet/test_metadata.py +++ b/python/pyarrow/tests/parquet/test_metadata.py @@ -893,3 +893,40 @@ def test_geospatial_types(parquet_test_datadir): column_schema = metadata.schema.column(1) assert column_schema.name == "geography" assert column_schema.logical_type.type == "GEOGRAPHY" + + +def test_damaged_metadata_raises_exception_not_abort(tmp_path): + # GH-54957: Damaged metadata or type mismatches in Parquet footer + # should raise a Python exception rather than aborting the process. + clean_path = tmp_path / "clean.parquet" + table = pa.table({"x": pa.array([1, 2, 3], pa.int64())}) + pq.write_table(table, clean_path) + + data = bytearray(clean_path.read_bytes()) + # Modify repetition_type in Thrift SchemaElement for "x" + # (zigzag 2 = OPTIONAL -> change to 0 = REQUIRED) + marker = b"\x25\x02\x18\x01x" + idx = data.index(marker) + data[idx + 1] = 0 + crash_path = tmp_path / "corrupt_levels.parquet" + crash_path.write_bytes(data) + + md = pq.ParquetFile(crash_path).metadata + with pytest.raises(OSError): + md.row_group(0).column(0) + + # Test physical type mismatch in ColumnMetaData vs SchemaElement + table_float = pa.table({"x": pa.array([1.0, 2.0, 3.0], pa.float64())}) + clean_float_path = tmp_path / "clean_float.parquet" + pq.write_table(table_float, clean_float_path) + + data_float = bytearray(clean_float_path.read_bytes()) + marker_float = b"\x15\x0a\x25\x02\x18\x01x" + idx_float = data_float.index(marker_float) + data_float[idx_float + 1] = 0 # DOUBLE -> BOOLEAN + crash_float_path = tmp_path / "corrupt_stats.parquet" + crash_float_path.write_bytes(data_float) + + col = pq.ParquetFile(crash_float_path).metadata.row_group(0).column(0) + with pytest.raises(OSError): + _ = col.statistics