From c44fc29c581807c3b01072f7fdedba024b46e2d2 Mon Sep 17 00:00:00 2001 From: ghoshp83 Date: Thu, 17 Sep 2026 20:19:56 +0100 Subject: [PATCH] fix(deletion-vector): reject a bitmap count larger than the payload _deserialize_bitmap read an 8-byte count and used it directly as a loop bound, so a small blob could declare a large number of bitmaps. Reading past the payload surfaced as an IndexError from the native bitmap decoder. Validate the count against the remaining bytes, since every bitmap contributes at least a 4-byte key. --- pyiceberg/table/deletion_vector.py | 5 +++++ tests/table/test_deletion_vector.py | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/pyiceberg/table/deletion_vector.py b/pyiceberg/table/deletion_vector.py index 88fb3daf73..96e0075f55 100644 --- a/pyiceberg/table/deletion_vector.py +++ b/pyiceberg/table/deletion_vector.py @@ -42,6 +42,11 @@ def _deserialize_bitmap(pl: bytes) -> list[BitMap]: number_of_bitmaps = int.from_bytes(pl[0:8], byteorder="little") pl = pl[8:] + # Every bitmap contributes at least a 4-byte key, so a count that cannot fit + # in the remaining payload is invalid and must not be used as a loop bound. + if number_of_bitmaps * 4 > len(pl): + raise ValueError(f"Payload declares {number_of_bitmaps} bitmaps, but only holds {len(pl)} bytes") + bitmaps = [] last_key = -1 for _ in range(number_of_bitmaps): diff --git a/tests/table/test_deletion_vector.py b/tests/table/test_deletion_vector.py index 788216f8b3..f855f8927c 100644 --- a/tests/table/test_deletion_vector.py +++ b/tests/table/test_deletion_vector.py @@ -66,6 +66,15 @@ def test_map_spread_vals() -> None: assert expected == actual +def test_map_declared_count_exceeds_payload() -> None: + # A truncated payload that claims a large number of bitmaps must be rejected, + # rather than driving the deserialization loop on data that is not there. + puffin = (2**32).to_bytes(8, byteorder="little") + b"\x00\x00\x00\x00" + + with pytest.raises(ValueError, match="Payload declares 4294967296 bitmaps, but only holds 4 bytes"): + _ = DeletionVector._deserialize_bitmap(puffin) + + def test_map_high_vals() -> None: puffin = _open_file("64maphighvals.bin")