Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1202,9 +1202,19 @@ silent:
- **itcl** — done. 10 failures, all upstream or Tcl 9's, two settled at
source, none ours.

**php — ***IT LINKS AND IT RUNS***: `PHP 8.5.12 (cli) (NTS)`, Zend
Engine v4.5.12, printed by the binary on the VM. Notes in
`docs/notes/php.md`.** **`php -v` then FAULTED at exit and the cause
**php — ***IT LINKS, IT RUNS AND IT EVALUATES***: `PHP 8.5.12 (cli)
(NTS)`, and `php-smoke.php` is **44 of 45** on the VM -- PHP_INT_SIZE
8, strlen, max, str_repeat at 100k, preg, json, and `/net/tcp/stats`
read as 404 bytes. Notes in `docs/notes/php.md`.**
***THE ONE THAT MATTERED WAS `exit()`, AND IT WAS kencc's 32-BIT
`sizeof`***: `zend_object_properties_size` is `sizeof(zval) * (count -
1)` with count 0, which C makes 2**64-16 and kencc makes 4294967280 by
multiplying in 32 bits and zero-extending -- so every `exit()` asked
for 4GB and died. *A script with no `exit()` was always clean*, which
is the one-command bisect that found it (`php_cli.c:1318` returns
before `module_started` is set), and `acid`'s `src(pc)` named the line
the round before. Fixed locally in php; **the compiler deviation is
recorded and not taken**, see the invariant above. **`php -v` then FAULTED at exit and the cause
is a platform consequence**: `ZEND_EXTENSIONS_SUPPORT` is 0 without
dlopen (`zend_portability.h:200`), so `zend_extension_startup`'s body
is compiled out and **no zend_extension's startup ever runs here** --
Expand Down Expand Up @@ -1834,6 +1844,18 @@ the index, so that nothing here is a surprise.
sign-extended with no diagnostic. `strtoumax` calling `strtoull`
without `<stdlib.h>` is what made bash print `FFFFFFFF9B3A59A5`;
`sys/lib/tests/apdecl-sweep.py` sweeps libap for the shape.
***AND THE ARGUMENT IS ONLY HALF OF IT: `sizeof` BREAKS ARITHMETIC
TOO, WITH NO MISSING PROTOTYPE AND NO DIAGNOSTIC.*** C requires
`sizeof` to yield `size_t`, which is 64 bits here; `cc/com.c:894`
gives it TULONG, which is 32. So `sizeof(T) * (n - 1)` with `n == 0`
-- php's `zend_object_properties_size`, ordinary correct C that
relies on the wraparound -- multiplies in 32 bits and then
**ZERO-extends**: `SUBL` / `SALL $4` / `MOVLQZX`, 4294967280 where
gcc gives 2**64-16. **Every `exit()` in every php script then asked
the allocator for 4GB.** The conformant fix is one line and is NOT
taken, because a 64-bit `sizeof` hands a variadic `%d` eight bytes;
`docs/notes/kencc.md` has the measurement, the trade and the control
set to run if anyone takes it.
- A variadic sentinel must be a pointer, never `0`.
- `free()` needs exactly the pointer `malloc()` returned.
- `longjmp` must never write to the stack; `main9.s` must use only
Expand Down
71 changes: 71 additions & 0 deletions docs/notes/kencc.md
Original file line number Diff line number Diff line change
Expand Up @@ -3352,3 +3352,74 @@ Both fixes are in the block the string-literal fix of the previous
round also touched, and the controls are the same three: `agree.c`
identical, `--probe` ten `regpair-min` and eight `regpair-probe` with
`kc` and `vc` at `:242`, clisp's 31 TUs clean.

## `sizeof` is 32 bits, and that breaks ARITHMETIC as well as arguments

This file and `CLAUDE.md` have carried "`sizeof` is 32-bit" for a long
time, always with the same consequence attached: *a call with no
prototype in scope corrupts the argument*. **There is a second
consequence and it is worse, because it needs no missing prototype and
produces no diagnostic at all.**

`cc/com.c:894` types the result of `sizeof` as `TULONG`, with the
reason written beside it: on Plan 9 `long` is 32 bits even on amd64,
so returning that width "matches the ABI width of every long/int
parameter without corrupting the call stack". **But C requires
`sizeof` to yield `size_t`, and APE's `size_t` here is 64 bits.** Any
expression that multiplies `sizeof` by something that can go negative
is therefore evaluated in 32 bits and then **zero-extended** to 64.

