Skip to content

fix(codegen): auto-vivify appends onto a null-container sentinel mixed cell#600

Open
mirchaemanuel wants to merge 4 commits into
illegalstudio:mainfrom
mirchaemanuel:fix/592-append-sentinel-autovivify
Open

fix(codegen): auto-vivify appends onto a null-container sentinel mixed cell#600
mirchaemanuel wants to merge 4 commits into
illegalstudio:mainfrom
mirchaemanuel:fix/592-append-sentinel-autovivify

Conversation

@mirchaemanuel

Copy link
Copy Markdown
Contributor

Fixes #592.

Root cause

A missed indexed read forwarded through a ternary/branch merge boxes the null-container sentinel (0x7ffffffffffffffe) into a mixed cell tagged as an indexed array. Appending to that cell ($r[] = v, lowered to Op::MixedArrayAppend) unboxes the receiver, accepts the tag-4 payload, and reads the array header inline (ldur x1, [x0, #-8] on aarch64, mov rsi, [rdi - 8] on x86_64) before calling __rt_array_to_mixed. The only pre-dereference guard was a plain-zero check (cbz x1 / test rdi, rdi), so the sentinel passed through and the header read faulted at sentinel - 8 (exit 139). This append path calls __rt_array_to_mixed directly rather than through lower_array_to_mixed, so the call-site sentinel guards added in #583 did not cover it.

PHP 8.4 instead auto-vivifies the null into a fresh single-element array and continues: for the repro it warns Undefined array key 5 on the missed read, then prints done with $r === ["z"] (no warning on the append).

<?php
$rows = [[1, 2]];
$r = $argc == 1 ? $rows[5] : ["a", "b"];
$r[] = "z";
echo "done", "\n";

Run with no arguments, this segfaulted (139); it now prints done.

Fix

lower_mixed_array_append (aarch64 + x86_64) now guards the unboxed payload for both the null pointer and the in-band null-container sentinel before the header dereference, per the issue #533 convention. For a container-shaped tag whose payload is null or the sentinel — indexed (4), associative (5), or null (8) — it auto-vivifies: it allocates a fresh empty indexed array (__rt_array_new(0, 8), matching __rt_mixed_new_empty_array_cell), transfers that reference into the Mixed cell, retags the cell as an indexed array, and appends at index 0 through the shared __rt_mixed_array_set tail. Scalars whose payload happens to be zero or equal to the sentinel value (PHP_INT_MAX - 1) are excluded by the tag check and keep the existing drop behavior.

Ownership is balanced (the cell owns the fresh array; the array owns the appended value; the replaced sentinel/null payload holds nothing to release), so the auto-vivify path stays heap-clean. Both supported architectures are handled in the same change.

PHP semantics

Matches PHP's $x[] = v on null: the null container auto-vivifies to a fresh [v] array, the missed read still warns, and the append itself does not warn. A real-null Mixed cell ($r = cond ? null : [1]; $r[] = "z";) also auto-vivifies correctly through the same tag-8 route.

Test plan

New tests/codegen/arrays/mixed_append_autovivify.rs:

  • the exact issue repro (prints done);
  • the ["z"] read-back (length 1, index 0);
  • a heap-clean sentinel variant asserting the missed-read warning is still emitted;
  • an associative missed-read sibling;
  • a multi-append case that exercises array growth;
  • a real-array Mixed-cell control guarding against a double-conversion regression.

Fixtures make the taken ternary arm $argc-dependent to defeat constant folding. Focused slices run green: arrays::mixed_append_autovivify, nested_autovivify, array_to_mixed, append, push, sentinel, null_container, mixed_array, regressions::arrays, ternary (~292 tests, 0 failures). x86_64 coverage relies on CI (no local cross-assembler).

https://claude.ai/code/session_01HRvbcjPHa3kAojdCjNZL5L

…d cell

A missed indexed read forwarded through a ternary/branch merge boxes the
null-container sentinel (0x7ffffffffffffffe) into a `mixed` cell tagged as
an indexed array. `$r[] = v` on that cell (`Op::MixedArrayAppend`) unboxed
the receiver, accepted the tag-4 payload, and read the array header inline
(`ldur x1, [x0, #-8]`) before `__rt_array_to_mixed`. The only pre-deref
guard was a plain-zero check, so the sentinel passed through and the header
read faulted at `sentinel - 8` (issue illegalstudio#592, exit 139). PHP instead
auto-vivifies the null into a fresh `["z"]` array and continues.

`lower_mixed_array_append` (aarch64 + x86_64) now guards the unboxed
payload for both the null pointer and the in-band null-container sentinel
before the header dereference (issue illegalstudio#533 convention). For a
container-shaped tag with such a payload — indexed (4), associative (5), or
null (8) — it auto-vivifies: it allocates a fresh empty indexed array
(`__rt_array_new(0, 8)`, as `__rt_mixed_new_empty_array_cell` does),
transfers that reference into the Mixed cell, retags the cell as an indexed
array, and appends at index 0 through the shared `__rt_mixed_array_set`
tail. Scalars whose payload happens to be zero or the sentinel are excluded
by the tag check and keep the existing drop behavior. Ownership is balanced
(the cell owns the fresh array; the array owns the appended value), so the
autovivify path stays heap-clean.

Adds regression coverage in tests/codegen/arrays/mixed_append_autovivify.rs:
the exact issue repro, the `["z"]` read-back, a heap-clean sentinel variant,
an associative missed-read sibling, a multi-append grow case, and a
real-array Mixed-cell control that guards against a double-conversion
regression. Fixtures make the taken arm `$argc`-dependent to defeat
constant folding.

Claude-Session: https://claude.ai/code/session_01HRvbcjPHa3kAojdCjNZL5L
@mirchaemanuel

Copy link
Copy Markdown
Contributor Author

While validating this fix I found two adjacent pre-existing bugs (both reverified on current main + this branch): #601 (implode() over a boxed-mixed indexed array leaks one block per appended element — the appends themselves are balanced) and #602 (count() on a raw sentinel cell still segfaults where PHP raises a TypeError — this PR's auto-vivify only masks it when an append happens first).

@github-actions github-actions Bot added area:runtime Touches runtime helpers, GC, ownership, or bridge runtimes. size:m Medium-sized pull request. and removed size:s Small pull request. labels Jul 24, 2026

nahime0 commented Jul 24, 2026

Copy link
Copy Markdown
Member

I updated this PR after merging the latest main and completed the Mixed/null-container write path beyond the original crash site.

Changes now included:

  • canonicalize zero/sentinel array and hash payloads to a real Mixed(null) while boxing;
  • share existing-cell array autovivification across append, set, and fetch-for-write paths;
  • route Mixed append through a single runtime helper for both supported codegen backends;
  • support real associative-array append with PHP's next automatic integer key;
  • keep defensive zero/sentinel guards for legacy array-shaped Mixed cells;
  • avoid the previous duplicate indexed-array conversion before append;
  • add regressions for pure null append, direct set after a missed read, nested write from null, and associative Mixed append.

Focused validation completed:

  • cargo build
  • macOS/AArch64: Mixed append autovivify (8/8), nested autovivify (17/17), nested Mixed write (8/8)
  • Linux/x86_64: Mixed append autovivify (8/8)
  • git diff --check
  • assembly comment/alignment checker on all touched codegen/runtime files

Published head: 4d61f4cffdd67ee6e2a911f919a874c60db637ce.

GitHub Actions for that exact head is currently queued.

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

Labels

area:codegen Touches target-aware assembly or backend lowering. area:runtime Touches runtime helpers, GC, ownership, or bridge runtimes. size:m Medium-sized pull request. type:fix Corrects broken or incompatible behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Appending to a boxed-mixed null-container sentinel segfaults in the inline append path instead of auto-vivifying

2 participants