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 22cf4f185..d6e7ab9da 100644 --- a/docs/notes/php.md +++ b/docs/notes/php.md @@ -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 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 ""; - -} 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); -?> -``` - 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/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); 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.