***THE MEASUREMENT, FROM THE COMPILER ITSELF.*** php's
`Zend/zend_objects_API.h:83` is ordinary, correct C:

return sizeof(zval) *
(ce->default_properties_count -
((ce->ce_flags & ZEND_ACC_USE_GUARDS) ? 0 : 1));

`zend_object` ends in `zval properties_table[1]`, so a class with no
properties wants one zval LESS than the struct; `default_properties_count`
is an `int`, the subtraction is -1, and `sizeof(zval) * -1` is
2**64-16, which the caller's `sizeof(zend_object) + ...` wraps back to
40. `6c -J -S` on that expression:

MOVL 4(CX),DI
SUBL AX,DI
MOVL DI,AX
SALL $4,AX
MOVLQZX AX,AX

-- a 32-bit subtract, a 32-bit multiply, and **`MOVLQZX`**, which
zero-extends. 4294967280 where gcc gives 2**64-16. Both numbers were
confirmed by running the same source under gcc (40) and reading
kencc's output (4294967336, page-rounded to 4294971392).

**That number killed every `exit()` in every php script**, because
`zend_throw_unwind_exit` constructs an object of a class with zero
properties and no guards. It is in `docs/notes/php.md` with how it was
found, and the local fix is to compute in `ptrdiff_t`, which makes
kencc emit `MOVLQSX` / `SUBQ` / `SALQ` and agree.

### Why the compiler is not being changed, and what it would take

**The conformant fix is one line**: give `OSIZE` the target's `size_t`
type -- `TUVLONG` where `ewidth[TIND]` is 8, `TULONG` where it is 4.
*It is not obviously safe*, and the comment already in `com.c` says
why: `print("%d", sizeof(x))` is ordinary in Plan 9 and in old C, and
with a 64-bit `sizeof` it hands a variadic `%d` eight bytes and
corrupts every argument after it. **That trade is a tree-wide decision
with a new silent failure mode on the other side of it**, so it is
recorded rather than taken.

If anyone does take it, the control set is unusually good and should
all be run: `agree.c` (codegen identical but for the two known
orderings), `hostkencc.sh --probe` (ten `regpair-min`, eight
`regpair-probe`, `kc` and `vc` at `:242`), `clisp-mirror.sh --compile`
(31 TUs), `php-mirror.sh --compile` (515 TUs) and `--undefined` (283
symbols), plus a sweep for `%d`/`%u` against a `sizeof` argument,
which is the regression the change would introduce.

**The idiom recurs**, and the local fix does not cover it:
`zend_call_graph.c:72` is `sizeof(zend_send_arg_info) * ((int)opline->extended_value - 1)`
and `Optimizer/dfa_pass.c:556` and `zend_ssa.c:1610` have the same
shape with counts that are non-negative in practice. *Unmeasured, and
listed so the next one is recognised rather than re-derived.*
112 changes: 112 additions & 0 deletions docs/notes/php.md
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,118 @@ general form is worth keeping**: a header-only helper that calls into
a file the port does not compile is a link error here and silence on
gcc.

### CLOSED: `exit()` was kencc's 32-bit `sizeof`

Three one-liners on the VM settled it with no rebuild:

php -r 'echo "no-exit\n";' -> clean
php -r 'echo "exit0\n"; exit(0);' -> 4294971392
php -d memory_limit=1 -r 'exit(0);' -> same, same limit

**A script that just ends is clean**, so request shutdown and module
shutdown are both fine and it is `ZEND_EXIT` alone.

`exit` calls `zend_throw_unwind_exit`, which is
`zend_objects_new(&zend_ce_unwind_exit)`, and that is

emalloc(sizeof(zend_object) + zend_object_properties_size(ce))

with `zend_object_properties_size` (`zend_objects_API.h:83`):

sizeof(zval) * (ce->default_properties_count -
((ce->ce_flags & ZEND_ACC_USE_GUARDS) ? 0 : 1))

`UnwindExit` is `INIT_CLASS_ENTRY(..., "UnwindExit", NULL)` -- zero
properties -- and `ZEND_ACC_USE_GUARDS` is set only under
`CG(compiler_options) & ZEND_COMPILE_GUARDS`, **which nothing in php
ever sets**. So the subtraction is `0 - 1`, deliberately: `zend_object`
ends in `zval properties_table[1]`, so a class with no properties wants
one zval less than the struct, and `sizeof(zval) * -1` is 2**64-16,
which the caller's `+ sizeof(zend_object)` wraps back to **40**.

