From 3ff4d422dc3ac2f5022b15e5a82aa4fd994ab10a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 17:18:19 +0000 Subject: [PATCH 1/3] php: the smoke test moves the crash from the loop to exit() 44 of 45 cases pass on the VM, including PHP_INT_SIZE 8, PHP_INT_MAX, strlen, max, str_repeat at 100k, preg, json, and /net/tcp/stats read as 404 bytes. The interpreter evaluates correctly, which is the thing `php -v' could not say. Section 5 reproduces display_text_table's padding loop over the exact table php had printed before dying, and it passes: $m is 15, not 2**32. So the Phoronix diagnosis was wrong and the test is what said so. The smoke test dies on its own line 194, which is `exit($fails);' -- after printing its total -- and Phoronix's line 854 is `exit;'. Two unrelated programs, the same line kind, the same byte count. The size decomposes: 4294971392 is 1048577 pages of 4096, a request of just over 2**32 rounded up one page, i.e. a length of exactly 2**32 plus a header. 2**32 is (uint32_t)-1 + 1, so the shape is a 32-bit -1 zero-extended into a 64-bit size and incremented -- not sign-extended, which would have given 0. An identical size in two programs of different sizes is not data-dependent. exit compiles to ZEND_EXIT, which calls zend_bailout, which is LONGJMP into EG(bailout) -- and zend_portability.h:427 makes that siglongjmp into a sigjmp_buf unconditionally on non-Windows, with no HAVE_SIGSETJMP to answer. APExp's sigsetjmp is assembly and its siglongjmp is C, and on inspection the two agree about the layout, so nothing is ruled in or out yet; what is established is that the fault is on the bailout-and-shutdown path and not in the engine. Also fixed an instrument fault. The PHP_INT_MIN case was eq("PHP_INT_MIN", PHP_INT_MIN, -9223372036854775807 - 1); and on Plan 9 it printed `got -9223372036854775807-1' against `want -9.223372036854776E+18': the got side is correct and the EXPECTED side carried the bug, because the subtraction overflowed into a float. An assertion whose expected value is computed by the thing under test reports a fault as a wrong expectation -- the same shape as the xcheck that read both sides out of the field it was testing. It is four direct cases now, and the overflow is a finding in its own right: -9223372036854775807 - 1 must stay an int. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WGAwvvTwDg2yknFkmZ3qzs --- docs/notes/php.md | 56 +++++++++++++++++++++++++++++++++++++ sys/lib/tests/php-smoke.php | 28 ++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/docs/notes/php.md b/docs/notes/php.md index 22cf4f185..247360569 100644 --- a/docs/notes/php.md +++ b/docs/notes/php.md @@ -746,6 +746,62 @@ 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. +### 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 diff --git a/sys/lib/tests/php-smoke.php b/sys/lib/tests/php-smoke.php index d41624e79..a253ecfc1 100644 --- a/sys/lib/tests/php-smoke.php +++ b/sys/lib/tests/php-smoke.php @@ -40,6 +40,12 @@ * other, which is exactly the shape that produces a high half full * of junk. * + * ***ON THIS PORT THE EXIT STATUS IS NOT USABLE YET, SO READ THE + * COUNT LINE.*** `exit()' itself asks the allocator for 2**32 bytes + * (see `docs/notes/php.md'), so the process dies with 137 after + * printing its total. The last line of output is the result until + * that is fixed; the status will mean what it says again afterwards. + * * Exit status is the failure count. */ @@ -73,7 +79,27 @@ function eq($what, $got, $want) { */ eq("PHP_INT_SIZE", PHP_INT_SIZE, 8); eq("PHP_INT_MAX", PHP_INT_MAX, 9223372036854775807); -eq("PHP_INT_MIN", PHP_INT_MIN, -9223372036854775807 - 1); +/* + * ***THE FIRST VERSION OF THIS CASE PUT ITS FINDING IN THE `want' + * COLUMN, WHICH IS BACKWARDS.*** It was + * + * eq("PHP_INT_MIN", PHP_INT_MIN, -9223372036854775807 - 1); + * + * and on Plan 9 it printed + * + * got -9223372036854775807-1 want -9.223372036854776E+18 + * + * -- the `got' side is correct and the EXPECTED side is the bug: the + * subtraction overflowed into a float. *An assertion whose expected + * value is computed by the thing under test reports the fault as a + * wrong expectation*, which is the same shape as the xcheck that read + * both sides out of the field it was testing. Asked directly now. + */ +eq("PHP_INT_MIN + PHP_INT_MAX is -1", PHP_INT_MIN + PHP_INT_MAX, -1); +eq("PHP_INT_MIN is an int", is_int(PHP_INT_MIN), true); +eq("LONG_MIN by subtraction stays an int", + is_int(-9223372036854775807 - 1), true); +eq("...and equals PHP_INT_MIN", (-9223372036854775807 - 1) === PHP_INT_MIN, true); eq("a literal above 2**32", 4294967296 + 4096, 4294971392); eq("1 << 32", 1 << 32, 4294967296); eq("a 2**32 round trip through a variable", (function () { $x = 4294967296; return $x; })(), 4294967296); From aedc8da1c5d0e0126d896e9d21c4ef71b50af89d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 17:19:16 +0000 Subject: [PATCH 2/3] plas_stack_readme: keep one copy The merge of main left the corrected document and the original concatenated -- 500 lines where the rewrite is 297. Same content as before, with the superseded copy removed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WGAwvvTwDg2yknFkmZ3qzs --- docs/notes/plas_stack_readme.md | 203 -------------------------------- 1 file changed, 203 deletions(-) diff --git a/docs/notes/plas_stack_readme.md b/docs/notes/plas_stack_readme.md index f40cdbe67..4f9549efe 100644 --- a/docs/notes/plas_stack_readme.md +++ b/docs/notes/plas_stack_readme.md @@ -1,4 +1,3 @@ - # The PLAS stack: Plan 9, Listener, SQLite, PHP A web stack with no dynamic linker, no package manager and no database @@ -296,205 +295,3 @@ dealt with, in that order. *The order matters: steps 2 and 3 are mkfile work with known inputs, and step 1 can invalidate step 2 before it is started.* -======= -# The PLAS Stack: Plan 9, Listen/Rc-httpd, Apache-less, SQLite, PHP -A Theoretical Architecture and Setup Guide for Statically Linked Web Environments on 9front. - ---- - -## 1. Architectural Overview -The **PLAS Stack** represents a radical departure from conventional web infrastructure. By coupling the distributed, file-centric philosophy of **9front (Plan 9)** with a completely self-contained, statically compiled **PHP binary containing an embedded SQLite engine**, this stack eliminates the need for dynamic linker dependencies, complex package managers, heavy relational database servers, and multi-layered container runtimes. - -``` - +-----------------------------------------+ - | Incoming HTTP Request | - +-----------------------------------------+ - | - v - +-----------------------------------------+ - | Plan 9 Native Listener (rc-httpd) | - +-----------------------------------------+ - | (Forks transient namespace) - v - +-----------------------------------------+ - | Statically Linked PHP Binary | - | (Embedded SQLite Engine / PDO) | - +-----------------------------------------+ - / | \ - v v v - +--------------------+ +-------------+ +--------------------+ - | local .sqlite file | | /net/tcp | | /dev/sensors | - | (Shared via 9P) | | (Plan 9 IP) | | (Synthetic files) | - +--------------------+ +-------------+ +--------------------+ -``` - -### Core Advantages: -1. **The Ultimate Edge "Un-Appliance":** Deployment is reduced to copying exactly two elements: a single executable file and a flat data file. There are no shared library conflicts (`glibc`/`musl` disparities), zero system drift, and a tiny overall storage footprint. -2. **Namespace Sandbox Isolation:** Plan 9 forks a localized namespace for incoming requests. If a PHP application is compromised, the attacker is isolated within a transient, severely restricted process space. -3. **9P-Native Integration:** PHP can interact directly with synthetic filesystems (like `/net`, `/proc`, and `/dev`), enabling the web interface to read hardware metrics or manage networking interfaces naturally via standard file I/O operations. -4. **Distributed 9P DB Locks:** SQLite is traditionally bound to local contexts due to POSIX network file-locking vulnerabilities (e.g., NFS). Plan 9's native **9P protocol** guarantees coherent file locking and atomic transactions, allowing multiple terminal nodes to safely write to a single centralized `.sqlite` file hosted on a file server (`cwfs` or `hjfs`). - ---- - -## 2. Theoretical Configuration & Deployment Guide - -This section outlines a theoretical methodology to hook your statically compiled PHP binary into Plan 9's native networking and web hosting layers. - -### Phase A: Organizing the Web Directory structure -In Plan 9, the standard web root is traditionally served from `/usr/web`. We will configure a dedicated workspace for our PLAS applications. - -```rc -# Create our application and data directories inside the web root -mkdir -p /usr/web/plas/bin -mkdir -p /usr/web/plas/db -mkdir -p /usr/web/plas/htdocs - -# Move your static PHP binary into place and make it executable -cp /sys/src/cmd/php/php /usr/web/plas/bin/php -chmod +x /usr/web/plas/bin/php - -# Pre-initialize an empty SQLite tracking database file -touch /usr/web/plas/db/app.sqlite -``` - -### Phase B: Hooking into `rc-httpd` via CGI -Plan 9’s `rc-httpd` maps specific directories or extensions to executable command scripts using a simple configuration style. To pass incoming scripts to your static binary, create a handling gateway wrapper script. - -Create the wrapper script at `/usr/web/plas/bin/php-cgi`: - -```rc -#!/bin/rc -# php-cgi - A simple bridge passing CGI parameters into our static PHP binary - -# Read standard CGI environment variables passed down by rc-httpd -# Plan 9 populates execution environments under /env/ - -if(! test -f $PATH_TRANSLATED){ - echo 'Status: 404 Not Found' - echo 'Content-Type: text/html' - echo '' - echo '

