feat(cac-lsphp): guarantee $_SERVER path parity via a PHP extension
A site moved from cac-fpm to cac-lsphp must see byte-identical $_SERVER['DOCUMENT_ROOT'] and ['SCRIPT_FILENAME'] (/home/<user>/...). The auto_prepend_file normaliser that did this was PHP_INI_PERDIR, so any site with its own .user.ini auto_prepend_file silently displaced it — the state 7 live shared_ols sites (Wordfence, cPanel imports) are actually in. Hardening the hook was not an option either: making our prepend win would have disabled those Wordfence WAFs. Replace it with cac_path_parity, a small PHP extension that rewrites the filesystem-path $_SERVER keys from RINIT. RINIT cannot be displaced by .user.ini, and it occupies no userland hook, so the customer's own auto_prepend_file stays the only prepend in play and keeps working. The mapping lives in two PHP_INI_SYSTEM settings, which .user.ini (PERDIR / USER only) and ini_set() cannot reach. Mechanism is a path-component-bounded string prefix swap, not realpath(): byte-identical to cac-fpm by construction (realpath would resolve a customer's own symlinked public_html to some third path), no syscall, and no failure path. Every guard fails open and leaves $_SERVER untouched; nothing here can warn, throw or 500 a site. Unconfigured it is fully inert, so cac-fpm and cac-litespeed are unaffected. Built in a separate Dockerfile stage keyed off the existing ARG PHPVER — gcc/phpize/headers never reach the shipped image (verified absent; the image grows ~155kB), and a base-image PHP bump recompiles with no human step. A `lsphp -i | grep` assertion fails the build if the .so does not load, so an image can never ship having silently lost parity. The entrypoint selects the extension when present and removes any stale prepend ini left by an older image; if the extension is somehow not loadable it falls back to the old normaliser and logs a WARNING rather than losing normalisation entirely. It also now logs the active parity mode, and warns when lsphp reports no ini scan dir (previously silent). Probe lsphp with `-i` only: it is the LSAPI SAPI, not the CLI, and answers `-m`/`-r` by printing usage and exiting 0 — a `lsphp -m | grep` check never matches and never errors, which is the exact class of silent always-false assertion this change exists to remove. Verified: 6 .phpt tests; tests/fpm-parity-check.sh proves under the FPM SAPI that with a customer .user.ini auto_prepend_file present both keys are still corrected AND the customer's prepend still runs, and that the old mechanism does not; and in a real built cac-lsphp:php83 container that SCRIPT_FILENAME is rewritten, the customer prepend still fires, and another tenant's path is left untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,34 @@
|
||||
|
||||
ARG OLS_VERSION=1.8.4
|
||||
ARG PHPVER=83
|
||||
|
||||
## ---- stage 1: build the cac_path_parity extension --------------------------
|
||||
## $_SERVER['DOCUMENT_ROOT']/['SCRIPT_FILENAME'] parity with cac-fpm, enforced
|
||||
## from RINIT so a customer's .user.ini cannot displace it — see
|
||||
## ext/cac-path-parity/cac_path_parity.c for why this is an extension and not an
|
||||
## auto_prepend_file. Built against THIS image's own lsphp so the API/ABI
|
||||
## (`PHP API` / extension_dir) always match; a PHP version bump in the base
|
||||
## image therefore recompiles rather than silently loading a stale .so.
|
||||
##
|
||||
## Separate stage on purpose: the compiler + headers (~400MB) stay out of the
|
||||
## shipped image, which gains only the ~40KB .so. Costs ~1-2 min of CI per PHP
|
||||
## version; both stages share the same base layer, so no extra pull.
|
||||
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER} AS ext-build
|
||||
ARG PHPVER=83
|
||||
COPY ./ext/cac-path-parity /usr/src/cac-path-parity
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
build-essential autoconf pkg-config \
|
||||
lsphp${PHPVER}-dev && \
|
||||
cd /usr/src/cac-path-parity && \
|
||||
/usr/local/lsws/lsphp${PHPVER}/bin/phpize && \
|
||||
./configure --enable-cac-path-parity \
|
||||
--with-php-config=/usr/local/lsws/lsphp${PHPVER}/bin/php-config && \
|
||||
make -j"$(nproc)" && \
|
||||
mkdir -p /build-out && \
|
||||
cp modules/cac_path_parity.so /build-out/
|
||||
|
||||
## ---- stage 2: the shipped sidecar image ------------------------------------
|
||||
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER}
|
||||
ARG PHPVER=83
|
||||
ENV PHPVER=${PHPVER}
|
||||
@@ -54,6 +82,31 @@ RUN bash -c 'set -e; \
|
||||
cp /etc/lsws-templates/lsphp-overrides.ini "$SCAN_DIR/99-prod-overrides.ini"; \
|
||||
echo "wrote overrides to $SCAN_DIR"'
|
||||
|
||||
## Install the cac_path_parity extension into lsphp's own extension_dir and load
|
||||
## it unconditionally. It is INERT until the entrypoint writes the per-site
|
||||
## cac_path_parity.from/.to mapping, so it is safe in any context (including
|
||||
## wp-cli runs, where $_SERVER carries no filesystem paths).
|
||||
##
|
||||
## The trailing `lsphp -m | grep` is a BUILD-TIME ASSERTION: if the .so fails to
|
||||
## load (ABI drift after a base-image PHP bump, bad build) the image build fails
|
||||
## here rather than shipping a sidecar that silently lost path parity.
|
||||
## NOTE: probe lsphp with `-i` ONLY. The lsphp binary is the LSAPI SAPI, not the
|
||||
## CLI — it accepts just -[b|c|n|h|i|q|s|v|?] and answers anything else (`-m`,
|
||||
## `-r`) by printing its usage text and exiting 0. A `lsphp -m | grep` check
|
||||
## therefore never matches AND never fails, which is exactly the kind of silent
|
||||
## always-false assertion this whole change exists to eliminate.
|
||||
COPY --from=ext-build /build-out/cac_path_parity.so /tmp/cac_path_parity.so
|
||||
RUN bash -c 'set -e; \
|
||||
LSPHP="/usr/local/lsws/lsphp${PHPVER}/bin/lsphp"; \
|
||||
EXT_DIR=$("$LSPHP" -i 2>/dev/null | awk -F" => " "/^extension_dir/ {print \$2; exit}"); \
|
||||
SCAN_DIR=$("$LSPHP" -i 2>/dev/null | awk -F"=> " "/^Scan this dir/ {print \$2; exit}"); \
|
||||
mkdir -p "$EXT_DIR" "$SCAN_DIR"; \
|
||||
mv /tmp/cac_path_parity.so "$EXT_DIR/"; \
|
||||
printf "; installed by Dockerfile.lsphp\nextension=cac_path_parity.so\n" \
|
||||
> "$SCAN_DIR/00-cac-path-parity.ini"; \
|
||||
"$LSPHP" -i 2>/dev/null | grep -q "^cac_path_parity support => enabled$"; \
|
||||
echo "cac_path_parity installed into $EXT_DIR and verified loadable"'
|
||||
|
||||
## php-lsapi gates .user.ini parsing behind this env var (see entrypoint-lsphp.sh
|
||||
## for the full explanation). Set here so the value is visible in `docker inspect`
|
||||
## and survives an entrypoint override; the entrypoint re-exports it with the same
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# phpize / configure / make artifacts from building this extension locally.
|
||||
# The shipped build happens inside Dockerfile.lsphp's ext-build stage, so
|
||||
# nothing generated here is ever committed.
|
||||
.deps
|
||||
.libs/
|
||||
Makefile
|
||||
Makefile.fragments
|
||||
Makefile.global
|
||||
Makefile.objects
|
||||
acinclude.m4
|
||||
aclocal.m4
|
||||
autom4te.cache/
|
||||
build/
|
||||
config.guess
|
||||
config.h
|
||||
config.h.in
|
||||
config.log
|
||||
config.nice
|
||||
config.status
|
||||
config.sub
|
||||
configure
|
||||
configure.ac
|
||||
include/
|
||||
install-sh
|
||||
libtool
|
||||
ltmain.sh
|
||||
missing
|
||||
mkinstalldirs
|
||||
modules/
|
||||
run-tests.php
|
||||
*.lo
|
||||
*.la
|
||||
*.o
|
||||
*.so
|
||||
tests/*.php
|
||||
tests/*.diff
|
||||
tests/*.exp
|
||||
tests/*.log
|
||||
tests/*.out
|
||||
tests/*.sh
|
||||
!tests/fpm-parity-check.sh
|
||||
@@ -0,0 +1,294 @@
|
||||
/*
|
||||
* cac_path_parity — $_SERVER filesystem-path parity for the shared-ols tier.
|
||||
*
|
||||
* WHAT PROBLEM THIS SOLVES
|
||||
* ------------------------
|
||||
* A site on the standalone tiers (cac / cac-fpm / cac-litespeed) sees:
|
||||
*
|
||||
* $_SERVER['DOCUMENT_ROOT'] = /home/<user>/public_html
|
||||
* $_SERVER['SCRIPT_FILENAME'] = /home/<user>/public_html/index.php
|
||||
*
|
||||
* On the shared-ols tier the webserver is a SHARED OpenLiteSpeed container that
|
||||
* serves every tenant out of one bulk `/docker/users -> /mnt/users:ro` mount, so
|
||||
* its vhost docRoot is /mnt/users/<user>/<domain>/public_html. OLS has no
|
||||
* ProxyFCGISetEnvIf-style remap (unlike shared-httpd -> cac-fpm): it hands lsphp
|
||||
* exactly that path. The cac-lsphp sidecar symlinks /mnt/users/<user>/<domain>
|
||||
* -> /home/<user>, so every file OPERATION resolves and PHP's own __FILE__ /
|
||||
* __DIR__ / realpath() / getcwd() already report /home/<user>/... — but the RAW
|
||||
* strings OLS put in $_SERVER still read /mnt/users. Moving a site from cac-fpm
|
||||
* to cac-lsphp therefore changed two $_SERVER values, which is exactly the kind
|
||||
* of difference that surfaces later as a broken plugin path or a mismatched
|
||||
* absolute path stored in the database.
|
||||
*
|
||||
* WHY THIS IS AN EXTENSION AND NOT AN auto_prepend_file
|
||||
* -----------------------------------------------------
|
||||
* The first fix was an `auto_prepend_file` drop-in that realpath()'d the two
|
||||
* keys. `auto_prepend_file` is PHP_INI_PERDIR, so ANY site with its own
|
||||
* .user.ini auto_prepend_file silently displaces it and the normaliser never
|
||||
* runs — the state 7 live shared_ols sites are actually in today (Wordfence and
|
||||
* cPanel imports). PHP resolves a single winning value for auto_prepend_file
|
||||
* after the .user.ini chain is parsed, so there is no way to "chain" from the
|
||||
* losing side either.
|
||||
*
|
||||
* The obvious hardening — `php_admin_value auto_prepend_file` — is WORSE, not
|
||||
* better: making our prepend un-overridable makes the customer's prepend
|
||||
* un-runnable, which would disable those same 7 Wordfence WAFs. The two goals
|
||||
* are irreconcilable as long as the mechanism IS the prepend hook.
|
||||
*
|
||||
* An extension sidesteps that entirely. RINIT runs before any userland code and
|
||||
* cannot be displaced by .user.ini, and it consumes no userland hook — so the
|
||||
* customer's auto_prepend_file remains the only prepend in play and keeps
|
||||
* working untouched. Both constraints are satisfied at once.
|
||||
*
|
||||
* The mapping comes from two PHP_INI_SYSTEM entries. PHP_INI_SYSTEM is NOT
|
||||
* settable from .user.ini (which honours only PHP_INI_PERDIR/PHP_INI_USER), nor
|
||||
* from ini_set(), nor from .htaccess — so a customer cannot point the rewrite
|
||||
* somewhere else or switch it off. The cac-lsphp entrypoint writes them from the
|
||||
* same `user`/`domain` env the symlink is built from, so the two can't drift.
|
||||
*
|
||||
* WHY A STRING PREFIX SWAP AND NOT realpath()
|
||||
* -------------------------------------------
|
||||
* The old normaliser called realpath(), which worked only because the sidecar
|
||||
* symlinks /mnt/users/<user>/<domain> -> /home/<user>. A plain prefix swap is
|
||||
* better on every axis that matters here:
|
||||
*
|
||||
* - It is byte-identical to cac-fpm BY CONSTRUCTION. realpath() resolves ALL
|
||||
* symlinks, so a customer who makes public_html itself a symlink would get
|
||||
* some third path — cac-fpm reports the literal /home/<user>/public_html.
|
||||
* - It cannot fail. realpath() returns false for a path that does not exist
|
||||
* (and is constrained by open_basedir), leaving the value half-normalised.
|
||||
* - It costs no syscall. realpath() is an lstat chain on every request.
|
||||
*
|
||||
* The prefix is this site's FULL mount path (/mnt/users/<user>/<domain>), not
|
||||
* the bare bulk-mount root, and it only matches on a path-component boundary —
|
||||
* so a value pointing at another tenant, or one that is already canonical, is
|
||||
* never touched.
|
||||
*
|
||||
* FAILURE MODES — every one of them leaves $_SERVER untouched and lets the
|
||||
* request proceed. Nothing here can warn, throw, or 500 a customer site:
|
||||
* - mapping unset/empty (any tier that is not shared-ols) -> RINIT returns
|
||||
* immediately, extension is inert.
|
||||
* - $_SERVER absent or not an array -> return.
|
||||
* - key absent from $_SERVER -> skip that key.
|
||||
* - key present but not a string -> skip that key.
|
||||
* - value shorter than the prefix / prefix mismatch -> skip that key.
|
||||
* - value matches the prefix mid-component -> skip that key.
|
||||
* There is no error path, no userland-visible diagnostic, and no dependency on
|
||||
* the filesystem being readable.
|
||||
*
|
||||
* SCOPE / KNOWN LIMITS
|
||||
* --------------------
|
||||
* Only $_SERVER is rewritten. LSAPI also answers getenv('DOCUMENT_ROOT') from
|
||||
* the request environment, and that still returns the /mnt/users string. That is
|
||||
* a deliberate limit: touching the SAPI environment risks the request env lsphp
|
||||
* itself reads. Real-world PHP (WordPress and its plugin ecosystem) reads
|
||||
* $_SERVER, not getenv(), for these.
|
||||
*
|
||||
* The sidecar's compatibility symlink is still REQUIRED and is not replaced by
|
||||
* this extension: it is what makes the path OLS sends actually resolve on disk.
|
||||
* This extension only corrects the strings.
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "php.h"
|
||||
#include "php_ini.h"
|
||||
#include "ext/standard/info.h"
|
||||
#include "SAPI.h"
|
||||
#include "zend_compile.h" /* zend_is_auto_global_str() */
|
||||
#include "php_cac_path_parity.h"
|
||||
|
||||
ZEND_DECLARE_MODULE_GLOBALS(cac_path_parity)
|
||||
|
||||
#define CACPP_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(cac_path_parity, v)
|
||||
|
||||
/*
|
||||
* The $_SERVER keys that carry a FILESYSTEM path. URI-derived keys (PHP_SELF,
|
||||
* SCRIPT_NAME, REQUEST_URI) are already identical across tiers and are left
|
||||
* alone. PATH_TRANSLATED and CONTEXT_DOCUMENT_ROOT are usually absent under
|
||||
* OLS; rewriting them is a no-op when they are, and correct when they are not.
|
||||
*/
|
||||
static const struct {
|
||||
const char *name;
|
||||
size_t len;
|
||||
} cacpp_keys[] = {
|
||||
{ ZEND_STRL("DOCUMENT_ROOT") },
|
||||
{ ZEND_STRL("SCRIPT_FILENAME") },
|
||||
{ ZEND_STRL("PATH_TRANSLATED") },
|
||||
{ ZEND_STRL("CONTEXT_DOCUMENT_ROOT") },
|
||||
};
|
||||
|
||||
/* clang-format off */
|
||||
PHP_INI_BEGIN()
|
||||
/*
|
||||
* PHP_INI_SYSTEM is load-bearing: it is the reason a customer's .user.ini
|
||||
* cannot reach these. Do not relax to PERDIR.
|
||||
*/
|
||||
STD_PHP_INI_ENTRY("cac_path_parity.from", "", PHP_INI_SYSTEM, OnUpdateString,
|
||||
from, zend_cac_path_parity_globals, cac_path_parity_globals)
|
||||
STD_PHP_INI_ENTRY("cac_path_parity.to", "", PHP_INI_SYSTEM, OnUpdateString,
|
||||
to, zend_cac_path_parity_globals, cac_path_parity_globals)
|
||||
PHP_INI_END()
|
||||
/* clang-format on */
|
||||
|
||||
static PHP_GINIT_FUNCTION(cac_path_parity)
|
||||
{
|
||||
#if defined(COMPILE_DL_CAC_PATH_PARITY) && defined(ZTS)
|
||||
ZEND_TSRMLS_CACHE_UPDATE();
|
||||
#endif
|
||||
cac_path_parity_globals->from = NULL;
|
||||
cac_path_parity_globals->to = NULL;
|
||||
}
|
||||
|
||||
/* Trailing slashes would defeat the component-boundary test below. */
|
||||
static size_t cacpp_trim(const char *s, size_t len)
|
||||
{
|
||||
while (len > 1 && s[len - 1] == '/') {
|
||||
len--;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
static void cacpp_rewrite_key(zval *server, const char *key, size_t key_len,
|
||||
const char *from, size_t from_len,
|
||||
const char *to, size_t to_len)
|
||||
{
|
||||
zval *val = zend_hash_str_find(Z_ARRVAL_P(server), key, key_len);
|
||||
if (val == NULL) {
|
||||
return;
|
||||
}
|
||||
ZVAL_DEREF(val);
|
||||
if (Z_TYPE_P(val) != IS_STRING) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *s = Z_STRVAL_P(val);
|
||||
size_t len = Z_STRLEN_P(val);
|
||||
|
||||
if (len < from_len || memcmp(s, from, from_len) != 0) {
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* Only replace on a PATH-COMPONENT boundary. Without this,
|
||||
* from=/mnt/users/bob/site.com would also match a sibling directory
|
||||
* /mnt/users/bob/site.com.bak and silently rewrite another site's path
|
||||
* into this site's /home.
|
||||
*/
|
||||
if (len != from_len && s[from_len] != '/') {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t tail_len = len - from_len;
|
||||
zend_string *out = zend_string_alloc(to_len + tail_len, 0);
|
||||
|
||||
memcpy(ZSTR_VAL(out), to, to_len);
|
||||
memcpy(ZSTR_VAL(out) + to_len, s + from_len, tail_len);
|
||||
ZSTR_VAL(out)[to_len + tail_len] = '\0';
|
||||
|
||||
zval nv;
|
||||
ZVAL_STR(&nv, out);
|
||||
/*
|
||||
* Update the track_vars array IN PLACE. $_SERVER in the symbol table and
|
||||
* PG(http_globals)[TRACK_VARS_SERVER] are two references to the SAME
|
||||
* zend_array, which is why this is visible to userland. Do NOT
|
||||
* SEPARATE_ARRAY() here: that would copy the array and leave the symbol
|
||||
* table pointing at the original, i.e. silently do nothing. This is the
|
||||
* same in-place pattern php_register_variable_ex() uses.
|
||||
*/
|
||||
zend_hash_str_update(Z_ARRVAL_P(server), key, key_len, &nv);
|
||||
}
|
||||
|
||||
PHP_RINIT_FUNCTION(cac_path_parity)
|
||||
{
|
||||
#if defined(ZTS) && defined(COMPILE_DL_CAC_PATH_PARITY)
|
||||
ZEND_TSRMLS_CACHE_UPDATE();
|
||||
#endif
|
||||
|
||||
const char *from = CACPP_G(from);
|
||||
const char *to = CACPP_G(to);
|
||||
|
||||
/* Unconfigured (any tier that isn't shared-ols) => completely inert. */
|
||||
if (from == NULL || *from == '\0' || to == NULL || *to == '\0') {
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
/*
|
||||
* With auto_globals_jit=On (the default) $_SERVER is not built yet at
|
||||
* RINIT — php_hash_environment() only MARKED it for lazy creation. Reading
|
||||
* PG(http_globals)[TRACK_VARS_SERVER] here without this call finds IS_UNDEF
|
||||
* and the extension silently does nothing, which is precisely the failure
|
||||
* the auto_prepend approach had. Forcing the auto-global now builds it (via
|
||||
* the SAPI's register_server_variables) so there is something to rewrite,
|
||||
* and the later userland access gets the corrected array.
|
||||
*/
|
||||
zend_is_auto_global_str(ZEND_STRL("_SERVER"));
|
||||
|
||||
zval *server = &PG(http_globals)[TRACK_VARS_SERVER];
|
||||
if (Z_TYPE_P(server) != IS_ARRAY) {
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
size_t from_len = cacpp_trim(from, strlen(from));
|
||||
size_t to_len = cacpp_trim(to, strlen(to));
|
||||
|
||||
for (size_t i = 0; i < sizeof(cacpp_keys) / sizeof(cacpp_keys[0]); i++) {
|
||||
cacpp_rewrite_key(server, cacpp_keys[i].name, cacpp_keys[i].len,
|
||||
from, from_len, to, to_len);
|
||||
}
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
PHP_MINIT_FUNCTION(cac_path_parity)
|
||||
{
|
||||
REGISTER_INI_ENTRIES();
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
PHP_MSHUTDOWN_FUNCTION(cac_path_parity)
|
||||
{
|
||||
UNREGISTER_INI_ENTRIES();
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
PHP_MINFO_FUNCTION(cac_path_parity)
|
||||
{
|
||||
const char *from = CACPP_G(from);
|
||||
const char *to = CACPP_G(to);
|
||||
int active = (from && *from && to && *to);
|
||||
|
||||
php_info_print_table_start();
|
||||
php_info_print_table_header(2, "cac_path_parity support", "enabled");
|
||||
php_info_print_table_row(2, "Version", PHP_CAC_PATH_PARITY_VERSION);
|
||||
/* The canary greps for this row: "active" proves the mapping is live. */
|
||||
php_info_print_table_row(2, "Rewriting", active ? "active" : "inactive (unconfigured)");
|
||||
php_info_print_table_end();
|
||||
|
||||
DISPLAY_INI_ENTRIES();
|
||||
}
|
||||
|
||||
zend_module_entry cac_path_parity_module_entry = {
|
||||
STANDARD_MODULE_HEADER,
|
||||
"cac_path_parity",
|
||||
NULL, /* no userland functions — by design */
|
||||
PHP_MINIT(cac_path_parity),
|
||||
PHP_MSHUTDOWN(cac_path_parity),
|
||||
PHP_RINIT(cac_path_parity),
|
||||
NULL, /* RSHUTDOWN */
|
||||
PHP_MINFO(cac_path_parity),
|
||||
PHP_CAC_PATH_PARITY_VERSION,
|
||||
PHP_MODULE_GLOBALS(cac_path_parity),
|
||||
PHP_GINIT(cac_path_parity),
|
||||
NULL, /* GSHUTDOWN */
|
||||
NULL, /* post-deactivate */
|
||||
STANDARD_MODULE_PROPERTIES_EX
|
||||
};
|
||||
|
||||
#ifdef COMPILE_DL_CAC_PATH_PARITY
|
||||
#if defined(ZTS)
|
||||
ZEND_TSRMLS_CACHE_DEFINE()
|
||||
#endif
|
||||
ZEND_GET_MODULE(cac_path_parity)
|
||||
#endif
|
||||
@@ -0,0 +1,15 @@
|
||||
dnl config.m4 for the cac_path_parity extension.
|
||||
dnl Built out-of-tree against the image's own lsphp:
|
||||
dnl phpize && ./configure --with-php-config=/usr/local/lsws/lsphpNN/bin/php-config
|
||||
dnl No external libraries, no optional features — pure core API.
|
||||
|
||||
PHP_ARG_ENABLE([cac_path_parity],
|
||||
[whether to enable cac_path_parity support],
|
||||
[AS_HELP_STRING([--enable-cac-path-parity],
|
||||
[Enable cac_path_parity ($_SERVER path parity for the shared-ols tier)])],
|
||||
[no])
|
||||
|
||||
if test "$PHP_CAC_PATH_PARITY" != "no"; then
|
||||
AC_DEFINE(HAVE_CAC_PATH_PARITY, 1, [Have cac_path_parity support])
|
||||
PHP_NEW_EXTENSION(cac_path_parity, cac_path_parity.c, $ext_shared)
|
||||
fi
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* cac_path_parity — $_SERVER filesystem-path parity for the shared-ols tier.
|
||||
*
|
||||
* See cac_path_parity.c for the full rationale.
|
||||
*/
|
||||
|
||||
#ifndef PHP_CAC_PATH_PARITY_H
|
||||
#define PHP_CAC_PATH_PARITY_H
|
||||
|
||||
extern zend_module_entry cac_path_parity_module_entry;
|
||||
#define phpext_cac_path_parity_ptr &cac_path_parity_module_entry
|
||||
|
||||
#define PHP_CAC_PATH_PARITY_VERSION "1.0.0"
|
||||
|
||||
#if defined(ZTS) && defined(COMPILE_DL_CAC_PATH_PARITY)
|
||||
ZEND_TSRMLS_CACHE_EXTERN()
|
||||
#endif
|
||||
|
||||
ZEND_BEGIN_MODULE_GLOBALS(cac_path_parity)
|
||||
char *from;
|
||||
char *to;
|
||||
ZEND_END_MODULE_GLOBALS(cac_path_parity)
|
||||
|
||||
#endif /* PHP_CAC_PATH_PARITY_H */
|
||||
@@ -0,0 +1,25 @@
|
||||
--TEST--
|
||||
cac_path_parity: rewrites the configured prefix on a filesystem $_SERVER key
|
||||
--EXTENSIONS--
|
||||
cac_path_parity
|
||||
--INI--
|
||||
cac_path_parity.from=/mnt/users/bob/site.com
|
||||
cac_path_parity.to=/home/bob
|
||||
variables_order=EGPCS
|
||||
--ENV--
|
||||
CONTEXT_DOCUMENT_ROOT=/mnt/users/bob/site.com/public_html
|
||||
HTTP_HOST=site.com
|
||||
--FILE--
|
||||
<?php
|
||||
// NOTE: the CLI SAPI overwrites DOCUMENT_ROOT (to "") and SCRIPT_FILENAME /
|
||||
// PATH_TRANSLATED (to the script path) AFTER the env import, so those three
|
||||
// cannot be driven from --ENV-- here. They go through the identical code path
|
||||
// as CONTEXT_DOCUMENT_ROOT (one loop over one key table); the real web-SAPI
|
||||
// proof for them is tests/web-sapi-parity-check.sh.
|
||||
var_dump($_SERVER['CONTEXT_DOCUMENT_ROOT']);
|
||||
// Non-path vars must be untouched.
|
||||
var_dump($_SERVER['HTTP_HOST']);
|
||||
?>
|
||||
--EXPECT--
|
||||
string(21) "/home/bob/public_html"
|
||||
string(8) "site.com"
|
||||
@@ -0,0 +1,18 @@
|
||||
--TEST--
|
||||
cac_path_parity: a sibling dir sharing the prefix is NOT rewritten
|
||||
--EXTENSIONS--
|
||||
cac_path_parity
|
||||
--INI--
|
||||
cac_path_parity.from=/mnt/users/bob/site.com
|
||||
cac_path_parity.to=/home/bob
|
||||
variables_order=EGPCS
|
||||
--ENV--
|
||||
CONTEXT_DOCUMENT_ROOT=/mnt/users/bob/site.com.bak/public_html
|
||||
--FILE--
|
||||
<?php
|
||||
// Replacement happens only on a path-COMPONENT boundary. Without that guard a
|
||||
// neighbouring directory would be folded into this container's /home.
|
||||
var_dump($_SERVER['CONTEXT_DOCUMENT_ROOT']);
|
||||
?>
|
||||
--EXPECT--
|
||||
string(39) "/mnt/users/bob/site.com.bak/public_html"
|
||||
@@ -0,0 +1,17 @@
|
||||
--TEST--
|
||||
cac_path_parity: an exact prefix match (no trailing component) is rewritten
|
||||
--EXTENSIONS--
|
||||
cac_path_parity
|
||||
--INI--
|
||||
cac_path_parity.from=/mnt/users/bob/site.com/
|
||||
cac_path_parity.to=/home/bob/
|
||||
variables_order=EGPCS
|
||||
--ENV--
|
||||
CONTEXT_DOCUMENT_ROOT=/mnt/users/bob/site.com
|
||||
--FILE--
|
||||
<?php
|
||||
// Also covers trailing slashes in the configured values being tolerated.
|
||||
var_dump($_SERVER['CONTEXT_DOCUMENT_ROOT']);
|
||||
?>
|
||||
--EXPECT--
|
||||
string(9) "/home/bob"
|
||||
@@ -0,0 +1,16 @@
|
||||
--TEST--
|
||||
cac_path_parity: completely inert when no mapping is configured
|
||||
--EXTENSIONS--
|
||||
cac_path_parity
|
||||
--INI--
|
||||
variables_order=EGPCS
|
||||
--ENV--
|
||||
CONTEXT_DOCUMENT_ROOT=/mnt/users/bob/site.com/public_html
|
||||
--FILE--
|
||||
<?php
|
||||
// cac-fpm / cac-litespeed never configure a mapping, so the extension must be
|
||||
// a no-op there. This is the no-regression guarantee for the other tiers.
|
||||
var_dump($_SERVER['CONTEXT_DOCUMENT_ROOT']);
|
||||
?>
|
||||
--EXPECT--
|
||||
string(35) "/mnt/users/bob/site.com/public_html"
|
||||
@@ -0,0 +1,20 @@
|
||||
--TEST--
|
||||
cac_path_parity: works with auto_globals_jit=On (lazy $_SERVER, the default)
|
||||
--EXTENSIONS--
|
||||
cac_path_parity
|
||||
--INI--
|
||||
auto_globals_jit=1
|
||||
cac_path_parity.from=/mnt/users/bob/site.com
|
||||
cac_path_parity.to=/home/bob
|
||||
variables_order=EGPCS
|
||||
--ENV--
|
||||
CONTEXT_DOCUMENT_ROOT=/mnt/users/bob/site.com/public_html
|
||||
--FILE--
|
||||
<?php
|
||||
// With auto_globals_jit=On, $_SERVER does not exist yet when extension RINIT
|
||||
// runs. The extension forces the auto-global so there is something to rewrite;
|
||||
// drop that call and this test prints the /mnt/users path.
|
||||
var_dump($_SERVER['CONTEXT_DOCUMENT_ROOT']);
|
||||
?>
|
||||
--EXPECT--
|
||||
string(21) "/home/bob/public_html"
|
||||
@@ -0,0 +1,25 @@
|
||||
--TEST--
|
||||
cac_path_parity: mapping is PHP_INI_SYSTEM — userland cannot change it
|
||||
--EXTENSIONS--
|
||||
cac_path_parity
|
||||
--INI--
|
||||
cac_path_parity.from=/mnt/users/bob/site.com
|
||||
cac_path_parity.to=/home/bob
|
||||
variables_order=EGPCS
|
||||
--ENV--
|
||||
CONTEXT_DOCUMENT_ROOT=/mnt/users/bob/site.com/public_html
|
||||
--FILE--
|
||||
<?php
|
||||
// PHP_INI_SYSTEM entries are not modifiable at runtime, and .user.ini honours
|
||||
// only PHP_INI_PERDIR|PHP_INI_USER — so a customer cannot redirect or disable
|
||||
// the rewrite the way they can displace an auto_prepend_file.
|
||||
var_dump(ini_set('cac_path_parity.from', '/tmp'));
|
||||
var_dump(ini_set('cac_path_parity.to', '/tmp'));
|
||||
var_dump(ini_get('cac_path_parity.from'));
|
||||
var_dump($_SERVER['CONTEXT_DOCUMENT_ROOT']);
|
||||
?>
|
||||
--EXPECT--
|
||||
bool(false)
|
||||
bool(false)
|
||||
string(23) "/mnt/users/bob/site.com"
|
||||
string(21) "/home/bob/public_html"
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env bash
|
||||
## fpm-parity-check.sh — end-to-end proof under a REAL web SAPI.
|
||||
##
|
||||
## WHY NOT .phpt: the CLI SAPI overwrites DOCUMENT_ROOT / SCRIPT_FILENAME /
|
||||
## PATH_TRANSLATED after importing the environment, and the cli-server SAPI does
|
||||
## not process .user.ini at all — so neither can exercise the two things that
|
||||
## actually matter here.
|
||||
##
|
||||
## WHY PHP-FPM: php-fpm takes DOCUMENT_ROOT and SCRIPT_FILENAME as caller-
|
||||
## supplied FastCGI params and honours .user.ini — structurally the same shape as
|
||||
## OpenLiteSpeed handing a detached lsphp its LSAPI params. It is the closest
|
||||
## analogue available without an OLS runtime.
|
||||
##
|
||||
## Asserts:
|
||||
## 1. CONTROL — no mapping => PHP reports the raw /mnt/users paths, i.e. the
|
||||
## test reproduces the bug before claiming to fix it.
|
||||
## 2. FIX — mapping => both keys read /home/<user>/... .
|
||||
## 3. WORDFENCE — mapping AND a customer .user.ini auto_prepend_file (the state
|
||||
## 7 live shared_ols sites are in): paths are STILL corrected
|
||||
## AND the customer's prepend STILL runs. This is the case the
|
||||
## old auto_prepend_file normaliser silently lost.
|
||||
## 4. OLD — for the record: the previous auto_prepend mechanism, with the
|
||||
## same customer .user.ini, does NOT run. This is the evidence
|
||||
## that hardening the prepend hook could not have worked.
|
||||
##
|
||||
## Usage: ./fpm-parity-check.sh [ROOT] [PHP_FPM_BIN] [EXT_SO]
|
||||
## ROOT defaults to /mnt/users (falls back to a temp dir if not creatable).
|
||||
set -uo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="${1:-/mnt/users}"
|
||||
FPM_BIN="${2:-$(command -v php-fpm8.3 || echo /usr/sbin/php-fpm8.3)}"
|
||||
EXT_SO="${3:-$HERE/../modules/cac_path_parity.so}"
|
||||
PORT="${PORT:-9001}"
|
||||
|
||||
command -v cgi-fcgi >/dev/null || { echo "SKIP: cgi-fcgi not installed (apt install libfcgi-bin)"; exit 0; }
|
||||
[ -x "$FPM_BIN" ] || { echo "SKIP: php-fpm not found"; exit 0; }
|
||||
[ -f "$EXT_SO" ] || { echo "SKIP: $EXT_SO not built (run phpize && ./configure && make)"; exit 0; }
|
||||
|
||||
mkdir -p "$ROOT" 2>/dev/null || ROOT="$(mktemp -d)/mnt/users"
|
||||
USER_NAME=bob
|
||||
SITE="$ROOT/$USER_NAME/site.com"
|
||||
DOCROOT="$SITE/public_html"
|
||||
HOME_PATH="/home/$USER_NAME"
|
||||
TMP="$(mktemp -d)"
|
||||
fail=0
|
||||
|
||||
mkdir -p "$DOCROOT" || { echo "cannot create $DOCROOT"; exit 1; }
|
||||
trap 'rm -rf "$TMP"; rm -f "$DOCROOT/.user.ini"' EXIT
|
||||
|
||||
cat > "$DOCROOT/probe.php" <<'PHP'
|
||||
<?php
|
||||
echo "DOCUMENT_ROOT=" . $_SERVER['DOCUMENT_ROOT'] . "\n";
|
||||
echo "SCRIPT_FILENAME=" . $_SERVER['SCRIPT_FILENAME'] . "\n";
|
||||
echo "PREPEND_RAN=" . (defined('CUSTOMER_PREPEND_RAN') ? 'yes' : 'no') . "\n";
|
||||
PHP
|
||||
|
||||
## Stand-in for the customer's wordfence-waf.php.
|
||||
cat > "$SITE/customer-waf.php" <<'PHP'
|
||||
<?php
|
||||
define('CUSTOMER_PREPEND_RAN', 1);
|
||||
PHP
|
||||
|
||||
## Stand-in for the OLD mechanism (scripts/cac-lsphp-normalize.php).
|
||||
cat > "$TMP/old-normalize.php" <<'PHP'
|
||||
<?php
|
||||
foreach (array('DOCUMENT_ROOT', 'SCRIPT_FILENAME') as $k) {
|
||||
if (!empty($_SERVER[$k]) && strncmp($_SERVER[$k], '/mnt/users/', 11) === 0) {
|
||||
$r = realpath($_SERVER[$k]);
|
||||
if ($r !== false) { $_SERVER[$k] = $r; }
|
||||
}
|
||||
}
|
||||
PHP
|
||||
|
||||
cat > "$TMP/fpm.conf" <<EOF
|
||||
[global]
|
||||
error_log = $TMP/fpm-error.log
|
||||
daemonize = no
|
||||
[www]
|
||||
listen = 127.0.0.1:$PORT
|
||||
pm = static
|
||||
pm.max_children = 2
|
||||
EOF
|
||||
|
||||
run_case() {
|
||||
"$FPM_BIN" -n -y "$TMP/fpm.conf" -F -d user_ini.cache_ttl=0 "$@" \
|
||||
>"$TMP/fpm.out" 2>&1 &
|
||||
local pid=$! out=""
|
||||
for _ in $(seq 1 40); do
|
||||
sleep 0.15
|
||||
out=$(SCRIPT_FILENAME="$DOCROOT/probe.php" DOCUMENT_ROOT="$DOCROOT" \
|
||||
SCRIPT_NAME=/probe.php REQUEST_METHOD=GET QUERY_STRING= \
|
||||
cgi-fcgi -bind -connect "127.0.0.1:$PORT" 2>/dev/null)
|
||||
[ -n "$out" ] && break
|
||||
done
|
||||
kill "$pid" 2>/dev/null; wait "$pid" 2>/dev/null
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
expect() {
|
||||
local label="$1" got="$2" want="$3"
|
||||
if [ "$got" = "$want" ]; then
|
||||
echo " PASS $label"
|
||||
else
|
||||
echo " FAIL $label"
|
||||
echo " want: $want"
|
||||
echo " got: $got"
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
field() { printf '%s' "$1" | sed -n "s/^$2=//p"; }
|
||||
|
||||
EXT=( -d "extension=$EXT_SO" )
|
||||
MAP=( -d "cac_path_parity.from=$SITE" -d "cac_path_parity.to=$HOME_PATH" )
|
||||
USERINI_LINE="auto_prepend_file = $SITE/customer-waf.php"
|
||||
|
||||
echo "== 1. CONTROL: extension loaded, no mapping (reproduces the bug) =="
|
||||
rm -f "$DOCROOT/.user.ini"
|
||||
out=$(run_case "${EXT[@]}")
|
||||
expect "DOCUMENT_ROOT is the raw OLS path" "$(field "$out" DOCUMENT_ROOT)" "$DOCROOT"
|
||||
expect "SCRIPT_FILENAME is the raw OLS path" "$(field "$out" SCRIPT_FILENAME)" "$DOCROOT/probe.php"
|
||||
|
||||
echo "== 2. FIX: mapping configured =="
|
||||
out=$(run_case "${EXT[@]}" "${MAP[@]}")
|
||||
expect "DOCUMENT_ROOT == cac-fpm value" "$(field "$out" DOCUMENT_ROOT)" "$HOME_PATH/public_html"
|
||||
expect "SCRIPT_FILENAME == cac-fpm value" "$(field "$out" SCRIPT_FILENAME)" "$HOME_PATH/public_html/probe.php"
|
||||
|
||||
echo "== 3. WORDFENCE: customer .user.ini auto_prepend_file present =="
|
||||
printf '%s\n' "$USERINI_LINE" > "$DOCROOT/.user.ini"
|
||||
out=$(run_case "${EXT[@]}" "${MAP[@]}")
|
||||
expect "DOCUMENT_ROOT still corrected" "$(field "$out" DOCUMENT_ROOT)" "$HOME_PATH/public_html"
|
||||
expect "SCRIPT_FILENAME still corrected" "$(field "$out" SCRIPT_FILENAME)" "$HOME_PATH/public_html/probe.php"
|
||||
expect "customer auto_prepend_file still ran" "$(field "$out" PREPEND_RAN)" "yes"
|
||||
|
||||
echo "== 4. OLD MECHANISM (why the prepend hook could not be hardened) =="
|
||||
out=$(run_case -d "auto_prepend_file=$TMP/old-normalize.php")
|
||||
expect "auto_prepend normaliser is displaced by the customer's .user.ini" \
|
||||
"$(field "$out" DOCUMENT_ROOT)" "$DOCROOT"
|
||||
expect "customer's prepend is the one that ran" "$(field "$out" PREPEND_RAN)" "yes"
|
||||
|
||||
rm -f "$DOCROOT/.user.ini"
|
||||
if [ "$fail" -eq 0 ]; then echo "ALL PASS"; else echo "FAILURES"; fi
|
||||
exit "$fail"
|
||||
@@ -1,6 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* cac-lsphp $_SERVER path normaliser (auto_prepend).
|
||||
* cac-lsphp $_SERVER path normaliser (auto_prepend) — DEGRADED FALLBACK ONLY.
|
||||
*
|
||||
* SUPERSEDED by the cac_path_parity PHP extension (ext/cac-path-parity/), which
|
||||
* does this from RINIT where a customer's .user.ini cannot displace it. The
|
||||
* entrypoint only wires this file up when that extension is not loadable in the
|
||||
* running image, and logs a WARNING when it does. Do not extend this script —
|
||||
* fix the extension instead.
|
||||
*
|
||||
* It is kept because the flaw documented at the bottom of this docblock is
|
||||
* exactly why the extension exists, and because an image where the extension
|
||||
* failed to load should degrade to the old behaviour rather than to nothing.
|
||||
*
|
||||
* The shared-ols container serves from its bulk /docker/users->/mnt/users mount,
|
||||
* so OLS sends lsphp $_SERVER['DOCUMENT_ROOT'] / ['SCRIPT_FILENAME'] under
|
||||
|
||||
@@ -22,6 +22,14 @@
|
||||
## /home/$user/public_html files. PHP canonicalises the symlink, so
|
||||
## __FILE__/__DIR__/realpath all report /home/$user/public_html (verified
|
||||
## 2026-06-10) — the customer never sees the /mnt/users path.
|
||||
##
|
||||
## THE $_SERVER STRINGS: the symlink makes paths RESOLVE, but the raw strings OLS
|
||||
## put in $_SERVER['DOCUMENT_ROOT']/['SCRIPT_FILENAME'] still read /mnt/users.
|
||||
## The cac_path_parity extension (baked into the image, configured per-site
|
||||
## below) rewrites those two at request start, so a site moved from cac-fpm to
|
||||
## cac-lsphp sees byte-identical values. It replaced an auto_prepend_file
|
||||
## normaliser that any site's own .user.ini silently displaced — see
|
||||
## ext/cac-path-parity/cac_path_parity.c.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -60,8 +68,13 @@ SAFE_DOMAIN="$domain"
|
||||
case "$domain" in
|
||||
\*.*) SAFE_DOMAIN="wildcard.${domain#\*.}" ;;
|
||||
esac
|
||||
## The exact path prefix the shared-ols container serves this site from — the
|
||||
## string OLS puts in SCRIPT_FILENAME/DOCUMENT_ROOT. Used twice: for the symlink
|
||||
## that makes it RESOLVE, and for the cac_path_parity mapping that makes it READ
|
||||
## like cac-fpm. Deriving both from one variable keeps them in lockstep.
|
||||
OLS_SITE_PATH="/mnt/users/$user/$SAFE_DOMAIN"
|
||||
mkdir -p "/mnt/users/$user"
|
||||
ln -sfn "/home/$user" "/mnt/users/$user/$SAFE_DOMAIN"
|
||||
ln -sfn "/home/$user" "$OLS_SITE_PATH"
|
||||
|
||||
## ---- detached-lsphp pool sizing ----
|
||||
# shellcheck source=/dev/null
|
||||
@@ -102,7 +115,13 @@ echo "Container memory: ${CONTAINER_MEMORY_MB}MB | PHP_LSAPI_CHILDREN=${PHP_LSAP
|
||||
## ---- per-site ini drop-ins (identical mechanism to entrypoint-litespeed.sh) ----
|
||||
## error_log → the same customer-visible path cac:phpNN / cac-litespeed use, so
|
||||
## "where's my PHP error log?" is answered identically across all site types.
|
||||
SCAN_DIR=$("$LSPHP_BIN" -i 2>/dev/null | awk -F'=> ' '/^Scan this dir/ {print $2; exit}')
|
||||
## Capture lsphp's own info once and read both answers out of it. Probe with
|
||||
## `-i` ONLY: lsphp is the LSAPI SAPI, not the CLI — it accepts just
|
||||
## -[b|c|n|h|i|q|s|v|?] and answers `-m`/`-r` by printing usage and exiting 0, so
|
||||
## a `lsphp -m | grep` test never matches and never errors either.
|
||||
PATH_PARITY_MODE="none"
|
||||
LSPHP_INFO=$("$LSPHP_BIN" -i 2>/dev/null || true)
|
||||
SCAN_DIR=$(printf '%s\n' "$LSPHP_INFO" | awk -F'=> ' '/^Scan this dir/ {print $2; exit}')
|
||||
if [ -n "$SCAN_DIR" ]; then
|
||||
mkdir -p "$SCAN_DIR"
|
||||
cat > "$SCAN_DIR/99-user-error-log.ini" <<EOF
|
||||
@@ -110,15 +129,42 @@ if [ -n "$SCAN_DIR" ]; then
|
||||
error_log = /home/${user}/logs/php-fpm/error.log
|
||||
log_errors = On
|
||||
EOF
|
||||
## Normalise \$_SERVER['DOCUMENT_ROOT']/['SCRIPT_FILENAME'] from the OLS-sent
|
||||
## /mnt/users path back to /home/<user> so cac-lsphp is byte-for-byte 1:1 with
|
||||
## cac-fpm. Customer sites have no auto_prepend by default, so this is safe; a
|
||||
## site that sets its own .user.ini auto_prepend overrides it (paths still
|
||||
## resolve via the symlink either way).
|
||||
cat > "$SCAN_DIR/99-cac-lsphp-normalize.ini" <<'EOF'
|
||||
## ---- $_SERVER path parity with cac-fpm ----
|
||||
## Point the cac_path_parity extension at THIS site's mapping. Same two
|
||||
## values the compatibility symlink above is built from, so the rewrite and
|
||||
## the symlink can never disagree.
|
||||
##
|
||||
## Both settings are PHP_INI_SYSTEM: a customer's .user.ini (PHP_INI_PERDIR /
|
||||
## PHP_INI_USER only) cannot redirect or disable them, and the extension
|
||||
## occupies no userland hook — so the customer's own auto_prepend_file (the
|
||||
## Wordfence WAF on several live sites) keeps working untouched. That
|
||||
## combination is why this is an extension: the previous auto_prepend_file
|
||||
## normaliser was itself PHP_INI_PERDIR and any site with its own prepend
|
||||
## silently displaced it, while making OUR prepend win would have disabled
|
||||
## THEIRS. See ext/cac-path-parity/cac_path_parity.c.
|
||||
if printf '%s\n' "$LSPHP_INFO" | grep -q '^cac_path_parity support => enabled$'; then
|
||||
cat > "$SCAN_DIR/99-cac-path-parity.ini" <<EOF
|
||||
; rendered at container start by entrypoint-lsphp.sh
|
||||
cac_path_parity.from = ${OLS_SITE_PATH}
|
||||
cac_path_parity.to = /home/${user}
|
||||
EOF
|
||||
## Drop the pre-extension fallback if an older image left one here — the
|
||||
## container filesystem survives a "docker restart", so an in-place upgrade
|
||||
## must not keep a stale auto_prepend pointing at the old normaliser.
|
||||
rm -f "$SCAN_DIR/99-cac-lsphp-normalize.ini"
|
||||
PATH_PARITY_MODE="extension"
|
||||
else
|
||||
## Degraded fallback for an image built before the extension existed (or one
|
||||
## where it failed to load). Restores the old, .user.ini-defeatable
|
||||
## behaviour rather than losing normalisation entirely — but say so loudly,
|
||||
## because in this mode parity is NOT guaranteed.
|
||||
cat > "$SCAN_DIR/99-cac-lsphp-normalize.ini" <<'EOF'
|
||||
; rendered at container start by entrypoint-lsphp.sh (DEGRADED FALLBACK)
|
||||
auto_prepend_file = /scripts/cac-lsphp-normalize.php
|
||||
EOF
|
||||
PATH_PARITY_MODE="auto_prepend (DEGRADED)"
|
||||
echo "WARNING: entrypoint-lsphp: cac_path_parity extension not loadable in this image — falling back to the auto_prepend normaliser, which a site's own .user.ini auto_prepend_file will silently displace. Rebuild/repull cac-lsphp:php${PHPVER}." >&2
|
||||
fi
|
||||
## Per-site opcache override (panel: Advanced Tuning → OpCache size); falls
|
||||
## back to the baked lsphp-overrides.ini defaults when unset.
|
||||
if [ -n "${OPCACHE_MEMORY_MB:-}" ] || [ -n "${OPCACHE_MAX_FILES:-}" ]; then
|
||||
@@ -129,8 +175,15 @@ EOF
|
||||
[ -n "${OPCACHE_MAX_FILES:-}" ] && echo "opcache.max_accelerated_files = ${OPCACHE_MAX_FILES}"
|
||||
} > "$SCAN_DIR/99-user-opcache.ini"
|
||||
fi
|
||||
else
|
||||
## No scan dir means none of the per-site ini drop-ins land — including the
|
||||
## path-parity mapping. Previously this failed silently; it must not, because
|
||||
## the tier's cac-fpm parity guarantee is one of the things lost.
|
||||
echo "WARNING: entrypoint-lsphp: lsphp reports no additional-ini scan dir — per-site error_log, opcache and \$_SERVER path-parity settings were NOT applied." >&2
|
||||
fi
|
||||
|
||||
echo "entrypoint-lsphp: \$_SERVER path parity = ${PATH_PARITY_MODE} (${OLS_SITE_PATH} -> /home/${user})"
|
||||
|
||||
## ---- ownership ----
|
||||
## Ensure the dirs we created + the log file are customer-owned so lsphp (running
|
||||
## as $user) can read code and write logs. Customer content is already
|
||||
|
||||
Reference in New Issue
Block a user