***UPSTREAM IS RIGHT AND THE COMPILER IS THE DEVIATION.*** C makes
`sizeof` a `size_t`, 64 bits here; kencc types it TULONG, 32 bits
(`cc/com.c:894`, on purpose). `6c -J -S` on the expression:

SUBL / SALL $4 / MOVLQZX

-- 32-bit subtract, 32-bit multiply, **zero-extend** -- so 4294967280
instead of 2**64-16, plus 56 is 4294967336, page-rounded to
**4294971392**. The same source under gcc asks for 40. *Both numbers
were run, not reasoned.*

Fixed in the vendored header by computing in `ptrdiff_t`, which is
signed and size_t's width on every target; kencc then emits
`MOVLQSX` / `SUBQ` / `SALQ` and agrees with gcc. 515 TUs still
compile. **The compiler change is recorded and not taken** --
`docs/notes/kencc.md` has the trade and the controls -- so *the same
idiom elsewhere is still exposed*, and three more instances in php's
optimizer are named there.

**Two reading errors on the way, both worth keeping.** I blamed
`display_text_table`'s padding loop from the line number alone; the
smoke test's reduction of that loop passes, and the real line was
`exit` in both programs. And my first reduction of the size expression
declared `default_properties_count` as `uint32_t` when it is `int`, so
gcc "confirmed" a 68GB request and briefly made upstream look broken on
Linux too. *A reduction that changes a type is not a reduction.*

### The smoke test ran, and it moved the bug

`php-smoke.php` on the VM: **44 of 45 cases pass**, including
`PHP_INT_SIZE` 8, `PHP_INT_MAX`, `strlen`, `max`, `str_repeat` at
100k, every arithmetic case, preg, json, and `/net/tcp/stats` read as
404 bytes. `PHP_OS = Plan9, PHP_VERSION = 8.5.12`. **The interpreter
evaluates correctly.**

***SO THE PHORONIX DIAGNOSIS WAS WRONG, AND THE TEST IS WHAT SAID
SO.*** Section 5 reproduces `display_text_table`'s padding loop over
the exact table php had printed, and it **passes** -- `$m` is 15, not
2**32. The crash is not in the loop.

**It is in `exit`.** The smoke test's own last line is

Hard memory limit of 268435456 bytes exhausted
(tried to allocate 4294971392 bytes) in php-smoke.php on line 194

and **line 194 is `exit($fails);`** -- printed *after* the test's own
total, so everything before it ran. Phoronix's line 854 is `exit;` in
the same `if($do_exit)` block. *Two unrelated programs, same line
kind, same byte count to the digit.*

**The size decomposes and the decomposition is the lead**:
4294971392 is 1048577 pages of 4096, i.e. a request of just over
2**32 rounded up by one page -- a length of **exactly 2**32** plus a
small header. And `2**32 == (uint32_t)-1 + 1`. So the shape is a
**32-bit -1 ZERO-extended into a 64-bit size and incremented**, not a
sign-extended one (that would be `(size_t)-1`, and `len+1` would be
0). *An identical size in two programs of different sizes is not
data-dependent.*

**What runs there**: `exit` compiles to ZEND_EXIT, which calls
`zend_bailout`, which is `LONGJMP(*EG(bailout), FAILURE)` --
and `zend_portability.h:427` makes that **`siglongjmp` into a
`sigjmp_buf`, unconditionally on non-Windows**; there is no
`HAVE_SIGSETJMP` test to answer. APExp's `sigsetjmp` is assembly
(`arch/amd64/setjmp.s`) and its `siglongjmp` is C
(`arch/amd64/notetramp.c:83`), and the two agree about the layout on
inspection -- 30 ints is 120 bytes against the 80 the pair uses, and
the C side passes `&jmpbuf[0]`, not the base. **So nothing is yet
ruled in or out**; what is established is that the fault is on the
bailout-and-shutdown path and not in the engine.

**Three one-liners separate the remaining possibilities**, with no
rebuild:

php -r 'echo "no-exit\n";' # ends without exit()
php -r 'echo "exit0\n"; exit(0);' # bailout
php -d memory_limit=-1 -r 'exit(0);' # same, limit removed

A clean first and a dying second puts it on `zend_bailout`; both
dying puts it on request shutdown generally; and the third turns a
clean `_exit(137)` into a real allocation failure or a fault, which
`acid` can name the way it named `zend_string.h:401`.

### The first real program: Phoronix, and a 2**32

`php -v` works, `php -h` works, and **the port has still never been
Expand Down
Loading
Loading