404 File Not Found

' - exit -} - -# Execute the static binary in CGI mode, parsing the target script -exec /usr/web/plas/bin/php -c /usr/web/plas/bin/php.ini -f $PATH_TRANSLATED -``` -Ensure the script is given execution permissions: -```rc -chmod +x /usr/web/plas/bin/php-cgi -``` - -Next, append a routing rule to your `/sys/lib/httpd.conf` config file so files ending in `.php` are piped into your wrapper: -``` -# /sys/lib/httpd.conf addition -# Direct requests targeting .php scripts to our compiled binary bridge -cgi .php /usr/web/plas/bin/php-cgi -``` - -### Phase C: Initializing the Web Daemon -To fire up the listening service on standard HTTP port 80, invoke the network listener application: -```rc -# Run the native Plan 9 HTTP daemon -ip/httpd -h /usr/web -``` - -Alternatively, if you want an isolated server instance bound specifically to your directory without messing with the global system configuration, use `aux/listen1`: -```rc -# Instantly stream incoming TCP traffic on port 8080 directly through the CGI wrapper -aux/listen1 -v tcp!*!8080 /usr/web/plas/bin/php-cgi -``` - ---- - -## 3. Practical Integration Scripts - -### Script 1: Interacting with SQLite database -Save this script as `/usr/web/plas/htdocs/index.php`. It demonstrates how the statically compiled engine processes structural queries internally on a local file resource. - -```php -setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - - // Create a demo table structure if initialization is fresh - $db->exec("CREATE TABLE IF NOT EXISTS system_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - event TEXT, - details TEXT - )"); - - // Log the current hit event - $stmt = $db->prepare("INSERT INTO system_logs (event, details) VALUES (:event, :details)"); - $stmt->execute([ - ':event' => 'HTTP_REQUEST', - ':details' => 'Client accessed endpoint via user-agent: ' . ($_SERVER['HTTP_USER_AGENT'] ?? 'Unknown') - ]); - - // Query historical entry points - $results = $db->query("SELECT * FROM system_logs ORDER BY id DESC LIMIT 5"); - - echo "

