From 04cd724a3e5bd45dfa4acd131dd3a9a1aa047734 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 14 Oct 2026 18:51:05 +0000 Subject: [PATCH 1/9] PYTHON-6172 Add regression test for short UUID binary payloads --- fuzz/regression/PYTHON-6172.bson | Bin 0 -> 12 bytes test/test_bson.py | 46 +++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 fuzz/regression/PYTHON-6172.bson diff --git a/fuzz/regression/PYTHON-6172.bson b/fuzz/regression/PYTHON-6172.bson new file mode 100644 index 0000000000000000000000000000000000000000..4635e81c8b70715df9989d8edfa7b52f9cb2cd0f GIT binary patch literal 12 Rcmd;KU|?VcVg?2l1^@ue02TlM literal 0 HcmV?d00001 diff --git a/test/test_bson.py b/test/test_bson.py index 392751ef7f..6991db963a 100644 --- a/test/test_bson.py +++ b/test/test_bson.py @@ -51,7 +51,9 @@ json_util, ) from bson.binary import ( + OLD_UUID_SUBTYPE, USER_DEFINED_SUBTYPE, + UUID_SUBTYPE, Binary, BinaryVector, BinaryVectorDtype, @@ -898,6 +900,50 @@ def test_uuid_legacy(self): transformed = bin.as_uuid(UuidRepresentation.PYTHON_LEGACY) self.assertEqual(id, transformed) + def test_short_uuid_subtype_binary(self): + # PYTHON-6172: the C and pure-Python decoders must agree on binary + # elements with UUID subtypes (3 and 4) whose payload is not 16 + # bytes: decode to Binary under the default UNSPECIFIED uuid + # representation, and raise only when UUID conversion is requested. + regression_file = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + os.pardir, + "fuzz", + "regression", + "PYTHON-6172.bson", + ) + with open(regression_file, "rb") as fp: + data = fp.read() + self.assertEqual(data, encode({"": Binary(b"", UUID_SUBTYPE)})) + + # Default UNSPECIFIED representation: decode to Binary. + for subtype in (OLD_UUID_SUBTYPE, UUID_SUBTYPE): + doc = {"x": Binary(b"", subtype)} + payload = encode(doc) + self.assertEqual(decode(payload), doc) + self.assertEqual(decode_all(payload), [doc]) + self.assertTrue(is_valid(payload)) + self.assertEqual(decode_all(data), [{"": Binary(b"", UUID_SUBTYPE)}]) + + # Conversion to uuid.UUID requested: raise InvalidBSON. + standard = CodecOptions(uuid_representation=UuidRepresentation.STANDARD) + with self.assertRaisesRegex(InvalidBSON, "16-char"): + decode_all(data, standard) + subtype3_payload = encode({"x": Binary(b"", OLD_UUID_SUBTYPE)}) + for rep in ( + UuidRepresentation.PYTHON_LEGACY, + UuidRepresentation.JAVA_LEGACY, + UuidRepresentation.CSHARP_LEGACY, + ): + opts = CodecOptions(uuid_representation=rep) + with self.assertRaisesRegex(InvalidBSON, "16-char"): + decode_all(subtype3_payload, opts) + # Mismatched subtype/representation pairs do not convert. + self.assertEqual( + decode_all(encode({"x": Binary(b"", UUID_SUBTYPE)}), opts), + [{"x": Binary(b"", UUID_SUBTYPE)}], + ) + def test_vector(self): """Tests of subtype 9""" # We start with valid cases, across the 3 dtypes implemented. From f329624de936e2f7621e231923a7f9fd6c0555e0 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 14 Oct 2026 18:52:38 +0000 Subject: [PATCH 2/9] PYTHON-6172 Remove eager 16-byte UUID check from C BSON decoder --- bson/_cbsonmodule.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/bson/_cbsonmodule.c b/bson/_cbsonmodule.c index 40f4d9a68b..7f221f72c9 100644 --- a/bson/_cbsonmodule.c +++ b/bson/_cbsonmodule.c @@ -2351,11 +2351,6 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer, PyObject* binary_value = NULL; char uuid_rep = options->uuid_rep; - /* UUID should always be 16 bytes */ - if (length != 16) { - goto uuiderror; - } - PyObject* subtype_obj = PyLong_FromLong(subtype); if (!subtype_obj) { goto uuiderror; From 6fb9450820d19bfe8017226ec784f0272b35a958 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 14 Oct 2026 18:56:29 +0000 Subject: [PATCH 3/9] PYTHON-6172 Add changelog entry --- doc/changelog.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/changelog.rst b/doc/changelog.rst index cea0a44699..afe906b5e6 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -72,11 +72,18 @@ Bug fixes - Fixed a bug where the pure-Python ``decode_all()`` accepted buffers ending in a partial document instead of raising ``InvalidBSON``, now matching the C extension (`PYTHON-6163`_). +- Fixed a disagreement between the C extension and pure-Python BSON decoders + for binary elements with UUID subtypes (3 and 4) whose payload is not 16 + bytes. Both now decode them to :class:`~bson.binary.Binary` under the + default ``UNSPECIFIED`` UUID representation, and raise + :class:`~bson.errors.InvalidBSON` when UUID conversion is requested + (`PYTHON-6172`_). .. _PYTHON-6074: https://jira.mongodb.org/browse/PYTHON-6074 .. _PYTHON-6040: https://jira.mongodb.org/browse/PYTHON-6040 .. _PYTHON-6111: https://jira.mongodb.org/browse/PYTHON-6111 .. _PYTHON-6163: https://jira.mongodb.org/browse/PYTHON-6163 +.. _PYTHON-6172: https://jira.mongodb.org/browse/PYTHON-6172 Changes in Version 4.18.2 (2026/09/24) -------------------------------------- From 32e2e484af0fe18e41714fb922045c3f4c027a2a Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 14 Oct 2026 14:21:57 -0500 Subject: [PATCH 4/9] PYTHON-6172 Inline regression bytes, trim test prose, drop fuzz corpus file --- test/test_bson.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/test/test_bson.py b/test/test_bson.py index 6991db963a..d7c5159c9a 100644 --- a/test/test_bson.py +++ b/test/test_bson.py @@ -901,19 +901,8 @@ def test_uuid_legacy(self): self.assertEqual(id, transformed) def test_short_uuid_subtype_binary(self): - # PYTHON-6172: the C and pure-Python decoders must agree on binary - # elements with UUID subtypes (3 and 4) whose payload is not 16 - # bytes: decode to Binary under the default UNSPECIFIED uuid - # representation, and raise only when UUID conversion is requested. - regression_file = os.path.join( - os.path.dirname(os.path.realpath(__file__)), - os.pardir, - "fuzz", - "regression", - "PYTHON-6172.bson", - ) - with open(regression_file, "rb") as fp: - data = fp.read() + # PYTHON-6172: both decoders must agree on short UUID payloads. + data = bytes.fromhex("0c0000000500000000000400") self.assertEqual(data, encode({"": Binary(b"", UUID_SUBTYPE)})) # Default UNSPECIFIED representation: decode to Binary. From 0173a461415bbd0873260023dc24e80eb011116e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 14 Oct 2026 14:22:12 -0500 Subject: [PATCH 5/9] PYTHON-6172 Inline regression bytes, trim test prose, drop fuzz corpus file --- fuzz/regression/PYTHON-6172.bson | Bin 12 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 fuzz/regression/PYTHON-6172.bson diff --git a/fuzz/regression/PYTHON-6172.bson b/fuzz/regression/PYTHON-6172.bson deleted file mode 100644 index 4635e81c8b70715df9989d8edfa7b52f9cb2cd0f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12 Rcmd;KU|?VcVg?2l1^@ue02TlM From 1001aefe3711a735de7b446a8363ca8caca820b7 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 14 Oct 2026 14:31:04 -0500 Subject: [PATCH 6/9] PYTHON-6172 Move changelog to 4.20.0, inline regression bytes, drop fuzz corpus file --- doc/changelog.rst | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index afe906b5e6..0c600ade39 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,6 +1,21 @@ Changelog ========= +Changes in Version 4.20.0 (2026/XX/XX) +-------------------------------------- + +Bug fixes +......... + +- Fixed a disagreement between the C extension and pure-Python BSON decoders + for binary elements with UUID subtypes (3 and 4) whose payload is not 16 + bytes. Both now decode them to :class:`~bson.binary.Binary` under the + default ``UNSPECIFIED`` UUID representation, and raise + :class:`~bson.errors.InvalidBSON` when UUID conversion is requested + (`PYTHON-6172`_). + +.. _PYTHON-6172: https://jira.mongodb.org/browse/PYTHON-6172 + Changes in Version 4.19.0 (2026/XX/XX) -------------------------------------- @@ -72,18 +87,11 @@ Bug fixes - Fixed a bug where the pure-Python ``decode_all()`` accepted buffers ending in a partial document instead of raising ``InvalidBSON``, now matching the C extension (`PYTHON-6163`_). -- Fixed a disagreement between the C extension and pure-Python BSON decoders - for binary elements with UUID subtypes (3 and 4) whose payload is not 16 - bytes. Both now decode them to :class:`~bson.binary.Binary` under the - default ``UNSPECIFIED`` UUID representation, and raise - :class:`~bson.errors.InvalidBSON` when UUID conversion is requested - (`PYTHON-6172`_). .. _PYTHON-6074: https://jira.mongodb.org/browse/PYTHON-6074 .. _PYTHON-6040: https://jira.mongodb.org/browse/PYTHON-6040 .. _PYTHON-6111: https://jira.mongodb.org/browse/PYTHON-6111 .. _PYTHON-6163: https://jira.mongodb.org/browse/PYTHON-6163 -.. _PYTHON-6172: https://jira.mongodb.org/browse/PYTHON-6172 Changes in Version 4.18.2 (2026/09/24) -------------------------------------- From 90f004502eb3a3b0731b5ee6a78c588dcd45d27c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 14 Oct 2026 14:36:18 -0500 Subject: [PATCH 7/9] PYTHON-6172 Cover overlong and short UUID payloads in conversion test --- test/test_bson.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/test_bson.py b/test/test_bson.py index d7c5159c9a..571c4555ae 100644 --- a/test/test_bson.py +++ b/test/test_bson.py @@ -914,10 +914,14 @@ def test_short_uuid_subtype_binary(self): self.assertTrue(is_valid(payload)) self.assertEqual(decode_all(data), [{"": Binary(b"", UUID_SUBTYPE)}]) - # Conversion to uuid.UUID requested: raise InvalidBSON. + # Conversion to uuid.UUID requested: raise InvalidBSON unless the + # payload is exactly 16 bytes, overlong included. standard = CodecOptions(uuid_representation=UuidRepresentation.STANDARD) - with self.assertRaisesRegex(InvalidBSON, "16-char"): - decode_all(data, standard) + for length in (0, 15, 17): + payload = encode({"x": Binary(b"x" * length, UUID_SUBTYPE)}) + self.assertTrue(is_valid(payload)) + with self.assertRaisesRegex(InvalidBSON, "16-char"): + decode_all(payload, standard) subtype3_payload = encode({"x": Binary(b"", OLD_UUID_SUBTYPE)}) for rep in ( UuidRepresentation.PYTHON_LEGACY, From 3d45b9af4cba194f75c916bb7e96ec85b967ca62 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 14 Oct 2026 16:17:29 -0500 Subject: [PATCH 8/9] test decode and is_valid for OLD_UUID_SUBTYPE with STANDARD --- test/test_bson.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/test_bson.py b/test/test_bson.py index 571c4555ae..99d6d9af41 100644 --- a/test/test_bson.py +++ b/test/test_bson.py @@ -936,6 +936,14 @@ def test_short_uuid_subtype_binary(self): decode_all(encode({"x": Binary(b"", UUID_SUBTYPE)}), opts), [{"x": Binary(b"", UUID_SUBTYPE)}], ) + # OLD_UUID_SUBTYPE with STANDARD is also mismatched: decode to + # Binary at any payload length, 16 included. + for length in (0, 15, 16, 17): + doc = {"x": Binary(b"x" * length, OLD_UUID_SUBTYPE)} + payload = encode(doc) + self.assertTrue(is_valid(payload)) + self.assertEqual(decode(payload, standard), doc) + self.assertEqual(decode_all(payload, standard), [doc]) def test_vector(self): """Tests of subtype 9""" From 684168fc61a910be64216095ab878a9e0ea30749 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 14 Oct 2026 16:25:48 -0500 Subject: [PATCH 9/9] Reject non-16-byte UUID payloads in Binary.as_uuid --- bson/binary.py | 8 ++++++-- test/test_bson.py | 25 ++++++++++++++++--------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/bson/binary.py b/bson/binary.py index 325f71f605..85f59db527 100644 --- a/bson/binary.py +++ b/bson/binary.py @@ -385,7 +385,7 @@ def as_uuid(self, uuid_representation: int = UuidRepresentation.STANDARD) -> UUI with the provided ``uuid_representation``. Raises :exc:`ValueError` if this :class:`~bson.binary.Binary` instance - does not contain a UUID. + does not contain a UUID, or its payload is not exactly 16 bytes. :param uuid_representation: A member of :class:`~bson.binary.UuidRepresentation`. Default: @@ -404,7 +404,11 @@ def as_uuid(self, uuid_representation: int = UuidRepresentation.STANDARD) -> UUI if uuid_representation == UuidRepresentation.UNSPECIFIED: raise ValueError("uuid_representation cannot be UNSPECIFIED") - elif uuid_representation == UuidRepresentation.PYTHON_LEGACY: + + if len(self) != 16: + raise ValueError(f"cannot decode a uuid from a {len(self)}-byte payload, expected 16") + + if uuid_representation == UuidRepresentation.PYTHON_LEGACY: if self.subtype == OLD_UUID_SUBTYPE: return UUID(bytes=self) elif uuid_representation == UuidRepresentation.JAVA_LEGACY: diff --git a/test/test_bson.py b/test/test_bson.py index 99d6d9af41..d07a9b4bb6 100644 --- a/test/test_bson.py +++ b/test/test_bson.py @@ -915,23 +915,30 @@ def test_short_uuid_subtype_binary(self): self.assertEqual(decode_all(data), [{"": Binary(b"", UUID_SUBTYPE)}]) # Conversion to uuid.UUID requested: raise InvalidBSON unless the - # payload is exactly 16 bytes, overlong included. + # payload is exactly 16 bytes, overlong included. JAVA_LEGACY must + # not silently truncate non-16-byte payloads. standard = CodecOptions(uuid_representation=UuidRepresentation.STANDARD) + converting = [(UUID_SUBTYPE, standard)] + [ + (OLD_UUID_SUBTYPE, CodecOptions(uuid_representation=rep)) + for rep in ( + UuidRepresentation.PYTHON_LEGACY, + UuidRepresentation.JAVA_LEGACY, + UuidRepresentation.CSHARP_LEGACY, + ) + ] for length in (0, 15, 17): - payload = encode({"x": Binary(b"x" * length, UUID_SUBTYPE)}) - self.assertTrue(is_valid(payload)) - with self.assertRaisesRegex(InvalidBSON, "16-char"): - decode_all(payload, standard) - subtype3_payload = encode({"x": Binary(b"", OLD_UUID_SUBTYPE)}) + for subtype, opts in converting: + payload = encode({"x": Binary(b"x" * length, subtype)}) + self.assertTrue(is_valid(payload)) + with self.assertRaisesRegex(InvalidBSON, "expected 16"): + decode_all(payload, opts) + # Mismatched subtype/representation pairs do not convert. for rep in ( UuidRepresentation.PYTHON_LEGACY, UuidRepresentation.JAVA_LEGACY, UuidRepresentation.CSHARP_LEGACY, ): opts = CodecOptions(uuid_representation=rep) - with self.assertRaisesRegex(InvalidBSON, "16-char"): - decode_all(subtype3_payload, opts) - # Mismatched subtype/representation pairs do not convert. self.assertEqual( decode_all(encode({"x": Binary(b"", UUID_SUBTYPE)}), opts), [{"x": Binary(b"", UUID_SUBTYPE)}],