a3256156908cecf11b6485efcbb1e1de84eb8dfc
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
07378506a7 |
harden(cac-lsphp): close the two remaining unvetted ini emissions; stop MINFO lying
Three non-blocking findings from the re-review of this branch. No design change:
the extension's fail-open-absolute invariant is untouched (still zero error
emitters, every RINIT return is SUCCESS) and both RINIT guards stay pure
narrowing.
1. entrypoint-lsphp.sh: 99-user-opcache.ini was still emitted unquoted
-------------------------------------------------------------------
Twenty-five lines below the mapping fix, the opcache override block
interpolated the raw env into an unquoted `echo` — the same injection class
the mapping fix closed. Measured on this branch's image, before this commit:
OPCACHE_MEMORY_MB=$'128\nprecision = 7\n; '
-> 99-user-opcache.ini gained a `precision = 7` line
-> lsphp -i reported precision => 7 => 7
WHP casts (int) and clamps 32-512 / 2000-32000 (site-pool-env.php), so this
is not exploitable today — but "the panel validates it" is precisely the
argument this branch already rejected for `domain`, and the panel is a
different repo on a different release cadence. Both siblings in the block are
now validated at the point of use (digits only, length-capped, range-checked)
and emitted double-quoted. A rejected value is dropped with a WARNING and the
image default applies; nothing here is ever fatal.
The accepted ranges are PHP's own limits for these directives (>= 8 MB;
[200, 1000000] files), deliberately a strict SUPERSET of the panel's clamps,
so widening a panel clamp later cannot start silently rejecting real sites.
The block now also removes a stale fragment when it has nothing valid to
write: the container filesystem outlives `docker restart`, so without that an
override that is later cleared — or rejected — would keep applying from the
previous boot's file.
2. 99-user-error-log.ini was written from an unvetted $user
--------------------------------------------------------
It was emitted before the INI_TOKENS_OK branch. Contained in practice (a
newline is inert inside the quotes, and a `${`-bearing user cannot exist
because useradd would have failed under `set -euo pipefail`), but "this
particular unvetted value happens to be contained" is the reasoning this
branch rejected one screenful up. Now gated identically.
Costs a rejected user nothing it needs: `log_errors = On` is already baked in
by 99-prod-overrides.ini, so PHP still logs — to stderr, i.e. `docker logs`,
which is more visible than a per-site file, not less. Verified fleet-wide
that no legitimate user reaches the branch (30 shared_ols sites, 4 hosts).
3. MINFO reported "active" for mappings RINIT ignores
---------------------------------------------------
The absolute-path guard was added to RINIT and MINFO kept testing only "both
values non-empty", so:
from=mnt/users/bob/site.com (relative -> INERT since the guard landed)
lsphp -i -> Rewriting => active
That row is what the post-deploy fleet canary greps to confirm parity is live,
so the diagnostic would have masked exactly the failure the canary exists to
find — and the C comment added by this branch documents it as the only runtime
signal. RINIT and MINFO now share one predicate pair
(cacpp_mapping_configured / cacpp_mapping_active) rather than two longhand
copies, which is what drifted. MINFO now distinguishes "inactive (mapping not
absolute)" from "inactive (unconfigured)" — different operational problems.
Pure reporting change: the predicates are side-effect-free and cannot fail, so
MINFO gains no error path.
Tests: two new .phpt cover both directions of the MINFO fix (009 relative
mapping must report inactive, 010 well-formed mapping must still report active),
so tightening it cannot overshoot into the opposite lie. The build gate's
EXPECTED count is derived from `ls tests/*.phpt`, so it picked them up: 10/10.
Non-vacuity, all five demonstrated by mutation:
- opcache quoting reverted -> injection lands, `precision => 7` observed
- error-log gate removed -> fragment written from the unvetted user
- MINFO reverted to non-empty -> 009 FAILS, build gate exits 1
- MINFO forced always-inactive -> 010 FAILS, build gate exits 1
- all restored -> 10/10, build exit 0
|
||
|
|
433e74975c |
fix(cac-lsphp): enable .user.ini support (LSPHP_ENABLE_USER_INI)
php-lsapi compiles .user.ini support in but leaves it DISABLED by default.
sapi/litespeed/lsapi_main.c has `static int parse_user_ini = 0;` and only
sets it when the process environment contains LSPHP_ENABLE_USER_INI=on.
WHP never set it, so lsphp never entered the user-ini chain at all.
The failure was silent: phpinfo() still reports user_ini.filename=.user.ini
and user_ini.cache_ttl=300, because those are core INI defaults that are
simply inert under this SAPI. Every other WHP PHP tier (cac, cac-fpm,
cac-litespeed) honors .user.ini, so shared_ols was quietly inconsistent.
Impact found in production (whp01/whp02/sdbees/TrueSelfCA, 29 sites):
- Per-site memory_limit / max_input_vars overrides were ignored. A Divi
site's max_input_vars stayed at the 2000 default while its .user.ini
asked for 20000.
- Wordfence's auto_prepend_file WAF never loaded on ANY shared_ols site.
11 sites had the plugin installed and reporting "Extended Protection"
enabled while the prepend was never executed.
Verified on a live sidecar (shadowdao.com, whp01) before this commit by
injecting the env var via whp.container_types.startup_env and recreating:
before: auto_prepend_file=/scripts/cac-lsphp-normalize.php (WAF absent)
after: auto_prepend_file=/home/shadowdao/public_html/wordfence-waf.php
wordfence-waf.php present in get_included_files()
class_exists('wfWAF') === true
The platform normalize prepend still chains in behind Wordfence's bootstrap,
so DOCUMENT_ROOT canonicalisation is not lost.
Set in two places on purpose: the Dockerfile ENV makes the value visible in
`docker inspect` and survives an entrypoint override, and the entrypoint
re-exports it with the same default because the runuser fallback exec path
resets the environment. Still overridable per-container
(LSPHP_ENABLE_USER_INI=off) as an escape hatch for a site whose legacy
cPanel-generated .user.ini has not been remediated yet.
NOTE: enabling this activates every previously-inert .user.ini at once.
Audit the fleet for stale cPanel directives before rolling this image —
session.save_path values under /var/cpanel/ that do not exist in the
container, memory_limit above the cgroup cap, and upload_max_filesize
values below the platform default were all found and remediated first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
1756d496e5 |
detect-memory: raise PHP_WORKER_ESTIMATE_MB default 60→128
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m22s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 1m20s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 1m15s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 1m19s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 1m17s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 1m25s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 1m16s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 1m17s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 1m14s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 1m21s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m16s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 1m15s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 1m23s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 1m15s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 27s
The 60 MB worker estimate was optimistic for plugin-heavy WordPress
and WooCommerce stacks. Concrete measurement on alphaone 2026-06-01:
Container memory : 1024 MiB (later 2048 MiB)
Pool sized by formula : pm.max_children = (1024-100)/60 = 15
Actual per-worker RSS : ~193 MB (anon+file+shmem from kernel OOM dumps)
Worst-case peak : 15 × 193 MB ≈ 2.9 GB
That math put traffic-burst peak demand well over the container cap,
producing 1,586 cumulative oom_kills across alphaone's two containers
over 18 days and intermittent fork-starvation for unrelated tenants
on the host.
128 MB is a more realistic baseline: closer to actual WP+Woo+page-
builder worker footprint, still conservative enough that lighter
sites continue to get reasonable concurrency. The matrix at common
container tiers:
Tier (MiB) | old children | new children | new peak demand
256 | 2 (floored) | 2 (floored) | ~256 MB
512 | 6 | 3 | ~384 MB
768 | 11 | 5 | ~640 MB
1024 | 15 | 7 | ~896 MB
2048 | 15 (capped*) | 15 | ~1.9 GB
(* old formula returned 32 at 2 GiB but production containers were
booted at lower tiers and never recalculated; see whp01 audit.)
Existing containers keep their boot-time pm.max_children until they
are recreated — this change only affects new containers. Customers
or operators can override per-container via FPM_MAX_CHILDREN env.
|