Skip to content

Fix GH-23586: Keep GC traversal consistent under memory exhaustion - #24645

Open
kairosci wants to merge 4 commits into
php:PHP-8.4from
kairosci:fix/gh-23586-gc-traversal-oom
Open

kairosci wants to merge 4 commits into
php:PHP-8.4from
kairosci:fix/gh-23586-gc-traversal-oom

Conversation

@kairosci

Copy link
Copy Markdown
Contributor

Fixes #23586 by allocating GC traversal storage outside the request memory limit and freeing it on normal return or bailout. Adds regression coverage for memory exhaustion, wide graphs, and destructor bailouts.

@arnaud-lb

Copy link
Copy Markdown
Member

I believe that this is fixed by 8.4.27. Could you confirm?

@kairosci

Copy link
Copy Markdown
Contributor Author

I don't think so, I can still reproduce the issue in 8.4 and 8.5.

@arnaud-lb

Copy link
Copy Markdown
Member

Could you share the exact command you use to execute the reproducer (preferably with php -n ...), and the output of php --version ?

@kairosci

kairosci commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor Author

I ran the reproducer with:

./sapi/cli/php -n run-tests.php -n Zend/tests/gh23586.phpt

The test passed, but it exhausted memory in zend_hash.c:161; I did not reproduce the GC assertion.

./sapi/cli/php --version

PHP 8.4.28-dev (cli) (built: Oct 26 2026 13:38:21) (NTS DEBUG)
Copyright (c) The PHP Group
Zend Engine v4.4.28-dev, Copyright (c) Zend Technologies

Could you point me to the change in 8.4.27 that fixes this?

@kairosci

kairosci commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor Author

I also verified the original reproducer with and without this change on PHP 8.4. Without the fix, it fails while allocating GC traversal storage; with the fix, it gets past that point. This confirms the fix addresses the issue

I also checked the PHP-8.4 branch and the 8.4.27 tag: both still use emalloc() for GC traversal storage, so this fix is not included there yet. The change in this PR replaces it with pemalloc().

@arnaud-lb

Copy link
Copy Markdown
Member

Could you point me to the change in 8.4.27 that fixes this?

The graceful OOM handling introduced in 3584a98 is supposed to fix this. The OOM you are observing is not an issue if this doesn't interrupt the GC anymore.

@kairosci
kairosci force-pushed the fix/gh-23586-gc-traversal-oom branch from b9dc33a to c0600e4 Compare November 2, 2026 21:18
@kairosci

Copy link
Copy Markdown
Contributor Author

The earlier statement that PHP 8.4.27 does not contain the graceful OOM change was incorrect. I verified that commit 3584a98bda0f2c0d367255b046798b0d3a221a19 is present in the PHP-8.4 branch.

Written with ChatGPT. I tested PHP-8.4 at f6c9428b97a in a debug build. Running sapi/cli/php -n -d memory_limit=93M /tmp/gh23586-original.php prints 0 and then a clean fatal OOM in zend_gc.c:336; the GC assertion is not reproduced. The original gh23586.phpt also passes on that base, so it does not distinguish the patch. However, gh23586_002.phpt fails on the same base because the wide graph traversal exhausts the request limit before completing. With this PR's implementation, that test passes in CI. I updated NEWS to describe that narrower behavior and removed the nondiscriminating test.

Could you confirm whether completing this wide traversal after the request limit is reached is useful independently of the assertion fix from 8.4.27?

@kairosci

Copy link
Copy Markdown
Contributor Author

I verified this with the wide-graph case. The 8.4.27 graceful OOM change prevents the original GC assertion, as you noted, but it does not let this traversal complete: on unmodified PHP-8.4 at f6c9428b97a, the test now uses 350,000 children and gc_collect_cycles() fatals while allocating the 2 MiB traversal segment in zend_gc.c:336. With this PR's implementation, the same case completes and collects 350,002 cycles. The PHPT now uses that threshold so it fails on the base and passes with the patch.

I think that behavior is useful independently: when a wide cycle graph is already at memory_limit, explicitly collecting it should be able to reclaim those cycles instead of terminating because GC's auxiliary traversal stack cannot allocate another request-memory segment. This is the narrower behavior the PR now describes; it is not a claim that the original assertion still reproduces.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants