[GIT-PULLS] [php-src] PR #23362: Fix GH-10134: report internal call arguments to GC

[email protected] (xhon-pelushi)
Newsgroups php.git-pulls
Message-ID <5wLgywreTG9tFjhUhET2yWtUHDTrNaBl0E5BdidfMvQ@main.internal.php.net>
Pull Request: https://github.com/php/php-src/pull/23362
Author: xhon-pelushi

Fixes GH-10134.

## The bug

A fiber can be suspended *below* an internal function. That leaves the internal frame parked on the fiber's stack with the arguments that were pushed for it still live — the frame owns them and only releases them when it returns.

`zend_unfinished_execution_gc_ex()` returns early for internal frames without reporting those arguments:

```c
if (!ZEND_USER_CODE(EX(func)->common.type)) {
    ZEND_ASSERT(...);
    return NULL;          /* arguments never reported */
}
```

So the cycle collector never sees them, and any cycle running through such an argument is uncollectable — it is only broken at request shutdown.

## Not specific to generators

GH-10134 reports this as a generator problem, because the reporter hit it through `Fiber::suspend()` inside a generator resumed by `Generator::send($fiber)` — `$fiber` lands in the argument slot of the internal `Generator::send` frame. But nothing about it is generator-specific. With no generator anywhere, a fiber suspended inside `array_map()` whose array argument holds the fiber behaves identically (second test added here).

## How it was diagnosed

`zend_fiber_object_gc()` is not at fault — it correctly walks the suspended fiber's frames, finds the generator frame, and calls `zend_generator_frame_gc()` on it. Instrumenting the collector's root processing showed the actual failure:

```
[root-before]     Fiber rc=2  PURPLE
  reported obj    Fiber rc=2            <- reported once
[root-after-mark] Fiber rc=1  GREY
[root-after-mark] Generator rc=3        <- restored from 0
```

The Fiber's refcount is 2 but the mark phase discovers only one of those references, so it lands at 1 instead of 0, `gc_scan` concludes it is externally referenced, and `gc_scan_black` restores every refcount it had decremented. Dumping the frames shows where the second reference lives:

```
frame ... INTERNAL func=send nargs=1
   ARG[0] type=8 Fiber 0x...      <- never reported
```

## On double counting

This was the main risk. It does not happen: on `ZEND_DO_ICALL` / `ZEND_DO_FCALL` the VM does `EX(call) = call->prev_execute_data`, so an *entered* frame is no longer in its caller's `EX(call)` chain and is therefore not also scanned by `zend_unfinished_calls_gc()`, which only walks calls that are still being built. Those pending frames are siblings rather than ancestors, so they never show up in the `prev_execute_data` walk either.

`zend_unfinished_execution_gc_ex()` has two callers. `zend_generator_frame_gc()` always passes `generator->execute_data`, which is user code, so only the fiber path is affected in practice.

## Testing

- Both new tests **fail on unmodified master and pass with this change** — I ran them against a pristine build to confirm they are real regression guards.
- The issue's verbatim script now prints the expected order (`Generator dtor`, `Fiber dtor`, `Shutdown`) instead of `Shutdown` first, and `gc_collect_cycles()` returns 1 instead of 0.
- **No premature frees**, which is the thing to worry about when handing GC additional edges: an object passed as an internal-frame argument that is *also* still referenced from live scope survives `gc_collect_cycles()` and remains usable. Checked explicitly.
- Full test suite on this build (`--disable-all --enable-debug`): **13386 passed, 0 failed, 0 warned** (7 expected-fail, the rest skipped for disabled extensions).
- `Zend/tests/fibers` and `Zend/tests/gc` under valgrind memcheck: clean.

Built and tested on Linux x86_64, `--disable-all --enable-debug`, so the extension-heavy parts of the suite were skipped; a normal CI run would cover those.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.