Guard the vendor autoload include against symlink double-loading#384
Closed
epeicher wants to merge 4 commits into
Closed
Guard the vendor autoload include against symlink double-loading#384epeicher wants to merge 4 commits into
epeicher wants to merge 4 commits into
Conversation
When wp-content is reached through a symlink, the same physical autoload.php can be included through two different paths. With opcache.revalidate_path=0 (the default), require_once does not resolve symlinks for deduplication, so the second include re-declares the ComposerAutoloaderInit* class and fatals, taking the export endpoint down with HTTP 500. Same mechanism afc8318 fixed for utils.php and class-http-server.php in export.php; the vendor/autoload.php include in the plugin loader was the remaining unguarded one. Guard on build_pdo_dsn(): it is a Composer `files` autoload entry, so it exists exactly when the autoloader has already run.
Contributor
Pull pipeline performance —
|
| Stage | PR | trunk | Δ | Status | Details |
|---|---|---|---|---|---|
playground-sqlite-db-pull |
9.39 s | 9.43 s | ⚪ -40 ms (-0.4%) | ✓ | condition=db-pull in PHP.wasm runtime=php.wasm 8.3 wp_mysql_parser=enabled mode=lexer native_lexer=verified native_token_stream=WP_MySQL_Native_Token_Stream native_token_count=18 native_parser=selected trunk: condition=db-pull in PHP.wasm runtime=php.wasm 8.3 wp_mysql_parser=enabled mode=lexer native_lexer=verified native_token_stream=WP_MySQL_Native_Token_Stream native_token_count=18 native_parser=selected |
playground-sqlite-db-apply |
3.62 s | 3.65 s | ⚪ -28 ms (-0.8%) | ✓ | condition=db-apply to SQLite in PHP.wasm runtime=php.wasm 8.3 wp_mysql_parser=enabled mode=parser native_lexer=verified native_token_stream=WP_MySQL_Native_Token_Stream native_token_count=18 native_parser=verified native_ast=WP_MySQL_Native_Parser_Node sqlite_driver_parser=verified trunk: condition=db-apply to SQLite in PHP.wasm runtime=php.wasm 8.3 wp_mysql_parser=enabled mode=parser native_lexer=verified native_token_stream=WP_MySQL_Native_Token_Stream native_token_count=18 native_parser=verified native_ast=WP_MySQL_Native_Parser_Node sqlite_driver_parser=verified |
| Total | 13.01 s | 13.08 s | ⚪ -68 ms (-0.5%) |
Numbers carry runner noise; treat single-run deltas as directional, not authoritative.
📈 Trunk performance history — commit-by-commit timeline.
Gated to SITE_EXPORT_TEST_MODE + the wpcloud vhost. Logs include origin at each bootstrap point, autoloader-init classes already declared, the loader guard's decision, and — via a shutdown hook, which still runs after a fatal — the ordered include list at the moment of death. To be reverted once the second include route is identified.
The E2E trace showed the function_exists guard is unreliable: the loader re-enters mid-getLoader(), when the ComposerAutoloaderInit* class is already declared but the Composer `files` entries have not yet defined build_pdo_dsn. It also showed the colliding include arrives through the realpath family — Composer's baked __DIR__ paths resolve symlinks — while this loader required through the symlink family, and require_once cannot dedupe across the two strings. Requiring the realpath puts this include in the same family as every resolved include, so require_once dedupes natively whichever side runs first, with no reliance on load-order symbols.
The trace they produced identified the collision: the second autoload include arrives through the realpath family via Composer's baked __DIR__ paths, while the loader required through the symlink family. Fixed by requiring the realpath in the previous commit.
Collaborator
Author
|
Closing this PR: CI traces showed the fatal is not fixable at the plugin's include site. Both attempts (symbol guard, then requiring the autoloader by realpath) still reproduced the crash, because with |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What it does
Requires the plugin's Composer autoloader by its
realpath(), so the same physicalautoload.phpcan no longer be included twice through two different path strings.Rationale
When
wp-contentis a symlink (the WP Cloud layout), the plugin's files are reachable through two path families — the symlink path and the realpath. Withopcache.revalidate_path=0(the default),require_oncedoes not dedupe across the two strings, so a cross-family double include ofautoload.phpre-declares theComposerAutoloaderInit*class:The export endpoint answers HTTP 500 and, in CI,
import-38-flatten-wpcloudcascades (files-sync fails, nothing lands in the fs root, flat-docroot reports "ABSPATH not found"). Which leg fails varies with per-file opcache cache-key state — it hit PHP 8.1 twice, then 8.4 — the same class of symlink double-load afc8318 (#148) fixed insideexport.php.A temporarily instrumented CI run (since reverted) captured the fatal in flight and pinned the mechanism:
__DIR__paths resolve symlinks, so everything the ClassLoader itself pulls in — classmap classes, and eventually the colliding secondautoload.phpinclude — runs through the realpath family;function_existson a Composerfilesentry, the first attempt in this PR) is unsound: the trace showed the loader re-entered mid-getLoader(), when theComposerAutoloaderInit*class is already declared but thefilesentries have not run yet.Requiring the realpath puts this loader's include in the same family Composer already uses, so
require_oncededupes natively whichever side runs first — no reliance on load-order symbols, no dependence on who the second includer is.Testing instructions
The failure is opcache- and version-dependent, so the E2E matrix is the practical test:
import-38-flatten-wpcloudexercises exactly this layout. With the fix, the full matrix went green (run failed on 8.4 before the fix; the follow-up runs pass) — the flake had fired on every recent PR run, including twice on the same leg of #383.php -l reprint-exporter-wp/lib.phppasses; behaviour is unchanged when no symlinks are involved (realpathis then the same path).