Skip to content

Key each foreach step to its activation so generator instances don't … - #382

Merged
alganet merged 1 commit into
mainfrom
generator-foreach
Jul 24, 2026
Merged

Key each foreach step to its activation so generator instances don't …#382
alganet merged 1 commit into
mainfrom
generator-foreach

Conversation

@alganet

@alganet alganet commented Jul 24, 2026

Copy link
Copy Markdown
Owner

…clash

foreach iteration state lives in ph7_foreach_info.aStep, a per-STATEMENT stack shared by every activation, and OP_FOREACH_STEP peeked the last entry. Two suspended activations of the same textual foreach — two generator instances, two fibers, or a recursive call paused in the same loop — resume out of LIFO order and grab each other's step, so $g1 = g([1,2,3]); $g2 = g([10,20]) with interleaved next()/current() printed 11020 where php prints 110220 (a silent wrong answer).

Record the owning activation frame on each step (ph7_foreach_step.pFrame, normalized past exception frames via VmSkipExceptionFrames, stamped at OP_FOREACH_INIT). OP_FOREACH_STEP now scans aStep top-down for the step whose pFrame matches the running activation — the most-recent push wins, so a leaked step that shares a recycled frame address never shadows the live one. The step teardown paths remove by pointer (new VmForeachStepUnlink) instead of popping the tail, order-preserving so a live step is never reordered below a leaked sibling. This mirrors the per-exec-context cursor the yield-from opcode already keeps for delegation.

Byte-verified vs php 8.5.7 across two-instance, recursive, two-fiber, nested-in-generator, and break-leak-interleave scenarios; cross-engine test foreach_same_generator_two_instances; test-compat green; docker ASan+UBSan clean; full and tiny builds warning-free.

…clash

foreach iteration state lives in ph7_foreach_info.aStep, a per-STATEMENT stack
shared by every activation, and OP_FOREACH_STEP peeked the last entry. Two
suspended activations of the same textual foreach — two generator instances,
two fibers, or a recursive call paused in the same loop — resume out of LIFO
order and grab each other's step, so $g1 = g([1,2,3]); $g2 = g([10,20]) with
interleaved next()/current() printed 11020 where php prints 110220 (a silent
wrong answer).

Record the owning activation frame on each step (ph7_foreach_step.pFrame,
normalized past exception frames via VmSkipExceptionFrames, stamped at
OP_FOREACH_INIT). OP_FOREACH_STEP now scans aStep top-down for the step whose
pFrame matches the running activation — the most-recent push wins, so a leaked
step that shares a recycled frame address never shadows the live one. The step
teardown paths remove by pointer (new VmForeachStepUnlink) instead of popping
the tail, order-preserving so a live step is never reordered below a leaked
sibling. This mirrors the per-exec-context cursor the yield-from opcode already
keeps for delegation.

Byte-verified vs php 8.5.7 across two-instance, recursive, two-fiber,
nested-in-generator, and break-leak-interleave scenarios; cross-engine test
foreach_same_generator_two_instances; test-compat green; docker ASan+UBSan
clean; full and tiny builds warning-free.
@alganet
alganet marked this pull request as ready for review July 24, 2026 17:21
@alganet
alganet merged commit c77d56d into main Jul 24, 2026
24 checks passed
@alganet
alganet deleted the generator-foreach branch July 24, 2026 17:21
@github-actions

Copy link
Copy Markdown

Coverage

Total Rate Hit/Total
Lines 81.14% 41086/50635
Functions 96.62% 1915/1982

Details: https://github.com/alganet/PHL/actions/runs/30112170816

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant