Version in base suite: 2.0.0-1 Version in overlay suite: 2.0.0-1+deb13u1 Base version: php-mongodb_2.0.0-1+deb13u1 Target version: php-mongodb_2.0.0-1+deb13u2 Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/php-mongodb/php-mongodb_2.0.0-1+deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/p/php-mongodb/php-mongodb_2.0.0-1+deb13u2.dsc changelog | 12 patches/0008-PHPC-2741-Fix-potential-issue.patch | 487 ++++++++++ patches/0009-PHPC-2739-Fix-potential-issue.patch | 394 ++++++++ patches/0010-PHPC-276-Prevent-stack-exhaustion-when-decoding-deep.patch | 259 +++++ patches/0011-PHPC-276-Enforce-the-decode-nesting-limit-for-CodeWS.patch | 336 ++++++ patches/series | 4 6 files changed, 1492 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp2k4xj_8o/php-mongodb_2.0.0-1+deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp2k4xj_8o/php-mongodb_2.0.0-1+deb13u2.dsc: no acceptable signature found diff -Nru php-mongodb-2.0.0/debian/changelog php-mongodb-2.0.0/debian/changelog --- php-mongodb-2.0.0/debian/changelog 2026-10-01 06:25:18.000000000 +0000 +++ php-mongodb-2.0.0/debian/changelog 2026-10-25 18:36:57.000000000 +0000 @@ -1,3 +1,15 @@ +php-mongodb (2.0.0-1+deb13u2) trixie-security; urgency=high + + * Backported from 2.1.11: + + [CVE-2026-106432]: Bound-check string and binary lengths in BSON + encoder + + [CVE-2026-106436]: Check libbson append return values in BSON encoder + * Backported from 2.1.12: + + [CVE-2026-112368]: Prevent stack exhaustion when decoding deeply + nested BSON + + -- Ondřej Surý Sun, 25 Oct 2026 19:36:57 +0100 + php-mongodb (2.0.0-1+deb13u1) trixie-security; urgency=high * PHPC-2744: Fix out-of-bounds read of field path diff -Nru php-mongodb-2.0.0/debian/patches/0008-PHPC-2741-Fix-potential-issue.patch php-mongodb-2.0.0/debian/patches/0008-PHPC-2741-Fix-potential-issue.patch --- php-mongodb-2.0.0/debian/patches/0008-PHPC-2741-Fix-potential-issue.patch 1970-01-01 00:00:00.000000000 +0000 +++ php-mongodb-2.0.0/debian/patches/0008-PHPC-2741-Fix-potential-issue.patch 2026-10-25 18:36:57.000000000 +0000 @@ -0,0 +1,487 @@ +From 7a592c262b2758e8cf84dd9b43a3cedb0ebf0f47 Mon Sep 17 00:00:00 2001 +From: Pauline Vos +Date: Thu, 8 Oct 2026 16:04:11 +0200 +Subject: [PATCH] PHPC-2741: Fix potential issue + +--- + src/BSON/Binary.c | 7 ++- + src/BSON/DBPointer.c | 5 ++ + src/BSON/Javascript.c | 5 ++ + src/BSON/Regex.c | 10 ++++ + src/BSON/Symbol.c | 5 ++ + src/MongoDB/BulkWrite.c | 5 ++ + src/MongoDB/Query.c | 5 ++ + src/phongo_bson.h | 6 ++ + src/phongo_bson_encode.c | 57 ++++++++++++++++++- + .../bson-encode-max-length_error-001.phpt | 45 +++++++++++++++ + .../bson-encode-max-length_error-002.phpt | 46 +++++++++++++++ + .../bson-encode-max-length_error-003.phpt | 36 ++++++++++++ + .../bson-encode-max-length_error-004.phpt | 30 ++++++++++ + tests/utils/skipif.php | 40 +++++++++++++ + 14 files changed, 298 insertions(+), 4 deletions(-) + create mode 100644 tests/bson/bson-encode-max-length_error-001.phpt + create mode 100644 tests/bson/bson-encode-max-length_error-002.phpt + create mode 100644 tests/bson/bson-encode-max-length_error-003.phpt + create mode 100644 tests/bson/bson-encode-max-length_error-004.phpt + +--- a/mongodb-2.0.0/src/BSON/Binary.c ++++ b/mongodb-2.0.0/src/BSON/Binary.c +@@ -35,8 +35,13 @@ + return false; + } + ++ if (data_len > PHONGO_BSON_MAX_LENGTH) { ++ phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Expected data length to be <= %" PRId32 " bytes, %zu given", (int32_t) PHONGO_BSON_MAX_LENGTH, data_len); ++ return false; ++ } ++ + if ((type == BSON_SUBTYPE_UUID_DEPRECATED || type == BSON_SUBTYPE_UUID) && data_len != PHONGO_BINARY_UUID_SIZE) { +- phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Expected UUID length to be %d bytes, %d given", PHONGO_BINARY_UUID_SIZE, data_len); ++ phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Expected UUID length to be %d bytes, %zu given", PHONGO_BINARY_UUID_SIZE, data_len); + return false; + } + +--- a/mongodb-2.0.0/src/BSON/DBPointer.c ++++ b/mongodb-2.0.0/src/BSON/DBPointer.c +@@ -29,6 +29,11 @@ + * be thrown on error. */ + static bool php_phongo_dbpointer_init(php_phongo_dbpointer_t* intern, const char* ref, size_t ref_len, const char* id, size_t id_len) + { ++ if (ref_len > PHONGO_BSON_MAX_LENGTH) { ++ phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Expected ref length to be <= %" PRId32 " bytes, %zu given", (int32_t) PHONGO_BSON_MAX_LENGTH, ref_len); ++ return false; ++ } ++ + if (strlen(ref) != (size_t) ref_len) { + phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Ref cannot contain null bytes"); + return false; +--- a/mongodb-2.0.0/src/BSON/Javascript.c ++++ b/mongodb-2.0.0/src/BSON/Javascript.c +@@ -35,6 +35,11 @@ + return false; + } + ++ if (code_len > PHONGO_BSON_MAX_LENGTH) { ++ phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Expected code length to be <= %" PRId32 " bytes, %zu given", (int32_t) PHONGO_BSON_MAX_LENGTH, code_len); ++ return false; ++ } ++ + if (strlen(code) != (size_t) code_len) { + phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Code cannot contain null bytes"); + return false; +--- a/mongodb-2.0.0/src/BSON/Regex.c ++++ b/mongodb-2.0.0/src/BSON/Regex.c +@@ -38,6 +38,16 @@ + * be thrown on error. */ + static bool php_phongo_regex_init(php_phongo_regex_t* intern, const char* pattern, size_t pattern_len, const char* flags, size_t flags_len) + { ++ if (pattern_len > PHONGO_BSON_MAX_LENGTH) { ++ phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Expected pattern length to be <= %" PRId32 " bytes, %zu given", (int32_t) PHONGO_BSON_MAX_LENGTH, pattern_len); ++ return false; ++ } ++ ++ if (flags_len > PHONGO_BSON_MAX_LENGTH) { ++ phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Expected flags length to be <= %" PRId32 " bytes, %zu given", (int32_t) PHONGO_BSON_MAX_LENGTH, flags_len); ++ return false; ++ } ++ + if (strlen(pattern) != (size_t) pattern_len) { + phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Pattern cannot contain null bytes"); + return false; +--- a/mongodb-2.0.0/src/BSON/Symbol.c ++++ b/mongodb-2.0.0/src/BSON/Symbol.c +@@ -27,6 +27,11 @@ + * be thrown on error. */ + static bool php_phongo_symbol_init(php_phongo_symbol_t* intern, const char* symbol, size_t symbol_len) + { ++ if (symbol_len > PHONGO_BSON_MAX_LENGTH) { ++ phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Expected symbol length to be <= %" PRId32 " bytes, %zu given", (int32_t) PHONGO_BSON_MAX_LENGTH, symbol_len); ++ return false; ++ } ++ + if (strlen(symbol) != (size_t) symbol_len) { + phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Symbol cannot contain null bytes"); + return false; +--- a/mongodb-2.0.0/src/MongoDB/BulkWrite.c ++++ b/mongodb-2.0.0/src/MongoDB/BulkWrite.c +@@ -251,6 +251,11 @@ + if (type == IS_STRING) { + zval* value = php_array_fetchc_deref(zoptions, "hint"); + ++ if (Z_STRLEN_P(value) > PHONGO_BSON_MAX_LENGTH) { ++ phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Expected \"hint\" option to be <= %" PRId32 " bytes, %zu given", (int32_t) PHONGO_BSON_MAX_LENGTH, Z_STRLEN_P(value)); ++ return false; ++ } ++ + if (!bson_append_utf8(boptions, "hint", 4, Z_STRVAL_P(value), Z_STRLEN_P(value))) { + phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Error appending \"hint\" option"); + return false; +--- a/mongodb-2.0.0/src/MongoDB/Query.c ++++ b/mongodb-2.0.0/src/MongoDB/Query.c +@@ -43,6 +43,11 @@ + return false; + } + ++ if (Z_STRLEN_P(value) > PHONGO_BSON_MAX_LENGTH) { ++ phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Expected \"%s\" %s to be <= %" PRId32 " bytes, %zu given", zarr_key, zarr_key[0] == '$' ? "modifier" : "option", (int32_t) PHONGO_BSON_MAX_LENGTH, Z_STRLEN_P(value)); ++ return false; ++ } ++ + if (!bson_append_utf8(opts, opts_key, strlen(opts_key), Z_STRVAL_P(value), Z_STRLEN_P(value))) { + phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Error appending \"%s\" option", opts_key); + return false; +--- a/mongodb-2.0.0/src/phongo_bson.h ++++ b/mongodb-2.0.0/src/phongo_bson.h +@@ -26,6 +26,12 @@ + #define PHONGO_ODM_FIELD_NAME "__pclass" + #define BSON_MAX_NESTING_LEVEL 100 + ++/* Maximum length for a BSON string or binary value. BSON encodes these lengths ++ * as int32, so anything larger cannot be represented on the wire and would be ++ * silently truncated (or wrap an allocation) by the narrowing casts in the ++ * encoder. */ ++#define PHONGO_BSON_MAX_LENGTH INT32_MAX ++ + typedef enum { + PHONGO_FIELD_PATH_ITEM_NONE, + PHONGO_FIELD_PATH_ITEM_ARRAY, +--- a/mongodb-2.0.0/src/phongo_bson_encode.c ++++ b/mongodb-2.0.0/src/phongo_bson_encode.c +@@ -44,9 +44,27 @@ + #endif + + /* Forwards declarations */ +-static void php_phongo_bson_append(bson_t* bson, php_phongo_field_path* field_path, php_phongo_bson_flags_t flags, const char* key, long key_len, zval* entry); ++static void php_phongo_bson_append(bson_t* bson, php_phongo_field_path* field_path, php_phongo_bson_flags_t flags, const char* key, size_t key_len, zval* entry); + static void php_phongo_zval_to_bson_internal(zval* data, php_phongo_field_path* field_path, php_phongo_bson_flags_t flags, bson_t* bson, bson_t** bson_out); + ++/* Rejects a string or binary length that BSON cannot represent. The encoder ++ * narrows these lengths to int32 or uint32 when appending, so an unchecked ++ * value would be silently truncated or wrap an allocation size. */ ++static bool phongo_bson_length_is_valid(size_t length, php_phongo_field_path* field_path, const char* description) ++{ ++ char* path_string; ++ ++ if (length <= PHONGO_BSON_MAX_LENGTH) { ++ return true; ++ } ++ ++ path_string = php_phongo_field_path_as_string(field_path); ++ phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Expected %s for field path \"%s\" to be <= %" PRId32 " bytes, %zu given", description, path_string, (int32_t) PHONGO_BSON_MAX_LENGTH, length); ++ efree(path_string); ++ ++ return false; ++} ++ + /* Determines whether the argument should be serialized as a BSON array or + * document. IS_ARRAY is returned if the argument's keys are a sequence of + * integers starting at zero; otherwise, IS_OBJECT is returned. */ +@@ -145,7 +163,7 @@ + * type. + * Other array or object values will be appended as an embedded document. + */ +-static void php_phongo_bson_append_object(bson_t* bson, php_phongo_field_path* field_path, php_phongo_bson_flags_t flags, const char* key, long key_len, zval* object) ++static void php_phongo_bson_append_object(bson_t* bson, php_phongo_field_path* field_path, php_phongo_bson_flags_t flags, const char* key, size_t key_len, zval* object) + { + if (Z_TYPE_P(object) == IS_OBJECT && instanceof_function(Z_OBJCE_P(object), php_phongo_type_ce)) { + if (instanceof_function(Z_OBJCE_P(object), php_phongo_document_ce)) { +@@ -183,6 +201,10 @@ + /* Persistable objects must always be serialized as BSON documents; + * otherwise, infer based on bsonSerialize()'s return value. */ + if (instanceof_function(Z_OBJCE_P(object), php_phongo_persistable_ce) || php_phongo_is_array_or_document(&obj_data) != IS_ARRAY) { ++ if (instanceof_function(Z_OBJCE_P(object), php_phongo_persistable_ce) && !phongo_bson_length_is_valid(Z_OBJCE_P(object)->name->len, field_path, "class name")) { ++ zval_ptr_dtor(&obj_data); ++ return; ++ } + bson_append_document_begin(bson, key, key_len, &child); + if (instanceof_function(Z_OBJCE_P(object), php_phongo_persistable_ce)) { + bson_append_binary(&child, PHONGO_ODM_FIELD_NAME, -1, 0x80, (const uint8_t*) Z_OBJCE_P(object)->name->val, Z_OBJCE_P(object)->name->len); +@@ -216,6 +238,10 @@ + if (instanceof_function(Z_OBJCE_P(object), php_phongo_binary_ce)) { + php_phongo_binary_t* intern = Z_BINARY_OBJ_P(object); + ++ if (!phongo_bson_length_is_valid(intern->data_len, field_path, "binary data")) { ++ return; ++ } ++ + bson_append_binary(bson, key, key_len, intern->type, (const uint8_t*) intern->data, (uint32_t) intern->data_len); + return; + } +@@ -274,6 +300,10 @@ + if (instanceof_function(Z_OBJCE_P(object), php_phongo_symbol_ce)) { + php_phongo_symbol_t* intern = Z_SYMBOL_OBJ_P(object); + ++ if (!phongo_bson_length_is_valid(intern->symbol_len, field_path, "symbol")) { ++ return; ++ } ++ + bson_append_symbol(bson, key, key_len, intern->symbol, intern->symbol_len); + return; + } +@@ -310,8 +340,17 @@ + /* Appends the zval argument to the BSON document. If the argument is an object, + * or an array that should be serialized as an embedded document, this function + * will defer to php_phongo_bson_append_object(). */ +-static void php_phongo_bson_append(bson_t* bson, php_phongo_field_path* field_path, php_phongo_bson_flags_t flags, const char* key, long key_len, zval* entry) ++static void php_phongo_bson_append(bson_t* bson, php_phongo_field_path* field_path, php_phongo_bson_flags_t flags, const char* key, size_t key_len, zval* entry) + { ++ /* A key longer than BSON_MAX_SIZE wraps a size_t/long length to a small ++ * non-negative int in the libbson append, silently truncating the field ++ * name to a prefix (SECBUG-4150). Reject before narrowing. The key is not ++ * included in the message or the field path, as it may be gigabytes. */ ++ if (key_len > PHONGO_BSON_MAX_LENGTH) { ++ phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Expected key length to be <= %" PRId32 " bytes, %zu given", (int32_t) PHONGO_BSON_MAX_LENGTH, (size_t) key_len); ++ return; ++ } ++ + php_phongo_field_path_write_item_at_current_level(field_path, key); + + try_again: +@@ -336,6 +375,10 @@ + break; + + case IS_STRING: ++ if (!phongo_bson_length_is_valid(Z_STRLEN_P(entry), field_path, "string")) { ++ break; ++ } ++ + if (bson_utf8_validate(Z_STRVAL_P(entry), Z_STRLEN_P(entry), true)) { + bson_append_utf8(bson, key, key_len, Z_STRVAL_P(entry), Z_STRLEN_P(entry)); + } else { +@@ -501,6 +544,9 @@ + } + + if (instanceof_function(Z_OBJCE_P(data), php_phongo_persistable_ce)) { ++ if (!phongo_bson_length_is_valid(Z_OBJCE_P(data)->name->len, field_path, "class name")) { ++ goto cleanup; ++ } + bson_append_binary(bson, PHONGO_ODM_FIELD_NAME, -1, 0x80, (const uint8_t*) Z_OBJCE_P(data)->name->val, Z_OBJCE_P(data)->name->len); + /* Ensure that we ignore an existing key with the same name + * if one exists in the bsonSerialize() return value. */ +@@ -698,6 +744,11 @@ + return true; + + case IS_STRING: ++ if (Z_STRLEN_P(data) > PHONGO_BSON_MAX_LENGTH) { ++ phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Expected string to be <= %" PRId32 " bytes, %zu given", (int32_t) PHONGO_BSON_MAX_LENGTH, Z_STRLEN_P(data)); ++ return false; ++ } ++ + value->value_type = BSON_TYPE_UTF8; + value->value.v_utf8.len = Z_STRLEN_P(data); + +--- /dev/null ++++ b/mongodb-2.0.0/tests/bson/bson-encode-max-length_error-001.phpt +@@ -0,0 +1,45 @@ ++--TEST-- ++PHPC-2741: Encoding rejects string and binary values larger than INT32_MAX ++--SKIPIF-- ++ ++ ++--INI-- ++memory_limit=-1 ++--FILE-- ++ $tooLong]); ++}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n"; ++ ++echo throws(function() use ($tooLong) { ++ new MongoDB\BSON\Binary($tooLong, MongoDB\BSON\Binary::TYPE_GENERIC); ++}, 'MongoDB\Driver\Exception\InvalidArgumentException'), "\n"; ++ ++echo throws(function() use ($tooLong) { ++ MongoDB\BSON\Symbol::__set_state(['symbol' => $tooLong]); ++}, 'MongoDB\Driver\Exception\InvalidArgumentException'), "\n"; ++ ++echo throws(function() use ($tooLong) { ++ new MongoDB\Driver\Query([], ['comment' => $tooLong]); ++}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n"; ++ ++?> ++===DONE=== ++ ++--EXPECT-- ++OK: Got MongoDB\Driver\Exception\UnexpectedValueException ++Expected string for field path "string" to be <= 2147483647 bytes, 2147483648 given ++OK: Got MongoDB\Driver\Exception\InvalidArgumentException ++Expected data length to be <= 2147483647 bytes, 2147483648 given ++OK: Got MongoDB\Driver\Exception\InvalidArgumentException ++Expected symbol length to be <= 2147483647 bytes, 2147483648 given ++OK: Got MongoDB\Driver\Exception\UnexpectedValueException ++Expected string to be <= 2147483647 bytes, 2147483648 given ++===DONE=== +--- /dev/null ++++ b/mongodb-2.0.0/tests/bson/bson-encode-max-length_error-002.phpt +@@ -0,0 +1,46 @@ ++--TEST-- ++PHPC-2741: Javascript, Regex and DBPointer reject values larger than INT32_MAX ++--SKIPIF-- ++ ++ ++--INI-- ++memory_limit=-1 ++--FILE-- ++ $tooLong, 'id' => '000000000000000000000000']); ++}, 'MongoDB\Driver\Exception\InvalidArgumentException'), "\n"; ++ ++?> ++===DONE=== ++ ++--EXPECT-- ++OK: Got MongoDB\Driver\Exception\InvalidArgumentException ++Expected code length to be <= 2147483647 bytes, 2147483648 given ++OK: Got MongoDB\Driver\Exception\InvalidArgumentException ++Expected pattern length to be <= 2147483647 bytes, 2147483648 given ++OK: Got MongoDB\Driver\Exception\InvalidArgumentException ++Expected flags length to be <= 2147483647 bytes, 2147483648 given ++OK: Got MongoDB\Driver\Exception\InvalidArgumentException ++Expected ref length to be <= 2147483647 bytes, 2147483648 given ++===DONE=== +--- /dev/null ++++ b/mongodb-2.0.0/tests/bson/bson-encode-max-length_error-003.phpt +@@ -0,0 +1,36 @@ ++--TEST-- ++PHPC-2741: String hint option rejects values larger than INT32_MAX ++--SKIPIF-- ++ ++ ++--INI-- ++memory_limit=-1 ++--FILE-- ++ $tooLong]); ++}, 'MongoDB\Driver\Exception\InvalidArgumentException'), "\n"; ++ ++echo throws(function() use ($tooLong) { ++ $bulk = new MongoDB\Driver\BulkWrite(); ++ $bulk->update(['x' => 1], ['$set' => ['x' => 2]], ['hint' => $tooLong]); ++}, 'MongoDB\Driver\Exception\InvalidArgumentException'), "\n"; ++ ++?> ++===DONE=== ++ ++--EXPECT-- ++OK: Got MongoDB\Driver\Exception\InvalidArgumentException ++Expected "hint" option to be <= 2147483647 bytes, 2147483648 given ++OK: Got MongoDB\Driver\Exception\InvalidArgumentException ++Expected "hint" option to be <= 2147483647 bytes, 2147483648 given ++===DONE=== +--- /dev/null ++++ b/mongodb-2.0.0/tests/bson/bson-encode-max-length_error-004.phpt +@@ -0,0 +1,30 @@ ++--TEST-- ++PHPC-2741: Encoding rejects field keys larger than INT32_MAX ++--SKIPIF-- ++ ++ ++--INI-- ++memory_limit=-1 ++--FILE-- ++ 1]); ++}, 'MongoDB\Driver\Exception\UnexpectedValueException'), "\n"; ++ ++?> ++===DONE=== ++ ++--EXPECT-- ++OK: Got MongoDB\Driver\Exception\UnexpectedValueException ++Expected key length to be <= 2147483647 bytes, 2147483648 given ++===DONE=== +--- a/mongodb-2.0.0/tests/utils/skipif.php ++++ b/mongodb-2.0.0/tests/utils/skipif.php +@@ -490,3 +490,43 @@ + exit('skip crypt_shared is not available'); + } + } ++ ++/** ++ * Returns the memory the process can plausibly allocate, in bytes, or null when ++ * it cannot be determined. Only Linux is handled: the platforms these tests run ++ * on outside CI expose no cheap equivalent, so the tests skip there. ++ */ ++function get_available_memory() ++{ ++ if (is_readable('/proc/meminfo') && preg_match('/^MemAvailable:\s+(\d+) kB$/m', file_get_contents('/proc/meminfo'), $matches)) { ++ return (int) $matches[1] * 1024; ++ } ++ ++ return null; ++} ++ ++/** ++ * Skips the test unless the process can plausibly allocate the given number of ++ * bytes. Tests exercising the 2 GiB BSON length limit need multi-gigabyte ++ * strings, which many CI hosts cannot supply. ++ * ++ * @param int $requiredBytes Bytes the test needs to allocate ++ */ ++function skip_if_not_enough_memory($requiredBytes) ++{ ++ if (PHP_INT_SIZE !== 8) { ++ exit('skip Allocating more than 2 GiB requires a 64-bit platform'); ++ } ++ ++ $availableBytes = get_available_memory(); ++ ++ if ($availableBytes === null) { ++ exit('skip Cannot determine available memory on this platform'); ++ } ++ ++ /* Require headroom beyond the payload itself, since the driver copies the ++ * string while encoding and the process needs room for everything else. */ ++ if ($availableBytes < $requiredBytes * 2) { ++ exit(sprintf('skip Requires %d bytes of available memory, %d available', $requiredBytes * 2, $availableBytes)); ++ } ++} diff -Nru php-mongodb-2.0.0/debian/patches/0009-PHPC-2739-Fix-potential-issue.patch php-mongodb-2.0.0/debian/patches/0009-PHPC-2739-Fix-potential-issue.patch --- php-mongodb-2.0.0/debian/patches/0009-PHPC-2739-Fix-potential-issue.patch 1970-01-01 00:00:00.000000000 +0000 +++ php-mongodb-2.0.0/debian/patches/0009-PHPC-2739-Fix-potential-issue.patch 2026-10-25 18:36:57.000000000 +0000 @@ -0,0 +1,394 @@ +From b9172b4f4c0d49dd446d157ef554c6f5e2d6381a Mon Sep 17 00:00:00 2001 +From: Pauline Vos +Date: Thu, 8 Oct 2026 16:07:37 +0200 +Subject: [PATCH] PHPC-2739: Fix potential issue + +--- + src/phongo_bson_encode.c | 164 ++++++++++++++++++++++++++++----------- + 1 file changed, 118 insertions(+), 46 deletions(-) + +--- a/mongodb-2.0.0/src/phongo_bson_encode.c ++++ b/mongodb-2.0.0/src/phongo_bson_encode.c +@@ -30,12 +30,8 @@ + #define MONGOC_LOG_DOMAIN "PHONGO-BSON" + + #if SIZEOF_ZEND_LONG == 8 +-#define BSON_APPEND_INT(b, key, keylen, val) \ +- if (val > INT32_MAX || val < INT32_MIN) { \ +- bson_append_int64(b, key, keylen, val); \ +- } else { \ +- bson_append_int32(b, key, keylen, val); \ +- } ++#define BSON_APPEND_INT(b, key, keylen, val) \ ++ (val > INT32_MAX || val < INT32_MIN ? bson_append_int64(b, key, keylen, val) : bson_append_int32(b, key, keylen, val)) + #elif SIZEOF_ZEND_LONG == 4 + #define BSON_APPEND_INT(b, key, keylen, val) \ + bson_append_int32(b, key, keylen, val) +@@ -65,6 +61,23 @@ + return false; + } + ++/* Throws if a libbson append call failed. libbson rejects an append when the ++ * resulting document would exceed the maximum BSON size, when a key contains ++ * an embedded NUL, or when regex options contain invalid characters. Does ++ * nothing if an exception is already pending. */ ++static void phongo_bson_append_check(bool append_ok, php_phongo_field_path* field_path) ++{ ++ char* path_string; ++ ++ if (append_ok || EG(exception)) { ++ return; ++ } ++ ++ path_string = php_phongo_field_path_as_string(field_path); ++ phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Could not append BSON data for field path \"%s\".", path_string); ++ efree(path_string); ++} ++ + /* Determines whether the argument should be serialized as a BSON array or + * document. IS_ARRAY is returned if the argument's keys are a sequence of + * integers starting at zero; otherwise, IS_OBJECT is returned. */ +@@ -168,14 +181,14 @@ + if (Z_TYPE_P(object) == IS_OBJECT && instanceof_function(Z_OBJCE_P(object), php_phongo_type_ce)) { + if (instanceof_function(Z_OBJCE_P(object), php_phongo_document_ce)) { + php_phongo_document_t* intern = Z_DOCUMENT_OBJ_P(object); +- bson_append_document(bson, key, key_len, intern->bson); ++ phongo_bson_append_check(bson_append_document(bson, key, key_len, intern->bson), field_path); + + return; + } + + if (instanceof_function(Z_OBJCE_P(object), php_phongo_packedarray_ce)) { + php_phongo_packedarray_t* intern = Z_PACKEDARRAY_OBJ_P(object); +- bson_append_array(bson, key, key_len, intern->bson); ++ phongo_bson_append_check(bson_append_array(bson, key, key_len, intern->bson), field_path); + + return; + } +@@ -205,16 +218,26 @@ + zval_ptr_dtor(&obj_data); + return; + } +- bson_append_document_begin(bson, key, key_len, &child); ++ /* If the append fails, the child is not initialized and must not ++ * be used. */ ++ if (!bson_append_document_begin(bson, key, key_len, &child)) { ++ phongo_bson_append_check(false, field_path); ++ zval_ptr_dtor(&obj_data); ++ return; ++ } + if (instanceof_function(Z_OBJCE_P(object), php_phongo_persistable_ce)) { +- bson_append_binary(&child, PHONGO_ODM_FIELD_NAME, -1, 0x80, (const uint8_t*) Z_OBJCE_P(object)->name->val, Z_OBJCE_P(object)->name->len); ++ phongo_bson_append_check(bson_append_binary(&child, PHONGO_ODM_FIELD_NAME, -1, 0x80, (const uint8_t*) Z_OBJCE_P(object)->name->val, Z_OBJCE_P(object)->name->len), field_path); + } + php_phongo_zval_to_bson_internal(&obj_data, field_path, flags, &child, NULL); +- bson_append_document_end(bson, &child); ++ phongo_bson_append_check(bson_append_document_end(bson, &child), field_path); + } else { +- bson_append_array_begin(bson, key, key_len, &child); ++ if (!bson_append_array_begin(bson, key, key_len, &child)) { ++ phongo_bson_append_check(false, field_path); ++ zval_ptr_dtor(&obj_data); ++ return; ++ } + php_phongo_zval_to_bson_internal(&obj_data, field_path, flags, &child, NULL); +- bson_append_array_end(bson, &child); ++ phongo_bson_append_check(bson_append_array_end(bson, &child), field_path); + } + + zval_ptr_dtor(&obj_data); +@@ -226,13 +249,13 @@ + php_phongo_objectid_t* intern = Z_OBJECTID_OBJ_P(object); + + bson_oid_init_from_string(&oid, intern->oid); +- bson_append_oid(bson, key, key_len, &oid); ++ phongo_bson_append_check(bson_append_oid(bson, key, key_len, &oid), field_path); + return; + } + if (instanceof_function(Z_OBJCE_P(object), php_phongo_utcdatetime_ce)) { + php_phongo_utcdatetime_t* intern = Z_UTCDATETIME_OBJ_P(object); + +- bson_append_date_time(bson, key, key_len, intern->milliseconds); ++ phongo_bson_append_check(bson_append_date_time(bson, key, key_len, intern->milliseconds), field_path); + return; + } + if (instanceof_function(Z_OBJCE_P(object), php_phongo_binary_ce)) { +@@ -242,49 +265,49 @@ + return; + } + +- bson_append_binary(bson, key, key_len, intern->type, (const uint8_t*) intern->data, (uint32_t) intern->data_len); ++ phongo_bson_append_check(bson_append_binary(bson, key, key_len, intern->type, (const uint8_t*) intern->data, (uint32_t) intern->data_len), field_path); + return; + } + if (instanceof_function(Z_OBJCE_P(object), php_phongo_decimal128_ce)) { + php_phongo_decimal128_t* intern = Z_DECIMAL128_OBJ_P(object); + +- bson_append_decimal128(bson, key, key_len, &intern->decimal); ++ phongo_bson_append_check(bson_append_decimal128(bson, key, key_len, &intern->decimal), field_path); + return; + } + if (instanceof_function(Z_OBJCE_P(object), php_phongo_int64_ce)) { + php_phongo_int64_t* intern = Z_INT64_OBJ_P(object); + +- bson_append_int64(bson, key, key_len, intern->integer); ++ phongo_bson_append_check(bson_append_int64(bson, key, key_len, intern->integer), field_path); + return; + } + if (instanceof_function(Z_OBJCE_P(object), php_phongo_regex_ce)) { + php_phongo_regex_t* intern = Z_REGEX_OBJ_P(object); + +- bson_append_regex(bson, key, key_len, intern->pattern, intern->flags); ++ phongo_bson_append_check(bson_append_regex(bson, key, key_len, intern->pattern, intern->flags), field_path); + return; + } + if (instanceof_function(Z_OBJCE_P(object), php_phongo_javascript_ce)) { + php_phongo_javascript_t* intern = Z_JAVASCRIPT_OBJ_P(object); + + if (intern->scope) { +- bson_append_code_with_scope(bson, key, key_len, intern->code, intern->scope); ++ phongo_bson_append_check(bson_append_code_with_scope(bson, key, key_len, intern->code, intern->scope), field_path); + } else { +- bson_append_code(bson, key, key_len, intern->code); ++ phongo_bson_append_check(bson_append_code(bson, key, key_len, intern->code), field_path); + } + return; + } + if (instanceof_function(Z_OBJCE_P(object), php_phongo_timestamp_ce)) { + php_phongo_timestamp_t* intern = Z_TIMESTAMP_OBJ_P(object); + +- bson_append_timestamp(bson, key, key_len, intern->timestamp, intern->increment); ++ phongo_bson_append_check(bson_append_timestamp(bson, key, key_len, intern->timestamp, intern->increment), field_path); + return; + } + if (instanceof_function(Z_OBJCE_P(object), php_phongo_maxkey_ce)) { +- bson_append_maxkey(bson, key, key_len); ++ phongo_bson_append_check(bson_append_maxkey(bson, key, key_len), field_path); + return; + } + if (instanceof_function(Z_OBJCE_P(object), php_phongo_minkey_ce)) { +- bson_append_minkey(bson, key, key_len); ++ phongo_bson_append_check(bson_append_minkey(bson, key, key_len), field_path); + return; + } + +@@ -294,7 +317,7 @@ + php_phongo_dbpointer_t* intern = Z_DBPOINTER_OBJ_P(object); + + bson_oid_init_from_string(&oid, intern->id); +- bson_append_dbpointer(bson, key, key_len, intern->ref, &oid); ++ phongo_bson_append_check(bson_append_dbpointer(bson, key, key_len, intern->ref, &oid), field_path); + return; + } + if (instanceof_function(Z_OBJCE_P(object), php_phongo_symbol_ce)) { +@@ -304,11 +327,11 @@ + return; + } + +- bson_append_symbol(bson, key, key_len, intern->symbol, intern->symbol_len); ++ phongo_bson_append_check(bson_append_symbol(bson, key, key_len, intern->symbol, intern->symbol_len), field_path); + return; + } + if (instanceof_function(Z_OBJCE_P(object), php_phongo_undefined_ce)) { +- bson_append_undefined(bson, key, key_len); ++ phongo_bson_append_check(bson_append_undefined(bson, key, key_len), field_path); + return; + } + +@@ -331,9 +354,15 @@ + { + bson_t child; + +- bson_append_document_begin(bson, key, key_len, &child); ++ /* If the append fails, the child is not initialized and must not be ++ * used. */ ++ if (!bson_append_document_begin(bson, key, key_len, &child)) { ++ phongo_bson_append_check(false, field_path); ++ return; ++ } ++ + php_phongo_zval_to_bson_internal(object, field_path, flags, &child, NULL); +- bson_append_document_end(bson, &child); ++ phongo_bson_append_check(bson_append_document_end(bson, &child), field_path); + } + } + +@@ -356,22 +385,22 @@ + try_again: + switch (Z_TYPE_P(entry)) { + case IS_NULL: +- bson_append_null(bson, key, key_len); ++ phongo_bson_append_check(bson_append_null(bson, key, key_len), field_path); + break; + case IS_TRUE: +- bson_append_bool(bson, key, key_len, true); ++ phongo_bson_append_check(bson_append_bool(bson, key, key_len, true), field_path); + break; + + case IS_FALSE: +- bson_append_bool(bson, key, key_len, false); ++ phongo_bson_append_check(bson_append_bool(bson, key, key_len, false), field_path); + break; + + case IS_LONG: +- BSON_APPEND_INT(bson, key, key_len, Z_LVAL_P(entry)); ++ phongo_bson_append_check(BSON_APPEND_INT(bson, key, key_len, Z_LVAL_P(entry)), field_path); + break; + + case IS_DOUBLE: +- bson_append_double(bson, key, key_len, Z_DVAL_P(entry)); ++ phongo_bson_append_check(bson_append_double(bson, key, key_len, Z_DVAL_P(entry)), field_path); + break; + + case IS_STRING: +@@ -380,7 +409,7 @@ + } + + if (bson_utf8_validate(Z_STRVAL_P(entry), Z_STRLEN_P(entry), true)) { +- bson_append_utf8(bson, key, key_len, Z_STRVAL_P(entry), Z_STRLEN_P(entry)); ++ phongo_bson_append_check(bson_append_utf8(bson, key, key_len, Z_STRVAL_P(entry), Z_STRLEN_P(entry)), field_path); + } else { + char* path_string = php_phongo_field_path_as_string(field_path); + phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Detected invalid UTF-8 for field path \"%s\": %s", path_string, Z_STRVAL_P(entry)); +@@ -406,10 +435,18 @@ + break; + } + +- bson_append_array_begin(bson, key, key_len, &child); ++ /* If the append fails, the child is not initialized and must ++ * not be used. */ ++ if (!bson_append_array_begin(bson, key, key_len, &child)) { ++ phongo_bson_append_check(false, field_path); ++ php_phongo_field_path_pop(field_path); ++ php_phongo_zend_hash_apply_protection_end(tmp_ht); ++ break; ++ } ++ + php_phongo_zval_to_bson_internal(entry, field_path, flags, &child, NULL); + php_phongo_field_path_pop(field_path); +- bson_append_array_end(bson, &child); ++ phongo_bson_append_check(bson_append_array_end(bson, &child), field_path); + + php_phongo_zend_hash_apply_protection_end(tmp_ht); + break; +@@ -468,8 +505,8 @@ + if (bson_iter_init(&iter, src)) { + while (bson_iter_next(&iter)) { + if (!bson_append_iter(dst, NULL, 0, &iter)) { +- /* This should not be able to happen since we are copying from +- * within a valid bson_t. */ ++ /* The source is a valid bson_t, so this only happens when the ++ * destination would exceed the maximum BSON size. */ + phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Error copying \"%s\" field from source document", bson_iter_key(&iter)); + return; + } +@@ -502,6 +539,10 @@ + + phongo_bson_copy_to_noinit(intern->bson, bson); + ++ if (EG(exception)) { ++ goto cleanup; ++ } ++ + // Check if the document instance already has an _id field + if (flags & PHONGO_BSON_ADD_ID && bson_iter_init_find(&iter, bson, "_id")) { + flags &= ~PHONGO_BSON_ADD_ID; +@@ -524,6 +565,10 @@ + + phongo_bson_copy_to_noinit(intern->bson, bson); + ++ if (EG(exception)) { ++ goto cleanup; ++ } ++ + goto done; + } + +@@ -547,7 +592,12 @@ + if (!phongo_bson_length_is_valid(Z_OBJCE_P(data)->name->len, field_path, "class name")) { + goto cleanup; + } +- bson_append_binary(bson, PHONGO_ODM_FIELD_NAME, -1, 0x80, (const uint8_t*) Z_OBJCE_P(data)->name->val, Z_OBJCE_P(data)->name->len); ++ if (!bson_append_binary(bson, PHONGO_ODM_FIELD_NAME, -1, 0x80, (const uint8_t*) Z_OBJCE_P(data)->name->val, Z_OBJCE_P(data)->name->len)) { ++ phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Could not append \"%s\" field to encoded document. The document may exceed the maximum BSON size of %zu bytes.", PHONGO_ODM_FIELD_NAME, (size_t) BSON_MAX_SIZE); ++ ++ goto cleanup; ++ } ++ + /* Ensure that we ignore an existing key with the same name + * if one exists in the bsonSerialize() return value. */ + skip_odm_field = true; +@@ -632,6 +682,12 @@ + php_phongo_bson_append(bson, field_path, flags & ~PHONGO_BSON_ADD_ID, ZSTR_VAL(string_key), strlen(ZSTR_VAL(string_key)), value); + + zend_string_release(string_key); ++ ++ /* Stop encoding as soon as a field could not be appended. ++ * Continuing would only throw again for each remaining field. */ ++ if (EG(exception)) { ++ goto cleanup; ++ } + } + ZEND_HASH_FOREACH_END(); + } +@@ -641,7 +697,12 @@ + bson_oid_t oid; + + bson_oid_init(&oid, NULL); +- bson_append_oid(bson, "_id", strlen("_id"), &oid); ++ ++ if (!bson_append_oid(bson, "_id", strlen("_id"), &oid)) { ++ phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Could not append \"_id\" field to encoded document. The document may exceed the maximum BSON size of %zu bytes.", (size_t) BSON_MAX_SIZE); ++ ++ goto cleanup; ++ } + } + + if (flags & PHONGO_BSON_RETURN_ID && bson_out) { +@@ -676,8 +737,9 @@ + php_phongo_field_path_free(field_path); + } + +-static void phongo_zval_to_bson_value_ex(zval* data, php_phongo_bson_flags_t flags, bson_value_t* value) ++static bool phongo_zval_to_bson_value_ex(zval* data, php_phongo_bson_flags_t flags, bson_value_t* value) + { ++ bool found = false; + bson_iter_t iter; + bson_t bson = BSON_INITIALIZER; + zval data_object; +@@ -689,12 +751,23 @@ + + php_phongo_zval_to_bson(&data_object, flags, &bson, NULL); + +- if (bson_iter_init_find(&iter, &bson, "data")) { +- bson_value_copy(bson_iter_value(&iter), value); ++ /* If the encoder threw, the value was never appended and must be left ++ * untouched so the caller does not read uninitialized memory. */ ++ if (!EG(exception)) { ++ if (bson_iter_init_find(&iter, &bson, "data")) { ++ bson_value_copy(bson_iter_value(&iter), value); ++ found = true; ++ } else { ++ /* Callers rely on an exception being thrown, so make sure one is ++ * pending even if the encoder failed without throwing. */ ++ phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Could not convert value to BSON"); ++ } + } + + bson_destroy(&bson); + zval_ptr_dtor(&data_object); ++ ++ return found; + } + + /* Converts the argument to a bson_value_t. If the object is an instance of +@@ -761,8 +834,7 @@ + case IS_ARRAY: + case IS_OBJECT: + /* Use php_phongo_zval_to_bson internally to convert arrays and documents */ +- phongo_zval_to_bson_value_ex(data, PHONGO_BSON_NONE, value); +- return true; ++ return phongo_zval_to_bson_value_ex(data, PHONGO_BSON_NONE, value); + } + + phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Unsupported type %s", zend_zval_type_name(data)); diff -Nru php-mongodb-2.0.0/debian/patches/0010-PHPC-276-Prevent-stack-exhaustion-when-decoding-deep.patch php-mongodb-2.0.0/debian/patches/0010-PHPC-276-Prevent-stack-exhaustion-when-decoding-deep.patch --- php-mongodb-2.0.0/debian/patches/0010-PHPC-276-Prevent-stack-exhaustion-when-decoding-deep.patch 1970-01-01 00:00:00.000000000 +0000 +++ php-mongodb-2.0.0/debian/patches/0010-PHPC-276-Prevent-stack-exhaustion-when-decoding-deep.patch 2026-10-25 18:36:57.000000000 +0000 @@ -0,0 +1,259 @@ +From 01363996a93b33fea58c90f5e8d1eaea5e01eafd Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= + +Date: Tue, 13 Oct 2026 23:27:45 +0200 +Subject: [PATCH] PHPC-276: Prevent stack exhaustion when decoding deeply + nested BSON (#2158) + +* PHPC-276: Prevent stack exhaustion when decoding deeply nested BSON + +Deeply nested BSON documents made the decoder recurse in C until the stack +was exhausted and the process crashed with SIGSEGV. The decode path now +throws an UnexpectedValueException once the nesting limit is exceeded, +mirroring the check the encoder already has (PHPC-2636). + +The decoding limit is a new constant, BSON_MAX_DECODE_NESTING_LEVEL, set to +500. It is higher than the encoding limit (BSON_MAX_NESTING_LEVEL, 100) +because the server stores documents nested up to 180 levels, so the driver +must be able to decode documents deeper than it can encode. The value mirrors +libbson's BSON_VALIDATION_MAX_NESTING_DEPTH and stays well below the depth at +which the C recursion exhausts the stack. + +The limit is checked just before recursing rather than when pushing the field +path, so raw BSON sub-documents, which are not traversed, do not count against +it. + +* PHPC-276: Cover the raw BSON depth-limit exemption in tests + +Add regression cases where a 501-level document and array map their deepest +field path to "bson". Because raw BSON sub-documents are not traversed, they +must decode without hitting the nesting limit. This would catch a regression +that moved the limit check before the raw BSON condition. + +* Apply batched suggestions from code review + +Co-authored-by: Kevin Albertson + +--------- + +Co-authored-by: Kevin Albertson +--- + src/phongo_bson.c | 30 ++++++++ + src/phongo_bson.h | 6 ++ + tests/bson/bug-phpc-276.phpt | 129 +++++++++++++++++++++++++++++++++++ + 3 files changed, 165 insertions(+) + create mode 100644 tests/bson/bug-phpc-276.phpt + +--- a/mongodb-2.0.0/src/phongo_bson.c ++++ b/mongodb-2.0.0/src/phongo_bson.c +@@ -728,6 +728,16 @@ + } + } + ++/* Throws when the maximum BSON nesting level is exceeded while decoding. */ ++static void php_phongo_bson_throw_nesting_exception(php_phongo_field_path* field_path) ++{ ++ char* path_string = php_phongo_field_path_as_string(field_path); ++ ++ phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Nesting level too deep for field path \"%s\"", path_string); ++ ++ efree(path_string); ++} ++ + static bool php_phongo_bson_visit_document(const bson_iter_t* iter ARG_UNUSED, const char* key, const bson_t* v_document, void* data) + { + zval* retval = PHONGO_BSON_STATE_ZCHILD(data); +@@ -735,6 +745,8 @@ + php_phongo_bson_state* parent_state = (php_phongo_bson_state*) data; + php_phongo_bson_state state; + ++ /* The return value reports the encoding limit, which is lower than the ++ * decoding limit checked below. */ + php_phongo_field_path_push(parent_state->field_path, key, PHONGO_FIELD_PATH_ITEM_DOCUMENT); + + PHONGO_BSON_INIT_STATE(state); +@@ -746,6 +758,14 @@ + + /* Only traverse BSON document if we're not returning a raw BSON structure */ + if (state.field_type.type != PHONGO_TYPEMAP_BSON) { ++ /* Check the limit just before recursing: raw BSON structures are not ++ * traversed and do not consume the stack. */ ++ if (parent_state->field_path->size > BSON_MAX_DECODE_NESTING_LEVEL) { ++ php_phongo_bson_state_dtor(&state); ++ php_phongo_bson_throw_nesting_exception(parent_state->field_path); ++ return true; ++ } ++ + if (!bson_iter_init(&child, v_document)) { + php_phongo_bson_state_dtor(&state); + return false; +@@ -821,6 +841,8 @@ + php_phongo_bson_state* parent_state = (php_phongo_bson_state*) data; + php_phongo_bson_state state; + ++ /* The return value reports the encoding limit, which is lower than the ++ * decoding limit checked below. */ + php_phongo_field_path_push(parent_state->field_path, key, PHONGO_FIELD_PATH_ITEM_ARRAY); + + PHONGO_BSON_INIT_STATE(state); +@@ -832,6 +854,14 @@ + + /* Only traverse BSON array if we're not returning a raw BSON structure */ + if (state.field_type.type != PHONGO_TYPEMAP_BSON) { ++ /* Check the limit just before recursing: raw BSON structures are not ++ * traversed and do not consume the stack. */ ++ if (parent_state->field_path->size > BSON_MAX_DECODE_NESTING_LEVEL) { ++ php_phongo_bson_state_dtor(&state); ++ php_phongo_bson_throw_nesting_exception(parent_state->field_path); ++ return true; ++ } ++ + if (!bson_iter_init(&child, v_array)) { + php_phongo_bson_state_dtor(&state); + return false; +--- a/mongodb-2.0.0/src/phongo_bson.h ++++ b/mongodb-2.0.0/src/phongo_bson.h +@@ -26,6 +26,12 @@ + #define PHONGO_ODM_FIELD_NAME "__pclass" + #define BSON_MAX_NESTING_LEVEL 100 + ++/* The maximum nesting level accepted when decoding BSON. Higher than the ++ * encoding limit because the user can pass deeper documents, and lower than the ++ * level at which the C recursion exhausts the stack. Mirrors libbson's ++ * BSON_VALIDATION_MAX_NESTING_DEPTH. */ ++#define BSON_MAX_DECODE_NESTING_LEVEL 500 ++ + /* Maximum length for a BSON string or binary value. BSON encodes these lengths + * as int32, so anything larger cannot be represented on the wire and would be + * silently truncated (or wrap an allocation) by the narrowing casts in the +--- /dev/null ++++ b/mongodb-2.0.0/tests/bson/bug-phpc-276.phpt +@@ -0,0 +1,128 @@ ++--TEST-- ++PHPC-276: Recursive BSON structures do not exhaust the stack ++--DESCRIPTION-- ++Encoding the result of a recursive bsonSerialize() method used to recurse in C ++until the stack was exhausted and the process crashed. The same happened when ++decoding a deeply nested BSON document. Both paths now throw an ++UnexpectedValueException before the stack is exhausted. ++ ++The encoding limit is 100 levels (BSON_MAX_NESTING_LEVEL, matching libbson's ++JSON parser). The decoding limit is higher (BSON_MAX_DECODE_NESTING_LEVEL, ++matching libbson's BSON_VALIDATION_MAX_NESTING_DEPTH). ++The server has a limit of 100 levels, but users may decode BSON directly. ++--FILE-- ++depth = $depth; ++ } ++ ++ public function bsonSerialize(): array ++ { ++ return $this->depth >= 1 ? [new self($this->depth - 1)] : ['end']; ++ } ++} ++ ++/* Builds a BSON document nesting a document under "a", $depth levels deep. */ ++function nestedDocument(int $depth): string ++{ ++ $doc = bsonDocument([bsonString('end', 'x')]); ++ ++ for ($i = 0; $i < $depth; $i++) { ++ $doc = bsonDocument(["\x03" . "a\x00" . $doc]); ++ } ++ ++ return $doc; ++} ++ ++/* Same as nestedDocument(), but nests BSON arrays instead of documents. */ ++function nestedArray(int $depth): string ++{ ++ $arr = bsonDocument([bsonString('0', 'end')]); ++ ++ for ($i = 0; $i < $depth; $i++) { ++ $arr = bsonDocument(["\x04" . "0\x00" . $arr]); ++ } ++ ++ return $arr; ++} ++ ++function bsonString(string $key, string $value): string ++{ ++ return "\x02" . $key . "\x00" . pack('V', strlen($value) + 1) . $value . "\x00"; ++} ++ ++function bsonDocument(array $elements): string ++{ ++ $body = implode('', $elements); ++ ++ return pack('V', 4 + strlen($body) + 1) . $body . "\x00"; ++} ++ ++echo "Encoding a recursive bsonSerialize() result\n"; ++echo throws(function() { ++ MongoDB\BSON\Document::fromPHP(new MyRecursiveSerializable(50000)); ++}, MongoDB\Driver\Exception\UnexpectedValueException::class), "\n"; ++ ++echo "Decoding a document nested 500 levels deep\n"; ++MongoDB\BSON\Document::fromBSON(nestedDocument(500))->toPHP(); ++ ++echo "Decoding a document nested 501 levels deep\n"; ++echo throws(function() { ++ MongoDB\BSON\Document::fromBSON(nestedDocument(501))->toPHP(); ++}, MongoDB\Driver\Exception\UnexpectedValueException::class), "\n"; ++ ++echo "Decoding an array nested 500 levels deep\n"; ++MongoDB\BSON\Document::fromBSON(nestedArray(500))->toPHP(); ++ ++echo "Decoding an array nested 501 levels deep\n"; ++echo throws(function() { ++ MongoDB\BSON\Document::fromBSON(nestedArray(501))->toPHP(); ++}, MongoDB\Driver\Exception\UnexpectedValueException::class), "\n"; ++ ++/* A raw BSON sub-document is not traversed, so it does not count against the ++ * nesting limit even at the deepest level. */ ++echo "Decoding a 501-level document with a raw BSON leaf\n"; ++MongoDB\BSON\Document::fromBSON(nestedDocument(501))->toPHP([ ++ 'fieldPaths' => [implode('.', array_fill(0, 501, 'a')) => 'bson'], ++]); ++ ++echo "Decoding a 501-level array with a raw BSON leaf\n"; ++MongoDB\BSON\Document::fromBSON(nestedArray(501))->toPHP([ ++ 'fieldPaths' => [implode('.', array_fill(0, 501, '0')) => 'bson'], ++]); ++ ++/* Well past the point at which the C recursion would exhaust the stack. */ ++echo "Decoding a document nested 20000 levels deep\n"; ++echo throws(function() { ++ MongoDB\BSON\Document::fromBSON(nestedDocument(20000))->toPHP(); ++}, MongoDB\Driver\Exception\UnexpectedValueException::class), "\n"; ++ ++?> ++===DONE=== ++ ++--EXPECTF-- ++Encoding a recursive bsonSerialize() result ++OK: Got MongoDB\Driver\Exception\UnexpectedValueException ++Nesting level too deep ++Decoding a document nested 500 levels deep ++Decoding a document nested 501 levels deep ++OK: Got MongoDB\Driver\Exception\UnexpectedValueException ++Nesting level too deep for field path "a%sa" ++Decoding an array nested 500 levels deep ++Decoding an array nested 501 levels deep ++OK: Got MongoDB\Driver\Exception\UnexpectedValueException ++Nesting level too deep for field path "0%s0" ++Decoding a 501-level document with a raw BSON leaf ++Decoding a 501-level array with a raw BSON leaf ++Decoding a document nested 20000 levels deep ++OK: Got MongoDB\Driver\Exception\UnexpectedValueException ++Nesting level too deep for field path "a%sa" ++===DONE=== diff -Nru php-mongodb-2.0.0/debian/patches/0011-PHPC-276-Enforce-the-decode-nesting-limit-for-CodeWS.patch php-mongodb-2.0.0/debian/patches/0011-PHPC-276-Enforce-the-decode-nesting-limit-for-CodeWS.patch --- php-mongodb-2.0.0/debian/patches/0011-PHPC-276-Enforce-the-decode-nesting-limit-for-CodeWS.patch 1970-01-01 00:00:00.000000000 +0000 +++ php-mongodb-2.0.0/debian/patches/0011-PHPC-276-Enforce-the-decode-nesting-limit-for-CodeWS.patch 2026-10-25 18:36:57.000000000 +0000 @@ -0,0 +1,336 @@ +From 795e914e6f7cc631a2f80c89aedcdd2a1df4ada9 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= + +Date: Wed, 14 Oct 2026 16:19:49 +0200 +Subject: [PATCH] PHPC-276: Enforce the decode nesting limit for CodeWScope + scopes (#2166) + +A CodeWScope scope is decoded with a fresh field path, so the nesting +depth was reset to zero at every level. A document nesting CodeWScope +values therefore bypassed the decode limit and exhausted the C stack: +decoding 20000 levels crashed the process with a segmentation fault. + +Carry the depth of the enclosing decode into the scope and check the +limit before recursing, so the limit applies to the combined nesting. +A standalone value, fetched with get() or through an iterator, sits one +level below its document, so its scope starts at depth 1. + +Cover the full decode and the direct access, both at the passing depth +and at the limit. +--- + src/BSON/Javascript.c | 3 +- + src/BSON/Javascript.h | 2 +- + src/phongo_bson.c | 52 ++++++++++++++++++----------- + src/phongo_bson.h | 15 ++++++--- + tests/bson/bug-phpc-276-001.phpt | 57 ++++++++++++++++++++++++++++++++ + tests/bson/bug-phpc-276.phpt | 35 ++++++++++++++++++++ + 6 files changed, 139 insertions(+), 25 deletions(-) + create mode 100644 tests/bson/bug-phpc-276-001.phpt + +--- a/mongodb-2.0.0/src/BSON/Javascript.c ++++ b/mongodb-2.0.0/src/BSON/Javascript.c +@@ -341,7 +341,7 @@ + php_phongo_handler_javascript.offset = XtOffsetOf(php_phongo_javascript_t, std); + } + +-bool phongo_javascript_new(zval* object, const char* code, size_t code_len, const bson_t* scope) ++bool phongo_javascript_new(zval* object, const char* code, size_t code_len, const bson_t* scope, size_t nesting_depth) + { + php_phongo_javascript_t* intern; + +@@ -350,6 +350,7 @@ + bool valid_scope; + + PHONGO_BSON_INIT_STATE(state); ++ state.nesting_depth = nesting_depth; + + valid_scope = php_phongo_bson_to_zval_ex(scope, &state); + zval_ptr_dtor(&state.zchild); +--- a/mongodb-2.0.0/src/BSON/Javascript.h ++++ b/mongodb-2.0.0/src/BSON/Javascript.h +@@ -17,6 +17,6 @@ + #ifndef PHONGO_BSON_JAVASCRIPT_H + #define PHONGO_BSON_JAVASCRIPT_H + +-bool phongo_javascript_new(zval* object, const char* code, size_t code_len, const bson_t* scope); ++bool phongo_javascript_new(zval* object, const char* code, size_t code_len, const bson_t* scope, size_t nesting_depth); + + #endif /* PHONGO_BSON_JAVASCRIPT_H */ +--- a/mongodb-2.0.0/src/phongo_bson.c ++++ b/mongodb-2.0.0/src/phongo_bson.c +@@ -224,7 +224,8 @@ + + static void php_phongo_bson_state_ctor(php_phongo_bson_state* state) + { +- state->field_path = php_phongo_field_path_alloc(false); ++ state->field_path = php_phongo_field_path_alloc(false); ++ state->field_path->base_depth = state->nesting_depth; + } + + static void php_phongo_bson_state_copy_ctor(php_phongo_bson_state* dst, php_phongo_bson_state* src) +@@ -493,13 +494,23 @@ + return false; + } + ++/* Throws when the maximum BSON nesting level is exceeded while decoding. */ ++static void php_phongo_bson_throw_nesting_exception(php_phongo_field_path* field_path) ++{ ++ char* path_string = php_phongo_field_path_as_string(field_path); ++ ++ phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Nesting level too deep for field path \"%s\"", path_string); ++ ++ efree(path_string); ++} ++ + static bool php_phongo_bson_visit_code(const bson_iter_t* iter ARG_UNUSED, const char* key, size_t v_code_len, const char* v_code, void* data) + { + zval* retval = PHONGO_BSON_STATE_ZCHILD(data); + php_phongo_bson_state* state = (php_phongo_bson_state*) data; + zval zchild; + +- if (!phongo_javascript_new(&zchild, v_code, v_code_len, NULL)) { ++ if (!phongo_javascript_new(&zchild, v_code, v_code_len, NULL, 0)) { + return true; + } + +@@ -540,8 +551,19 @@ + zval* retval = PHONGO_BSON_STATE_ZCHILD(data); + php_phongo_bson_state* state = (php_phongo_bson_state*) data; + zval zchild; ++ size_t nesting_depth; + +- if (!phongo_javascript_new(&zchild, v_code, v_code_len, v_scope)) { ++ php_phongo_field_path_write_item_at_current_level(state->field_path, key); ++ ++ /* The scope is decoded with its own field path, so the nesting depth is ++ * carried over explicitly. The scope counts as one level below this value. */ ++ nesting_depth = state->field_path->base_depth + state->field_path->size + 1; ++ if (nesting_depth > BSON_MAX_DECODE_NESTING_LEVEL) { ++ php_phongo_bson_throw_nesting_exception(state->field_path); ++ return true; ++ } ++ ++ if (!phongo_javascript_new(&zchild, v_code, v_code_len, v_scope, nesting_depth)) { + return true; + } + +@@ -551,8 +573,6 @@ + ADD_ASSOC_ZVAL(retval, key, &zchild); + } + +- php_phongo_field_path_write_item_at_current_level(state->field_path, key); +- + return false; + } + +@@ -728,16 +748,6 @@ + } + } + +-/* Throws when the maximum BSON nesting level is exceeded while decoding. */ +-static void php_phongo_bson_throw_nesting_exception(php_phongo_field_path* field_path) +-{ +- char* path_string = php_phongo_field_path_as_string(field_path); +- +- phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Nesting level too deep for field path \"%s\"", path_string); +- +- efree(path_string); +-} +- + static bool php_phongo_bson_visit_document(const bson_iter_t* iter ARG_UNUSED, const char* key, const bson_t* v_document, void* data) + { + zval* retval = PHONGO_BSON_STATE_ZCHILD(data); +@@ -760,7 +770,8 @@ + if (state.field_type.type != PHONGO_TYPEMAP_BSON) { + /* Check the limit just before recursing: raw BSON structures are not + * traversed and do not consume the stack. */ +- if (parent_state->field_path->size > BSON_MAX_DECODE_NESTING_LEVEL) { ++ if (parent_state->field_path->base_depth + parent_state->field_path->size > ++ BSON_MAX_DECODE_NESTING_LEVEL) { + php_phongo_bson_state_dtor(&state); + php_phongo_bson_throw_nesting_exception(parent_state->field_path); + return true; +@@ -856,7 +867,8 @@ + if (state.field_type.type != PHONGO_TYPEMAP_BSON) { + /* Check the limit just before recursing: raw BSON structures are not + * traversed and do not consume the stack. */ +- if (parent_state->field_path->size > BSON_MAX_DECODE_NESTING_LEVEL) { ++ if (parent_state->field_path->base_depth + parent_state->field_path->size > ++ BSON_MAX_DECODE_NESTING_LEVEL) { + php_phongo_bson_state_dtor(&state); + php_phongo_bson_throw_nesting_exception(parent_state->field_path); + return true; +@@ -1074,7 +1086,7 @@ + return phongo_dbpointer_new(zv, value->value.v_dbpointer.collection, value->value.v_dbpointer.collection_len, &value->value.v_dbpointer.oid); + + case BSON_TYPE_CODE: +- return phongo_javascript_new(zv, value->value.v_code.code, value->value.v_code.code_len, NULL); ++ return phongo_javascript_new(zv, value->value.v_code.code, value->value.v_code.code_len, NULL, 0); + + case BSON_TYPE_CODEWSCOPE: + if (!bson_init_static(&bson, value->value.v_codewscope.scope_data, value->value.v_codewscope.scope_len)) { +@@ -1082,7 +1094,9 @@ + return false; + } + +- return phongo_javascript_new(zv, value->value.v_codewscope.code, value->value.v_codewscope.code_len, &bson); ++ /* A standalone value sits one level below its document, so its scope starts ++ * at depth 1, like a top-level CodeWScope in a full document decode. */ ++ return phongo_javascript_new(zv, value->value.v_codewscope.code, value->value.v_codewscope.code_len, &bson, 1); + + case BSON_TYPE_SYMBOL: + return phongo_symbol_new(zv, value->value.v_symbol.symbol, value->value.v_symbol.len); +--- a/mongodb-2.0.0/src/phongo_bson.h ++++ b/mongodb-2.0.0/src/phongo_bson.h +@@ -49,8 +49,12 @@ + php_phongo_bson_field_path_item_types* element_types; + size_t allocated_size; + size_t size; +- size_t ref_count; +- bool owns_elements; ++ /* Nesting depth inherited from an enclosing decode, added to "size" when ++ * enforcing the decoding nesting limit. Non-zero for the scope of a ++ * CodeWScope value, which is decoded with its own field path. */ ++ size_t base_depth; ++ size_t ref_count; ++ bool owns_elements; + } php_phongo_field_path; + + typedef enum { +@@ -92,8 +96,11 @@ + * driver-internal conversions, where the BSON may originate from a server + * reply or from data the application does not control, and where there is + * no type map through which a caller could opt out. */ +- bool skip_odm; +- php_phongo_field_path* field_path; ++ bool skip_odm; ++ php_phongo_field_path* field_path; ++ /* Nesting depth inherited from an enclosing decode, copied to the field path ++ * when the state creates it. Set for the scope of a CodeWScope value. */ ++ size_t nesting_depth; + php_phongo_bson_typemap_element field_type; + } php_phongo_bson_state; + +--- /dev/null ++++ b/mongodb-2.0.0/tests/bson/bug-phpc-276-001.phpt +@@ -0,0 +1,57 @@ ++--TEST-- ++PHPC-276: Fetching a deeply nested CodeWScope value does not exhaust the stack ++--DESCRIPTION-- ++A standalone BSON value is decoded one level below its document. When the value ++is a CodeWScope, its scope is decoded eagerly, so the decode nesting limit must ++count from that level. Fetching the value directly used to start the scope at ++depth 0 and accept one level more than a full document decode. ++--FILE-- ++get('a'); ++ ++echo "Fetching a CodeWScope nested 501 levels deep directly\n"; ++echo throws(function() { ++ MongoDB\BSON\Document::fromBSON(nestedCodeWithScope(501))->get('a'); ++}, MongoDB\Driver\Exception\UnexpectedValueException::class), "\n"; ++ ++?> ++===DONE=== ++ ++--EXPECTF-- ++Fetching a CodeWScope nested 500 levels deep directly ++Fetching a CodeWScope nested 501 levels deep directly ++OK: Got MongoDB\Driver\Exception\UnexpectedValueException ++Nesting level too deep for field path "a" ++===DONE=== +--- a/mongodb-2.0.0/tests/bson/bug-phpc-276.phpt ++++ b/mongodb-2.0.0/tests/bson/bug-phpc-276.phpt +@@ -55,11 +55,32 @@ + return $arr; + } + ++/* Builds a document nesting a CodeWScope under "a", $depth levels deep. Each ++ * scope is a document decoded with its own field path. */ ++function nestedCodeWithScope(int $depth): string ++{ ++ $doc = bsonDocument([]); ++ ++ for ($i = 0; $i < $depth; $i++) { ++ $doc = bsonDocument([bsonCodeWithScope('a', 'x', $doc)]); ++ } ++ ++ return $doc; ++} ++ + function bsonString(string $key, string $value): string + { + return "\x02" . $key . "\x00" . pack('V', strlen($value) + 1) . $value . "\x00"; + } + ++function bsonCodeWithScope(string $key, string $code, string $scopeDocument): string ++{ ++ $codeString = pack('V', strlen($code) + 1) . $code . "\x00"; ++ $length = 4 + strlen($codeString) + strlen($scopeDocument); ++ ++ return "\x0F" . $key . "\x00" . pack('V', $length) . $codeString . $scopeDocument; ++} ++ + function bsonDocument(array $elements): string + { + $body = implode('', $elements); +@@ -88,6 +109,16 @@ + MongoDB\BSON\Document::fromBSON(nestedArray(501))->toPHP(); + }, MongoDB\Driver\Exception\UnexpectedValueException::class), "\n"; + ++/* A CodeWScope scope is decoded with its own field path, so the nesting depth ++ * must be carried over explicitly. */ ++echo "Decoding a CodeWScope nested 500 levels deep\n"; ++MongoDB\BSON\Document::fromBSON(nestedCodeWithScope(500))->toPHP(); ++ ++echo "Decoding a CodeWScope nested 501 levels deep\n"; ++echo throws(function() { ++ MongoDB\BSON\Document::fromBSON(nestedCodeWithScope(501))->toPHP(); ++}, MongoDB\Driver\Exception\UnexpectedValueException::class), "\n"; ++ + /* A raw BSON sub-document is not traversed, so it does not count against the + * nesting limit even at the deepest level. */ + echo "Decoding a 501-level document with a raw BSON leaf\n"; +@@ -121,6 +152,10 @@ + Decoding an array nested 501 levels deep + OK: Got MongoDB\Driver\Exception\UnexpectedValueException + Nesting level too deep for field path "0%s0" ++Decoding a CodeWScope nested 500 levels deep ++Decoding a CodeWScope nested 501 levels deep ++OK: Got MongoDB\Driver\Exception\UnexpectedValueException ++Nesting level too deep for field path "a" + Decoding a 501-level document with a raw BSON leaf + Decoding a 501-level array with a raw BSON leaf + Decoding a document nested 20000 levels deep diff -Nru php-mongodb-2.0.0/debian/patches/series php-mongodb-2.0.0/debian/patches/series --- php-mongodb-2.0.0/debian/patches/series 2026-10-01 06:25:18.000000000 +0000 +++ php-mongodb-2.0.0/debian/patches/series 2026-10-25 18:36:57.000000000 +0000 @@ -5,3 +5,7 @@ 0005-PHPC-2743-Do-not-infer-ODM-class-when-decoding-write.patch 0006-PHPC-2743-Do-not-infer-ODM-class-in-ClientEncryption.patch 0007-PHPC-2636-Respect-libbson-nesting-limit-when-parsing-PHP.patch +0008-PHPC-2741-Fix-potential-issue.patch +0009-PHPC-2739-Fix-potential-issue.patch +0010-PHPC-276-Prevent-stack-exhaustion-when-decoding-deep.patch +0011-PHPC-276-Enforce-the-decode-nesting-limit-for-CodeWS.patch