PHPC-2505: Fix foreach after GC invocation - #1982
Merged
Merged
Conversation
GromNaN
requested review from
alcaeus and
Copilot
and removed request for
a team
April 13, 2026 12:17
GromNaN
marked this pull request as draft
April 13, 2026 12:20
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes a GC/foreach inconsistency affecting BSON objects by preventing the engine’s standard property table/GC behavior from interfering with the driver’s cached BSON property HashTables.
Changes:
- Adds per-class property/GC handlers that store dynamic PHP properties separately from BSON “properties” caches.
- Removes the shared
get_gcoverride fromphongo.cand updates HashTable teardown to usezend_hash_release. - Adds regression tests covering
gc_collect_cycles()and property set/unset (incl. by-reference) scenarios.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| phongo.c | Removes the shared std get_gc override. |
| phongo.h | Introduces shared macros to define/assign per-class property + get_gc handlers; updates temp HashTable freeing. |
| src/phongo_structs.h | Adds php_properties to various internal structs to separate dynamic props from cached BSON properties. |
| src/BSON/Binary.c | Releases cached HashTables with zend_hash_release; assigns new property handlers. |
| src/BSON/DBPointer.c | Same as above for DBPointer. |
| src/BSON/Decimal128.c | Same as above for Decimal128. |
| src/BSON/Int64.c | Same as above for Int64. |
| src/BSON/Javascript.c | Same as above for Javascript. |
| src/BSON/Iterator.c | Same as above for Iterator. |
| src/BSON/ObjectId.c | Same as above for ObjectId. |
| src/BSON/PackedArray.c | Same as above for PackedArray. |
| src/BSON/Regex.c | Same as above for Regex. |
| src/BSON/Symbol.c | Same as above for Symbol. |
| src/BSON/Timestamp.c | Same as above for Timestamp. |
| src/BSON/UTCDateTime.c | Same as above for UTCDateTime. |
| src/BSON/MaxKey.c | Adds php_properties cleanup; assigns new property handlers. |
| src/BSON/MinKey.c | Adds php_properties cleanup; assigns new property handlers. |
| src/BSON/Document.c | Updates cached HashTable teardown to use zend_hash_release. |
| src/MongoDB/Manager.c | Switches subscriber HashTable teardown to zend_hash_release. |
| src/MongoDB/ServerApi.c | Adds php_properties cleanup; assigns new property handlers. |
| src/MongoDB/ServerDescription.c | Adds php_properties cleanup; assigns new property handlers. |
| src/MongoDB/TopologyDescription.c | Adds php_properties cleanup; assigns new property handlers. |
| tests/bson/bug2505-001.phpt | New regression test: gc_collect_cycles() doesn’t break foreach. |
| tests/bson/bug2505-002.phpt | New regression test: set/unset dynamic prop doesn’t break foreach. |
| tests/bson/bug2505-003.phpt | New regression test: set/unset via reference doesn’t break foreach. |
| tests/bson/bug1598-002.phpt | Updates existing GC-cycle test to run on newer PHP versions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
GromNaN
force-pushed
the
phpc-2505-tests
branch
from
April 13, 2026 16:19
5b45a55 to
190f1bd
Compare
This was referenced Sep 28, 2026
GromNaN
force-pushed
the
phpc-2505-tests
branch
3 times, most recently
from
September 29, 2026 21:33
ef70bfa to
b11fe62
Compare
GromNaN
force-pushed
the
phpc-2505-tests
branch
from
September 29, 2026 22:10
e4f9f48 to
956b0fe
Compare
GromNaN
marked this pull request as ready for review
September 29, 2026 22:21
kevinAlbs
approved these changes
Oct 1, 2026
kevinAlbs
left a comment
Contributor
There was a problem hiding this comment.
LGTM with a possible comment tweak.
GromNaN
force-pushed
the
phpc-2505-tests
branch
from
October 1, 2026 21:01
956b0fe to
bb319aa
Compare
GromNaN
force-pushed
the
phpc-2505-tests
branch
from
October 1, 2026 21:08
bb319aa to
97ed8de
Compare
The shared get_gc handler called zend_std_get_properties(), which materialized an empty zend_object.properties table for classes that do not declare PHP properties. ZEND_FE_RESET_R reads zend_object.properties first and uses it instead of the get_properties handler, so after a GC run foreach silently iterated nothing. Replace the handler with one that returns object->properties without materializing it when the class defines a custom get_properties handler, and delegates to zend_std_get_gc otherwise. This keeps foreach independent from garbage collection, keeps user-assigned dynamic properties visible to the cycle collector, and does not expose the intern->properties cache to the GC. Also replace zend_hash_destroy + FREE_HASHTABLE with zend_hash_release in PHONGO_GET_PROPERTY_HASH_FREE_PROPS, Document.c, and Manager.c so HashTables are released through their reference count. Add a test (bug2505-001) verifying that foreach iteration on BSON objects is consistent before and after gc_collect_cycles().
…rties phongo_serverapi_get_properties_hash() populated the cached property HashTable with zend_hash_str_add(). On the second and later calls the keys already exist, so the add fails and the freshly allocated "version" zend_string is never released. foreach calls get_properties several times per object, so every iteration leaked one string. Use zend_hash_str_update() as every other class's property builder does, so an existing value is replaced and the previous one released. Found while adding ServerApi coverage to tests/bson/bug2505-001.phpt.
GromNaN
force-pushed
the
phpc-2505-tests
branch
from
October 1, 2026 21:12
97ed8de to
6c0697b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PHPC-2505. Fixes #1776.
Problem
Calling
gc_collect_cycles()changed howforeachiterates BSON value objects, as well asMongoDB\Driver\ServerApi,ServerDescription, andTopologyDescription. Before a GC run, anObjectIdyielded itsoidproperty. After a GC run, the loop body never executed, with no error or warning.Root cause: the extension's shared
get_gchandler calledzend_std_get_properties(). For these classes, which declare no PHP properties, that materialized an emptyzend_object.propertiestable. TheZEND_FE_RESET_Ropcode readszend_object.propertiesfirst and, when it is non-NULL, uses it instead of calling theget_propertieshandler. The empty table then madeforeachskip the loop silently.Approaches considered
Remove the shared
get_gchandler and rely on the engine default (the first commit of this PR). The engine default invokes the classget_propertieshandler when it is overridden, which returns the object's cached debug property table (intern->properties). That table is owned by the object and freed byfree_object, so exposing it to the collector is the lifetime hazard that PHPC-1598 fixed. It also hides user assigned dynamic properties from the collector, which broketests/bson/bug1598-002.phpton PHP 8.1. Rejected.Add a
php_propertiesHashTable plus per-classread_property,write_property,has_property,unset_property,get_property_ptr_ptr, andget_gchandlers (PR Fix foreach after the garbage collector is invoked #1850, revised as PHPC-2505: Fix foreach after GC invocation #1940). This separates user assigned dynamic properties from the debug cache and also stabilizes the set/unset property case. It was abandoned: it duplicates engine property semantics across 19 classes, had a garbage collector refcount leak on the returned table, and conflicted with the typed readonly properties added by PHPC-2699.Replace
get_propertieswithget_properties_for(PHPC-1622, prototype PHPC-1622: Use get_properties_for handler (prototype: ObjectId only) #1969). It does not fix this bug: for a nonTraversableobject,ZEND_FE_RESET_Rcallsget_propertiesdirectly, soforeachstill reads the materialized table.Add typed readonly properties to the BSON classes and drop the custom property handling (PHPC-2705 and PHPC-2706). This is the long term fix and would give native PHP behavior, but those tickets are still in the backlog and this bug is blocked on them. Out of scope here.
Fix
Keep a
get_gchandler, but make it narrow. When the class defines a customget_propertieshandler, returnobject->propertieswithout ever materializing it. Otherwise, delegate tozend_std_get_gc. The property cache (intern->properties) is never exposed to the collector, user assigned dynamic properties are still scanned for cycles, andforeachno longer depends on whether the garbage collector ran.The change is limited to the
get_gchandler inphongo.cand a regression test. Replacing the remainingzend_hash_destroyplusFREE_HASHTABLEteardowns withzend_hash_releaseis handled separately in PHPC-2682.ServerApi property cache leak
Adding
ServerApito the foreach regression test exposed a separate, pre-existing bug.phongo_serverapi_get_properties_hash()filled the cached property HashTable withzend_hash_str_add(). From the second call on, the keys already exist, the add fails, and the newly allocatedversionstring is never released.foreachcallsget_propertiesseveral times per object, so each iteration leaked a string. It now useszend_hash_str_update(), like every other class's property builder. This is a second commit.Known limitation
Setting and then unsetting a dynamic property (
$oid->foo = 'x'; unset($oid->foo);) still leavesforeachyielding nothing. The nativewrite_propertyhandler createszend_object.properties, and it keeps shadowingget_propertieseven once emptied. Fixing that requires removing the customget_propertieshandling altogether, which is tracked by PHPC-2705. Dynamic properties are deprecated as of PHP 8.2.Follow-ups
zend_hash_destroyplusFREE_HASHTABLEteardowns onzend_hash_release.