diff --git a/CLAUDE.md b/CLAUDE.md index 398c32168..978c08ae5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1232,6 +1232,21 @@ and upstream's own commented-out direct call is the fix; and 6.8.6.4p1 forbids -- fixed in php, **deviation recorded in 0.6 and not taken**, since `ORETURN` with a left operand means "compute into the return location" and accepting void is a statement-tree rewrite. +***`system-info` RUNS AND ASKS FOR NOTHING MORE.*** What is left on +its output is three messages and only one is ours: +`disk_free_space(): Function not implemented`, which is libap's +`statvfs` stub and **a protocol limit** -- *9P2000 has no `Tstatfs`*, +the message set simply has no filesystem-level query, so Phoronix's +`Lock creation failed due to lack of disk space` has no fix on our +side. **And php's `config.h` answers `HAVE_STATVFS` YES against this +tree's own rule, deliberately**: with it yes php checks the return and +gives `false`; with it **no** php compiles neither arm, keeps +`bytesfree = 0` and returns SUCCESS -- *a silent `0.0`, which is the +quiet version of the same failure.* The other two are not ours: the +`strpos()/substr(): Passing null` deprecations are Phoronix's own code +not guarding a phodevi probe that read a Linux `/proc` path, and +`process exceeds 100 file descriptors` occurs nowhere under `sys/src` +-- it is the kernel growing an fd table, a notice and not a limit. ***AND THE SECOND ONE WAS EVERY MULTIPLICATION.*** `zend_multiply.h:135`'s fallback arm -- the one this port takes -- declared the product `long`, which is 64 bits on LP64 and **32 diff --git a/docs/notes/php.md b/docs/notes/php.md index 9db0a222f..e3cc41c48 100644 --- a/docs/notes/php.md +++ b/docs/notes/php.md @@ -1377,3 +1377,68 @@ control message. Phoronix wants sockets only "when running the Phoromatic Server", so it is the least valuable of the nine and the only one needing a public struct to move. *Deferred on cost, not on possibility.* + +## `disk_free_space` on a 9P namespace, and why the config.h is right anyway + +Phoronix's `system-info` runs with every module satisfied, and what is +left on its output is three messages. Only one of them is APExp's. + +**`disk_free_space(): Function not implemented`** (and +`disk_total_space()`, from `phodevi_disk:323`) is `strerror(ENOSYS)` +coming out of libap's `stat/statvfs.c`, which is a deliberate stub. +Its own comment makes the case and the case holds: **9P2000 has no +`Tstatfs`.** The message set is Tversion, Tauth, Tattach, Tflush, +Twalk, Topen, Tcreate, Tread, Twrite, Tclunk, Tremove, Tstat, Twstat — +`Tstat` returns a per-file `Dir` and there is no filesystem-level +query at all. (Linux's v9fs gets one from the `9P2000.L` dialect, +which Plan 9 servers do not speak.) A 9front file server will tell you +its free space through its own console — `echo df >>/srv/cwfs.cmd` — +but that is per-server, needs write access to a `srv` file, and is not +something a libc call can do generically. *So this is a platform +limit, established from the protocol rather than from the comment.* + +### The config.h claims a capability libap stubs, and that is the shape + +`HAVE_SYS_STATVFS_H 1` and `HAVE_STATVFS 1` are both answered yes while +`statvfs` is `errno = ENOSYS; return -1`. That is clisp's +`HAVE_WORKING_MPROTECT` and xz's `fsync` for the third time: **a +hand-answered capability file saying yes to something the library only +pretends to have.** + +***BUT THE ANSWER IS STILL THE RIGHT ONE, AND THE REASON IS WORTH +KEEPING.*** Read what php does with each answer: + +* **`HAVE_STATVFS` yes** — `filestat.c:224` calls `statvfs`, gets -1, + does `php_error_docref(E_WARNING, "%s", strerror(errno))` and + returns `FAILURE`, so `disk_free_space()` is **`false`** and says + why. +* **`HAVE_STATVFS` no** — neither arm is compiled, `bytesfree` keeps + its initialiser, and the function falls through to + `*space = bytesfree; return SUCCESS;`. `disk_free_space()` is + **`0.0`**, silently. + +*Turning the macro off is the SILENT version of the same failure*, and +`false` is what PHP documents for a platform that cannot answer, which +a caller can tell from a real zero with `=== false`. **The shape bites +when a caller does not check the return value; php checks.** Answered +yes, deliberately, with this paragraph as the reason. + +Phoronix then prints `Lock creation failed due to lack of disk space.` +because `pts_client`'s lock check compares the result against a +threshold, and `false < 1024`. *It would print the same thing for a +silent `0.0`*, so there is nothing to gain by changing our side. + +### The other two messages are not ours + +**`strpos()/substr(): Passing null to parameter #1 ... is deprecated`** +at `phodevi.php:728` and `pts_client:1659`/`:1661` is Phoronix's own +code handing `null` where PHP 8.1+ wants a string. The `null` is a +phodevi probe that found nothing — it reads Linux's `/proc` and `/sys` +layout, which Plan 9 does not have — and Phoronix does not guard the +result. **A deprecation, not an error**, and the run continues past it. + +**`process exceeds 100 file descriptors`** is not from this tree: the +phrase occurs nowhere under `sys/src`. It is the 9front kernel's own +advisory as it grows a process's fd table, and it is a notice rather +than a limit. Worth a second look only if the number keeps climbing, +which would be a leak rather than a large working set. diff --git a/docs/notes/plas_stack_readme.md b/docs/notes/plas_stack_readme.md index 4f9549efe..8786f1091 100644 --- a/docs/notes/plas_stack_readme.md +++ b/docs/notes/plas_stack_readme.md @@ -1,30 +1,49 @@ # The PLAS stack: Plan 9, Listener, SQLite, PHP -A web stack with no dynamic linker, no package manager and no database -server — PHP and SQLite on 9front, served by Plan 9's own listener. - -**STATUS: PART MEASURED, PART NOT, AND ONE PART IS KNOWN BROKEN.** The -architecture below is attractive and most of it is reachable. It does -not work today, and this file says which pieces are which, because a -guide that reads as instructions when it is a plan wastes the reader's -afternoon. Every claim marked **measured** was run; every one marked -**unmeasured** is a prediction with the command that would settle it. +A web stack on 9front with no dynamic linker, no package manager and no +database server: PHP 8.5 and SQLite 3.53 as static binaries, served +either by PHP's own built-in HTTP server or by Plan 9's listener. + +This is a guide to trying it, and a description of what the platform +does and does not give you. **Each claim carries its state**: *measured* +means it was run and the output is quoted; *unmeasured* means it follows +from the source but nobody has run it, and the command that would settle +it is given. The distinction is the useful part of this document — a +stack guide that reads as instructions when it is a plan costs you an +afternoon. | piece | state | |---|---| -| PHP 8.5.12 CLI binary | **measured**: builds, installs, runs, `php -v` exits 0 | -| SQLite 3.53.4 as `libsqlite.a` | **measured**: builds, in the default build | -| PHP *bound to* SQLite (PDO / sqlite3) | **absent**: no extension is compiled in | -| DOM, SimpleXML, ZIP | **absent**: same reason | -| POSIX file locking under SQLite | **BROKEN**: `fcntl` answers `EINVAL` | -| rc-httpd / listener wiring | **unmeasured** | -| per-request namespace isolation | **overstated** — see §4 | +| PHP 8.5.12 CLI, 23 extensions | **measured** — builds, installs, runs | +| SQLite 3.53.4 (`libsqlite.a` + `sqlite3` shell) | **measured** — builds, in the default build | +| `SQLite3` class in PHP | **measured on the build host**, not yet on 9front | +| file-backed SQLite **writes** | **unmeasured, and the one likely blocker** — see §3 | +| `php -S` built-in HTTP server | **unmeasured** — compiled in, see §4 | +| rc-httpd / `listen1` wiring | **unmeasured** — see §4 | +| reading `/net` and `/proc` from PHP | **measured** — see §5 | +| `disk_free_space()` | **known limitation** — see §6 | +| PDO | **not built** — use the `SQLite3` class | --- -## 1. What exists today +## 1. Getting it + +APExp does not install into `/`. `mk install` writes into the repository +(`$objtype/bin/ape`, `$objtype/lib/ape`), and `apexp-sh` union-mounts +those over `/bin` at run time. So everything below is run **inside +`apexp-sh`**, where the binaries are plain names on `$PATH`. + +```rc +cd /path/to/APExp +mk install +./apexp-sh +``` + +Then: -`php -v` on 9front: +``` +php -v +``` ``` PHP 8.5.12 (cli) (built: Nov 5 2026 16:46:25) (NTS) @@ -32,185 +51,170 @@ Copyright (c) The PHP Group Zend Engine v4.5.12, Copyright (c) Zend Technologies ``` -That is a real interpreter: 515 translation units, the Zend engine, -opcache, pcre2, the standard library, date, hash, json, lexbor, -random, reflection, spl and uri. It is built `--disable-all`, which in -PHP 8.5 still leaves eleven extensions because they are -unconditional. +Outside `apexp-sh` the binary is `/amd64/bin/ape/php`. -**What `--disable-all` leaves out is everything this stack needs.** -Running the Phoronix Test Suite under it says so in its own words: +## 2. What is in the build + +Twenty-three extensions are registered (`php -m` also shows `Core`, and +prints opcache as `Zend OPcache` under a separate heading): ``` -MISSING DOM REQUIRED - The Document Object Model is required for XML operations. -MISSING ZIP REQUIRED - ZIP support is required for file (de)compression. -MISSING SimpleXML REQUIRED - SimpleXML is required for XML operations. +date pcre hash json lexbor opcache random reflection standard spl uri +libxml dom simplexml zlib +posix pcntl bz2 readline openssl curl sqlite3 zip ``` -So *the binary is finished and the build is not*. There is no `PDO`, -no `sqlite3` driver, and `new PDO('sqlite:...')` in a script would -throw before touching a database. - -### The good news: the libraries are already here +The first line is what PHP 8.5 builds unconditionally — `--disable-all` +does not mean a small build. The second and third are APExp's, and they +link against libraries this tree already builds: `lib/xml2`, `lib/z`, +`lib/sqlite`, `lib/libressl`, `lib/curl`, `lib/zip`, `lib/bz2`, +`lib/readline`. Each is pulled in by `#pragma lib` in its own header, +the Plan 9 way, so `cmd/php/mkfile` names no libraries at all. -`sys/src/ape/lib` already carries ports of everything the missing -extensions link against, which is the hard half: +**Everything must be compiled in.** Plan 9 has no `dlopen`, so +`extension=` in `php.ini` and `dl()` do nothing: +`zend_portability.h:200` sets `ZEND_EXTENSIONS_SUPPORT` to 0 when +dynamic loading is absent. Adding an extension is three edits — +objects into `OFILES`, the `phpext_*_ptr` into +`internal_functions_cli.c`, the `HAVE_*` into `cfg/main/php_config.h` — +and a rebuild. -| extension | library it needs | in this tree? | -|---|---|---| -| `sqlite3`, `pdo_sqlite` | SQLite 3.53.4 | **yes** — `lib/sqlite`, and a `cmd/sqlite` shell | -| `dom`, `simplexml`, `xml` | libxml2 | **yes** — `lib/xml2` | -| `zip` | libzip + zlib | **yes** — `lib/zip`, `lib/z` | -| `curl` | libcurl | **yes** — `lib/curl` | -| `openssl` | LibreSSL | **yes** — `lib/libressl` | -| `mbstring` | oniguruma | **yes** — `lib/onig` | +**Not built**: PDO and `pdo_sqlite` (use the `SQLite3` class instead), +mbstring, GD (there is no libgd in the tree), and `ext/sockets` (which +needs `struct cmsghdr` and a real `recvmsg` in libap). -Adding one is three edits to `sys/src/ape/cmd/php/mkfile` and its -`cfg/`: the extension's objects into `OFILES`, its `phpext_*_ptr` into -`internal_functions_cli.c`, and `HAVE_*` into `cfg/main/php_config.h`. -*It has not been done, so none of it is measured.* +To check the build on the machine: ---- +```rc +php /path/to/APExp/sys/lib/tests/php-smoke.php +``` -## 2. The database layer has a blocker underneath it +Its Section 8 exercises each extension *through its library* — bz2 and +zlib round-trip bytes, sqlite3 creates and queries, openssl digests, zip +writes and reads an archive — rather than only asking +`extension_loaded()`. It exits with the failure count. **0 failures on +host PHP 8.3.6; not yet run on 9front.** -***THE DOCUMENT THIS REPLACES CLAIMED THE OPPOSITE OF THE TRUTH HERE, -AND IT IS THE CLAIM MOST WORTH GETTING RIGHT***, because believing it -is how someone ends up with a corrupted database rather than an error -message. +--- -The claim was: *"SQLite is traditionally bound to local contexts due to -POSIX network file-locking vulnerabilities. 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."* +## 3. The database layer: test this before building anything on it -**9P has no byte-range locking, and APExp has none either.** -`sys/src/ape/lib/ap/fcntl/fcntl.c:180` is: +SQLite works in memory today. What is unproven is a **file-backed +database being written to**, and there is a specific reason to doubt it. -```c -case F_GETLK: -case F_SETLK: -case F_SETLKW: - err = EINVAL; - break; -``` +### The mechanism SQLite's default unix VFS takes a SHARED lock to read and escalates -through RESERVED to EXCLUSIVE to write, **all with `fcntl` byte-range -locks**, on every transaction. With `EINVAL` coming back, those calls -fail. Plan 9's own exclusive-use bit (`DMEXCL`) is whole-file and is -not what SQLite asks for. +through RESERVED to EXCLUSIVE to write, all with **`fcntl` byte-range +locks**, on every transaction. APExp's `fcntl` answers `EINVAL` to all +three lock operations (`lib/ap/fcntl/fcntl.c:180`), because **9P has no +byte-range locking** — Plan 9's own exclusive-use bit (`DMEXCL`) is +whole-file and is not what SQLite asks for. -*Answering `EINVAL` is the right answer* — a lock stub that returned -success would let two writers both believe they held the file, which -is the corruption the original text promised to prevent. But it means -the question is not "can several machines share a database over 9P", -it is **"does a single process get a write transaction at all"**, and -that has never been run. +`EINVAL` is the right answer. A lock stub that returned success would +let two writers each believe they held the file, which is the corruption +the locks exist to prevent. But it means a file-backed write is expected +to fail with `disk I/O error` (`SQLITE_IOERR_LOCK`). -**The measurement is one command** and needs nothing built: +### The test — two commands, nothing to build ```rc sqlite3 /tmp/t.db 'create table t(a); insert into t values(1); select * from t;' ``` -- prints `1` → the write path works, and the open question is only - concurrency. -- an `SQLITE_IOERR`/`disk I/O error` → locking is the blocker, as - predicted here. - -**If it is the blocker, the remedy is known and is a build flag, not -new code.** SQLite ships alternative locking styles — dot-file locking -in particular, which needs only `create`/`remove` and therefore works -over 9P. They are compiled only under `SQLITE_ENABLE_LOCKING_STYLE`, -and the amalgamation in this tree has it at **0** -(`sys/src/external/sqlite/sqlite3.c:169`), while -`lib/sqlite/mkfile` passes no `-DSQLITE_*` at all. Turning it on and -opening with the `unix-dotfile` VFS is the first thing to try; a -Plan 9 VFS under `SQLITE_OS_OTHER` is the thorough version. +- prints `1` → the write path works and only concurrency is open. +- `disk I/O error` → locking is the blocker, as predicted. ---- +If it is the blocker, try the alternative locking style immediately: -## 3. What "statically linked" does and does not buy +```rc +sqlite3 -vfs unix-dotfile /tmp/t2.db 'create table t(a); insert into t values(1); select * from t;' +``` -The stack is statically linked, and **that is not a property of this -port — Plan 9 has no dynamic linker at all.** Every Plan 9 binary is -static. So: +### Why dotfile locking should work here -- The "no `glibc`/`musl` disparity, no shared-library conflicts" point - is true, and it is true of every program on the system. -- The same fact removes capability. **PHP cannot load an extension at - run time here**: `dl()` and `extension=` in `php.ini` do nothing, - because `zend_portability.h:200` sets `ZEND_EXTENSIONS_SUPPORT` to 0 - when there is no `dlopen`. Every extension must be compiled in. -- *That same line caused the first crash this port had*: opcache's - module shutdown destroyed globals whose constructor — reached only - through the zend-extension startup that is compiled out — had never - run. See `docs/notes/php.md`. +`unix-dotfile` uses **`mkdir()` as its atomic primitive** — +`dotlockLock()` calls `osMkdir(zLockFile, 0777)` and `dotlockUnlock()` +calls `osRmdir()`. `mkdir` on an existing directory fails, which makes +it a genuine test-and-set, and it is an ordinary 9P operation. No +`fcntl` is involved. -"Deployment is copying two files" is fair, with the caveat that the -binary is around the size of everything it statically contains. +**`unix-dotfile` and `unix-none` are registered unconditionally** — +only `unix-flock`, `unix-posix` and the Apple VFSes sit behind +`SQLITE_ENABLE_LOCKING_STYLE` (which is 0 in this amalgamation). So the +VFS is already in the `libsqlite.a` you have; nothing needs rebuilding +to try it. ---- +### Making it the default, for PHP -## 4. The namespace claim, narrowed +PHP cannot select a VFS from a script — `SQLite3::__construct` has no +VFS parameter and `sqlite3_open_v2` is called with a NULL VFS name +(`ext/sqlite3/sqlite3.c:143`), so PHP always gets the default. The knob +is a compile-time define in `sys/src/ape/lib/sqlite/mkfile`: -The original said Plan 9 "forks a localized namespace for incoming -requests", so a compromised application is "isolated within a -transient, severely restricted process space". +``` +CFLAGS=-c -I$SQSOURCE -DSQLITE_DEFAULT_UNIX_VFS='"unix-dotfile"' +``` -**A new namespace is not a restricted one.** `rfork n` gives the child -a *copy* of the parent's namespace — the same files, now privately -mutable. It becomes a sandbox only when the service script then does -the restricting itself: unmounting what it does not need, binding a -subtree over `/`, dropping the network directory. Plan 9 makes that -unusually easy and unusually cheap, and it is a genuine advantage over -a Unix CGI — but it is work the handler script has to do, and none of -the scripts below do it. +**Precedent**: the amalgamation already does exactly this for WASI +(`sqlite3.c:40394`), which is also a platform without POSIX locking. -The honest version: *Plan 9 gives you the cheapest per-request sandbox -of any system here, and you still have to build it.* +**This change has not been made and must not be made blind** — it +changes locking for every SQLite consumer in the tree, and dotfile +locking has a real cost: a crash leaves the lock *directory* behind and +the next opener blocks until it is removed by hand. Run the two shell +commands above first; they tell you whether it is needed and whether it +works. --- -## 5. Serving it — unverified, and flagged as such - -I have not run a web server on this system, and the 9front specifics -below are the part of this document I am least able to check from the -source tree. Treat §5 as a sketch to verify, not a recipe. - -**What I can state:** - -- APExp **does not install into `/`**. `mk install` puts binaries in - `$APEXPROOT/$objtype/bin/ape`, and `apexp-sh` union-mounts them over - `/bin`. So `cp /sys/src/cmd/php/php ...` is wrong; inside `apexp-sh` - the binary is just `php`, and outside it the path is - `/amd64/bin/ape/php`. -- **This build has no CGI SAPI.** It is configured `--disable-cgi`, so - there is no `php-cgi` binary. Under the **CLI** SAPI `header()` is a - no-op: PHP does not emit HTTP headers, so a CGI bridge must print - the status line and headers itself before running the script — which - is what the wrapper below does, and why the example scripts in §6 - must not rely on `header()`. -- `aux/listen1 tcp!*!8080 ` hands the program a *raw connection* - on its standard input and output. It does not set the CGI - environment, so a program behind `listen1` has to read and parse the - HTTP request itself. Using rc-httpd instead is what gets you CGI - variables. -- **Which 9front HTTP server and how it is configured, I did not - verify.** 9front ships `rc-httpd`, a set of rc scripts, and it is - configured by editing its own handler-selection script rather than - by an `httpd.conf`; the `cgi .php ...` line and `ip/httpd -h` in the - original look like the older Plan 9 `ip/httpd`, and may not apply. - **Check `/rc/bin/rc-httpd/` on the machine before copying anything - from here.** - -A CLI-SAPI bridge, written to the constraints above: +## 4. Serving it + +Two routes. Neither has been run on 9front. + +### 4.1 PHP's built-in server — the one to try first + +`sapi/cli/php_cli_server.c` and `php_http_parser.c` **are compiled into +this binary**, so `php -S` is available: + +```rc +php -S '*:8080' -t /usr/glenda/www +``` + +This is worth trying before any Plan 9 web server because it removes +every other variable. It is single-process by default — it forks only +when `PHP_CLI_SERVER_WORKERS` is set, which you should leave unset — and +needs only `socket`, `bind`, `listen`, `accept` and `select`, all of +which libap has. + +**Under `php -S`, `header()` works.** The built-in server is its own +SAPI (`cli-server`) with its own header handler +(`php_cli_server.c:552`). This is the opposite of the plain CLI SAPI — +see §4.3. + +### 4.2 rc-httpd, and a caution + +9front ships `rc-httpd`, a set of rc scripts configured by editing its +own handler-selection script rather than an `httpd.conf`. **Read +`/rc/bin/rc-httpd/` on the machine before copying anything**; recipes +that mention `ip/httpd -h` and a `cgi .php` line are for the older +Plan 9 `ip/httpd` and may not apply. + +`aux/listen1 tcp!*!8080 ` is the lower-level option. It hands the +program a **raw connection** on standard input and output and sets no +CGI environment, so a program behind `listen1` must parse the HTTP +request itself. rc-httpd is what gets you CGI variables. + +### 4.3 If you use CGI, the CLI SAPI emits no headers + +**There is no CGI SAPI in this build** (`--disable-cgi`, so no +`php-cgi`). Under the plain CLI SAPI — `php -f script.php` — `header()` +is a no-op, so a CGI bridge must print the status line and headers +itself: ```rc #!/bin/rc -# php-cgi -- run a .php file under the CLI SAPI and supply the headers +# php-cgi -- run a .php file under the CLI SAPI, supplying the headers # ourselves, because the CLI SAPI emits none. if(! test -f $PATH_TRANSLATED){ @@ -225,26 +229,25 @@ echo exec php -f $PATH_TRANSLATED ``` -*`rc` note*: `echo` with no argument prints the blank line that ends -the headers. Do not write `key=value` arguments anywhere in an rc -script — `=` is a syntax character and is legal only as a leading -assignment. +*rc notes*: `echo` with no argument prints the blank line that ends the +headers; and `=` is a syntax character in rc, legal only as a leading +assignment, so never write `key=value` as an argument to a command. --- -## 6. The example scripts, corrected - -### 6.1 Reading Plan 9's synthetic files — the part that already works +## 5. The part that is genuinely Plan 9: reading the namespace -This is the genuinely interesting half, it needs **no extension at -all**, and every function in it is in the build we have. +This needs **no extension at all** and every function in it is in the +build. It is also the most interesting thing the stack can do, because +on Plan 9 the kernel, the network stack and every process are files. ```php 'PLAS', 'metrics' => []]; if (is_readable('/dev/sysstat')) { - $out['metrics']['sysstat'] = preg_split('/\s+/', trim(file_get_contents('/dev/sysstat'))); + $out['metrics']['sysstat'] = + preg_split('/\s+/', trim(file_get_contents('/dev/sysstat'))); } $conns = []; @@ -259,39 +262,65 @@ $out['tcp'] = ['count' => count($conns), 'connections' => $conns]; echo json_encode($out, JSON_PRETTY_PRINT), "\n"; ``` -Changed from the original: `glob('/net/tcp/[0-9]*')` rather than `*`, -because `/net/tcp` also holds `clone` and `stats`, which are not -connection directories; `is_readable` rather than `file_exists`, -because a synthetic file can exist and refuse to be read; and no -`header()`, which does nothing under the CLI SAPI. +Three things in that script are load-bearing: -**Note for anyone extending this**: reading files under `/net` and -`/proc` is the whole appeal of the idea, and it is also where a long -run will meet Plan 9's `warning: process exceeds 100 file descriptors` -— seen already in the Phoronix run. Close what you open. +* **`glob('/net/tcp/[0-9]*')`, not `*`.** `/net/tcp` also contains + `clone` and `stats`, which are not connection directories. +* **`is_readable`, not `file_exists`.** A synthetic file can exist and + refuse to be read. +* **No `header()`** — it does nothing under `php -f` (§4.3). -### 6.2 The SQLite script — does not run yet +`/net/tcp/stats` is **measured**: `php-smoke.php` reads it as 404 bytes +on 9front. + +**Close what you open.** A long-running script walking `/net` or `/proc` +will meet the kernel's `warning: process exceeds 100 file descriptors`; +it is an advisory rather than a limit, but a climbing count is a leak. + +### Per-request namespaces: real, but you have to build it + +Plan 9 gives the cheapest per-request sandbox of any system here, and it +is not automatic. **`rfork n` gives the child a *copy* of the parent's +namespace — the same files, now privately mutable.** It becomes a +sandbox only when the handler then does the restricting itself: +unmounting what it does not need, binding a subtree over `/`, dropping +the network directory. None of the scripts in this document do that. + +--- -The original's `new PDO('sqlite:...')` needs `pdo_sqlite`, which is -not compiled in (§1), and would then need write locking, which is -`EINVAL` (§2). It is kept here as the target rather than as an -instruction, and it is worth attempting only after both of those are -dealt with, in that order. +## 6. Platform limits worth knowing before you start + +* **No dynamic loading.** Every Plan 9 binary is static; this is not a + property of the port. "Deployment is copying a file" is fair, with the + caveat that the file contains everything it uses. +* **No byte-range file locking** — §3. +* **`disk_free_space()` and `disk_total_space()` return `false`** with + `Function not implemented`. **9P2000 has no `Tstatfs`**: the protocol's + entire message set has no filesystem-level space query, so libap's + `statvfs` fails rather than reporting zeros, which would read as an + empty disk. A 9front file server will tell you on its own console + (`echo df >>/srv/cwfs.cmd`), but that is per-server and not reachable + from a libc call. Applications that check free space before writing — + the Phoronix Test Suite is one — will report a full disk. +* **Whole-second file timestamps**, no symlinks, no hard links, no + `/dev/fd`, no `mkfifo`. +* **No job control**, so anything expecting process groups will not get + them. --- -## 7. The shortest path from here - -1. `sqlite3 /tmp/t.db 'create table t(a);'` — does the database layer - work at all? One command, nothing to build. **Do this first**: if it - fails, everything downstream is blocked on one build flag. -2. Add `ext/sqlite3` and `ext/pdo_sqlite` to `cmd/php/mkfile` against - the `libsqlite.a` already in the tree. -3. Add `ext/dom`, `ext/simplexml` and `ext/zip` against `libxml2` and - `libzip` — which is also what makes Phoronix runnable, and would - make it a real test of the port rather than of its extension list. -4. Only then the server wiring, and verify §5 against the machine - rather than against this file. - -*The order matters: steps 2 and 3 are mkfile work with known inputs, -and step 1 can invalidate step 2 before it is started.* +## 7. A suggested order + +1. `php -v`, then `php sys/lib/tests/php-smoke.php`. Confirms the + interpreter and all 23 extensions on the machine, in one command. +2. The two `sqlite3` commands in §3. **Do this before designing + anything around a database** — it is the one piece with a known + mechanism pointing at failure, and it needs no build. +3. `php -S '*:8080'` with a one-line `>/srv/cwfs.cmd` — but + that is per-server and needs write access to a `srv` file, so it is + not reachable from a libc call. `lib/ap/stat/statvfs.c` fails rather + than filling the struct with zeros, **deliberately**, because zeros + read as "an empty file system". + **MEASURED** by php's `disk_free_space()`, which Phoronix calls + before taking a lock and which therefore prints + `Lock creation failed due to lack of disk space.` + ***AND php's `config.h` ANSWERS `HAVE_STATVFS` YES ON PURPOSE, + AGAINST THIS LIST'S OWN RULE.*** With it yes, `filestat.c:224` + checks the return, warns with `strerror(errno)` and gives `false`. + With it **no**, php compiles neither arm, keeps `bytesfree = 0` and + returns **SUCCESS** — a silent `0.0`. *Saying no to a capability we + do not have is the SILENT version of the same failure here*, and + `false` is what PHP documents for a platform that cannot answer. The + shape bites when a caller ignores the return value; this one checks. + - **No dynamic loading anywhere.** Plan 9 has no `dlopen`. perl has no XS extensions, bash has no `enable -f` and no loadable builtins, and anything that probes for shared-library support gets a truthful no.