Skip to content

fix: raise count() TypeError on a null-container Mixed receiver instead of segfaulting#616

Open
mirchaemanuel wants to merge 3 commits into
illegalstudio:mainfrom
mirchaemanuel:fix/602-count-sentinel-typeerror
Open

fix: raise count() TypeError on a null-container Mixed receiver instead of segfaulting#616
mirchaemanuel wants to merge 3 commits into
illegalstudio:mainfrom
mirchaemanuel:fix/602-count-sentinel-typeerror

Conversation

@mirchaemanuel

Copy link
Copy Markdown
Contributor

Summary

count() on a boxed mixed receiver whose payload is the null-container sentinel segfaulted
(exit 139) instead of raising PHP's TypeError. A missed array read forwarded through a
ternary merge ($argc == 1 ? $rows[5] : ["a", "b"]) is boxed as an indexed-array mixed
cell whose payload pointer is the in-band NULL_SENTINEL; __rt_mixed_count dereferenced
that payload past a plain-zero guard.

Repro

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

Run with no arguments (missed-read arm):

  • Before: Warning: Undefined array key 5, then SIGSEGV (exit 139).
  • After: the same warning, then Fatal error: Uncaught TypeError: count(): Argument #1 ($value) must be of type Countable|array, null given (exit 1) — matching PHP's diagnostic
    in elephc's uncaught-exception format, and catchable as a TypeError.

Root cause

The count() lowering for a boxed Mixed receiver delegated to __rt_mixed_count, whose
tag-4/5 payload read (ldr x0, [x9] / mov rax, [r10]) was guarded only against a zero
pointer. The non-zero in-band NULL_SENTINEL slipped through and was dereferenced — the same
hole family as #526/#533 (read-side hardening), #585/PR #591 (get/set helpers), and
#592/PR #600 (append path). The concrete-array count() arm already raised this exact
TypeError via emit_type_error; the Mixed arm lacked the equivalent guard.