PLAS Stack Deployment Status: Operational

"; - echo "

Recent Database Entries (SQLite Embedded Engine):

"; - echo "
    "; - foreach ($results as $row) { - echo "
  • [" . htmlspecialchars($row['timestamp']) . "] " . htmlspecialchars($row['event']) . " - " . htmlspecialchars($row['details']) . "
  • "; - } - echo "
"; - -} catch (PDOException $e) { - echo "

Database Error: " . htmlspecialchars($e->getMessage()) . "

"; -} -?> -``` - -### Script 2: Exposing Plan 9 System/Network States as JSON -Because Plan 9 presents kernel states as accessible flat text structures, your static PHP application can read core metrics directly without invoking external shell scripts. Save this example as `/usr/web/plas/htdocs/sysstat.php`. - -```php - 'PLAS (Plan 9, Listen, SQLite, PHP)', - 'status' => 'healthy', - 'system_metrics' => [] -]; - -// 1. Read System Uptime and CPU utilization metrics out of /dev/sysstat -if (file_exists('/dev/sysstat')) { - $sysstat = file_get_contents('/dev/sysstat'); - $metrics = preg_split('/\s+/', trim($sysstat)); - $response['system_metrics']['raw_sysstat'] = $metrics; -} - -// 2. Read Active TCP Network connections straight out of Plan 9 synthetic networking stack -if (file_exists('/net/tcp')) { - $tcp_connections = []; - $connections = glob('/net/tcp/*'); - foreach ($connections as $conn) { - if (is_dir($conn) && file_exists($conn . '/status')) { - $status = trim(file_get_contents($conn . '/status')); - if (!empty($status)) { - $tcp_connections[] = [ - 'connection_id' => basename($conn), - 'status' => $status - ]; - } - } - } - $response['network_state'] = [ - 'active_tcp_directories' => count($connections), - 'connections' => $tcp_connections - ]; -} - -echo json_encode($response, JSON_PRETTY_PRINT); -?> -``` - From a7fcb4a4a2d2da9826a1e3b0ef8d1a6897920406 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 17:45:43 +0000 Subject: [PATCH 3/3] php: exit() asked for 4GB because kencc's sizeof is 32 bits Three one-liners settled where it was, with no rebuild: a script that just ends is clean, `exit(0)' dies, and the memory limit is not involved. 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), which is emalloc(sizeof(zend_object) + zend_object_properties_size(ce)) and that function is sizeof(zval) * (ce->default_properties_count - ((ce->ce_flags & ZEND_ACC_USE_GUARDS) ? 0 : 1)) UnwindExit has 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; cc/com.c:894 types it TULONG, 32 bits, on purpose and with its reason in a comment. `6c -J -S' on the expression is SUBL / SALL $4 / MOVLQZX -- a 32-bit multiply and then a ZERO-extend -- so 4294967280 instead of 2**64-16, plus 56 is 4294967336, which the allocator page-rounds to 4294971392: the number from the Phoronix crash and from the smoke test, to the digit. The same source compiled by gcc asks for 40. Both numbers were run. 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. It is one line -- TUVLONG where ewidth[TIND] is 8 -- and it would introduce a new silent failure wherever a sizeof is printed with %d, which is ordinary in Plan 9's own sources. docs/notes/kencc.md has both -S listings, the gcc control, the trade, and the control set to run if it is ever taken; 0.6's inventory carries the entry; and three more instances of the idiom in php's optimizer are named, unmeasured. Two reading errors on the way, both recorded. I blamed display_text_table's padding loop from the line number alone, and the smoke test's reduction of that loop passes -- the real line was `exit' in both programs. And my first reduction 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 as well. A reduction that changes a type is not a reduction. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WGAwvvTwDg2yknFkmZ3qzs --- CLAUDE.md | 28 +++++++- docs/notes/kencc.md | 71 ++++++++++++++++++++ docs/notes/php.md | 56 +++++++++++++++ docs/releases/0.6.md | 16 +++++ sys/src/external/php/Zend/zend_objects_API.h | 40 ++++++++++- 5 files changed, 205 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 11d7d1c17..5a9418cc7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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** -- @@ -1834,6 +1844,18 @@ the index, so that nothing here is a surprise. sign-extended with no diagnostic. `strtoumax` calling `strtoull` without `` 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 diff --git a/docs/notes/kencc.md b/docs/notes/kencc.md index 3e83d78ac..ab6a14b3c 100644 --- a/docs/notes/kencc.md +++ b/docs/notes/kencc.md @@ -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.* diff --git a/docs/notes/php.md b/docs/notes/php.md index 247360569..d6e7ab9da 100644 --- a/docs/notes/php.md +++ b/docs/notes/php.md @@ -746,6 +746,62 @@ 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 diff --git a/docs/releases/0.6.md b/docs/releases/0.6.md index 1894cc900..c165e50f8 100644 --- a/docs/releases/0.6.md +++ b/docs/releases/0.6.md @@ -1327,6 +1327,22 @@ distinction is the point of the list. rather than one that fails to compile. The prediction and what would refute it are in `docs/releases/0.7.md`. +- **`sizeof` has type TULONG (32 bits on a 64-bit target) where C + requires `size_t` (64), and that breaks ARITHMETIC as well as + unprototyped arguments.** `cc/com.c:894`, deliberately, with the + reason in a comment: a 64-bit `sizeof` would hand a variadic `%d` + eight bytes. **MEASURED**: `sizeof(T) * (n - 1)` with `n == 0` + compiles to `SUBL` / `SALL $4` / `MOVLQZX` -- 32-bit multiply, then + ZERO-extend -- giving 4294967280 where gcc gives 2**64-16. php's + `zend_object_properties_size` is exactly that shape, and it made + every `exit()` in every script ask the allocator for 4GB. *Fixed in + php, not in the compiler*: the conformant change is one line + (`TUVLONG` where `ewidth[TIND]` is 8) and would introduce a new + silent failure wherever a `sizeof` is printed with `%d`, which is + ordinary in Plan 9's own sources. `docs/notes/kencc.md` has both + `-S` listings, the gcc control, and the full control set to run if + the trade is ever taken. + - **A bare `hidden` or `visible` specifier is swallowed only when the previous token is not a name, and a `;` does not clear that.** `lex.c` drops the unprefixed GNU visibility attributes — the Linux diff --git a/sys/src/external/php/Zend/zend_objects_API.h b/sys/src/external/php/Zend/zend_objects_API.h index 86c3a49f8..70b08532f 100644 --- a/sys/src/external/php/Zend/zend_objects_API.h +++ b/sys/src/external/php/Zend/zend_objects_API.h @@ -82,9 +82,43 @@ static zend_always_inline void zend_object_release(zend_object *obj) static zend_always_inline size_t zend_object_properties_size(zend_class_entry *ce) { - return sizeof(zval) * - (ce->default_properties_count - - ((ce->ce_flags & ZEND_ACC_USE_GUARDS) ? 0 : 1)); + /* + * ***THIS SUBTRACTION IS MEANT TO GO NEGATIVE, AND THE WHOLE + * EXPRESSION HAS TO BE EVALUATED AT size_t WIDTH FOR THAT TO + * WORK.*** `zend_object' ends in `zval properties_table[1]', so + * a class with NO properties wants one zval LESS than the + * struct -- the count is `int', the result is -1, and + * `sizeof(zval) * -1' is 2**64-16, which the caller's + * `sizeof(zend_object) + ...' wraps back to 40. Upstream is + * right and relies on C: `sizeof' yields `size_t'. + * + * **kencc gives `sizeof' the type TULONG, which on amd64 is 32 + * BITS** (`cc/com.c:894', deliberately, so an unprototyped call + * is not handed eight bytes). `size_t' here is 64. So the + * multiply happened in 32 bits and the result was ZERO-extended: + * `6c -S' is `SUBL' / `SALL $4' / **`MOVLQZX`**, giving + * 4294967280 where gcc gives 2**64-16. + * + * Every `exit()' in every script then asked the allocator for + * 4294967336 bytes, which it page-rounds to 4294971392 -- the + * number that killed `php -v'-sized scripts and the Phoronix + * Test Suite alike, at `zend_throw_unwind_exit' -> + * `zend_objects_new(&zend_ce_unwind_exit)', a class with zero + * properties and no guards. + * + * Doing the subtraction and the multiply in `ptrdiff_t' -- which + * is signed and size_t's width on every target -- makes kencc + * emit `MOVLQSX' / `SUBQ' / `SALQ' and agree with gcc. It is a + * no-op anywhere `sizeof' already has the width C requires. + * + * *The compiler is the deviation here, not php*; see + * `docs/notes/kencc.md'. This is the local fix, and the same + * idiom in any other package is still exposed. + */ + ptrdiff_t n = (ptrdiff_t)ce->default_properties_count + - ((ce->ce_flags & ZEND_ACC_USE_GUARDS) ? 0 : 1); + + return (size_t)(n * (ptrdiff_t)sizeof(zval)); } /* Allocates object type and zeros it, but not the standard zend_object and properties.