Fix

  • __rt_mixed_count now recognizes every null form of the receiver — a null pointer, the
    in-band sentinel as the cell or as a tag-4/5 payload (checked before the dereference, per
    fix(codegen): guard container consumers against the null-container sentinel (chained-read miss segfault) #533), and a boxed null cell (tag 8) — and returns NULL_SENTINEL as a "receiver was a
    null container" signal. Other non-countable tags still return 0.
  • The count() call site turns that signal into PHP's TypeError via the existing
    emit_type_error, matching the concrete-array path. A plain 0 result (an empty container)
    passes through unchanged, so the sentinel signal never collides with a legitimate count.
  • Guards land on both AArch64 and x86_64.

Real null (count(json_decode("null")), tag 8) previously returned 0 silently; it now
raises the identical TypeError, so the sentinel and real null behave the same way and both
match PHP. Non-null non-countable scalars (e.g. count(json_decode("42"))) still return 0,
unchanged (tracked separately as a quiet PHP-parity divergence).

Tests

New regression file tests/codegen/regressions/mixed_count_sentinel.rs:

  • uncaught sentinel count() prints the PHP TypeError fatal and exits with failure;
  • the same error is catchable and execution continues;
  • a real null cell (tag 8) raises the identical TypeError;
  • the same ternary merge delivering a real array counts correctly;
  • an empty container in a mixed cell still counts as 0;
  • the catchable error path is heap-clean under heap debug.

Focused slices green: mixed_count_sentinel (6), count (67), mixed (286), sentinel
(43), miss (119, including the #526 array-miss controls), sentinels lib unit tests (5).

Fixes #602

https://claude.ai/code/session_01HRvbcjPHa3kAojdCjNZL5L

…iver

count() on a boxed Mixed cell whose payload is the null-container sentinel —
produced by a missed array read forwarded through a ternary merge, which boxes
the sentinel as an indexed-array cell — dereferenced the sentinel past a
plain-zero guard and segfaulted (issue illegalstudio#602).

__rt_mixed_count now recognizes every null form of the receiver (a null
pointer, the in-band sentinel as the cell or as a tag-4/5 payload, and a boxed
null cell tag 8) and returns a null signal; the count() call site turns that
signal into PHP's TypeError: count(): Argument illegalstudio#1 ($value) must be of type
Countable|array, null given, matching the concrete-array path. Real containers,
including empty ones, still count correctly and the catchable error path stays
heap-clean. Guards land on both AArch64 and x86_64.

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

Copy link
Copy Markdown
Contributor Author

The remaining count() parity gap mentioned in the description (non-null scalars in a mixed cell silently counting as 0, where PHP raises TypeError with the scalar type in the message) is now tracked as #617 — the machinery from this PR makes it a small follow-up, but it changes a test-locked behavior so it's kept separate.

@github-actions github-actions Bot added area:codegen Touches target-aware assembly or backend lowering. area:runtime Touches runtime helpers, GC, ownership, or bridge runtimes. size:s Small pull request. type:fix Corrects broken or incompatible behavior. labels Jul 23, 2026
@mirchaemanuel

Copy link
Copy Markdown
Contributor Author

CI caught a real interaction here: extending the TypeError to plain null receivers breaks the deliberate BUG-0 CLI convention that off-web superglobals count as 0 (bug0_cli_read_of_server_superglobal_before_assignment_does_not_crash). Correcting to raise the TypeError for the sentinel forms only (the actual #602 crash) and restoring the legacy 0 for plain null / boxed-null receivers — that residual PHP-parity gap moves to #617's scope. The docs-sync failure is just line-number drift from the builtins.rs edit; regenerating. Fix incoming on this branch.

The initial illegalstudio#602 fix over-reached: routing a plain null-pointer receiver and a
boxed null cell (tag 8) to the count() TypeError broke the test-locked off-web
convention that count($_SERVER) returns 0 (an uninitialized superglobal reaches
__rt_mixed_count as a zero cell pointer), regressing
bug0_cli_read_of_server_superglobal_before_assignment_does_not_crash.

Narrow the guard to the actual crash: only the in-band NULL_SENTINEL — carried
as the cell itself or as a tag-4/5 payload pointer (a missed refcounted read
boxed as an array) — is guarded before the dereference and signaled back so the
call site raises PHP's TypeError. A plain null pointer, a boxed null cell, a
null payload, and every other non-countable tag keep returning 0 as before.
Full PHP parity for real null (which raises a TypeError) is tracked with the
non-container-scalar divergence in issue illegalstudio#617. Both AArch64 and x86_64.

test_mixed_count_real_null_cell_matches_sentinel becomes
test_mixed_count_real_null_cell_is_legacy_zero, locking count(json_decode("null"))
=== 0; the sentinel uncaught/catchable/heap-clean tests are unchanged.

Claude-Session: https://claude.ai/code/session_01HRvbcjPHa3kAojdCjNZL5L
Regenerate the builtin catalog after the count() null-container guard shifted
lower_empty's line reference in src/codegen/lower_inst/builtins.rs. No builtin
signature, lowering strategy, or area assignment changed; only the source line
link moves (1077 -> 1099). Produced by
`python3 scripts/docs/extract_builtins.py --render --force`.

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

Copy link
Copy Markdown
Contributor Author

Correction pushed (f16c4b668 + docs sync ac606f34c). Final routing: only values carrying the in-band NULL_SENTINEL (as the cell or as a tag-4/5 payload) raise the TypeError — i.e. exactly the forms that segfaulted; a plain null cell (off-web superglobals), a null tag-4/5 payload, and a boxed real null (tag 8) all keep their legacy 0, so the BUG-0 CLI convention (count($_SERVER) === 0) is preserved and bug0_cli_read_of_server_superglobal_before_assignment_does_not_crash passes again. PHP parity for real-null/scalar receivers stays tracked in #617. Docs desync was the lower_empty embedded line shifting 1077→1099 from the new guard; regenerated with the CI sequence, idempotent.

@github-actions github-actions Bot added the area:tooling-ci Touches CI, development tooling, Docker, or repository scripts. label Jul 23, 2026
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. area:tooling-ci Touches CI, development tooling, Docker, or repository scripts. size:s Small pull request. type:fix Corrects broken or incompatible behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

count() on a null-container sentinel mixed cell segfaults instead of raising PHP's TypeError

1 participant