From e5da88a966094c0d073d065ed0336bde63e6dc65 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Thu, 27 Aug 2026 14:03:54 +0200 Subject: [PATCH] Decline only the file that declares the function of that name wouldReIncludeALoadedFile() declined on any trapped read of an already-loaded file, but an aliasing autoloader reads the file of the class it aliases *to*: class_alias(Target::class, 'File') autoloads Target, and Target already being loaded is the normal case - class_alias() includes nothing. So the guard declined the very case it was written to keep working, and `use File;` still reported class.notFound with Laravel's AliasLoader. Only re-including the file that declares the function of that name can redeclare it, so compare the trapped read against that file. A built-in has no declaring file and nothing to redeclare. e2e/bug-15102b required its alias target upfront, which is why it passed: class_alias() then reads no file and the guard never fires. It now autoloads the target like AliasLoader does, and fails without this change. Reported and diagnosed by @hoetaek, including the suggested condition and a red-team case for the redeclare hazard. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/e2e-tests.yml | 1 + e2e/bug-15102b/.gitignore | 2 ++ e2e/bug-15102b/bootstrap.php | 9 +++++---- e2e/bug-15102b/composer.json | 7 +++++++ phpstan-baseline.neon | 6 ++++++ .../AutoloadFunctionsSourceLocator.php | 15 +++++++++++++++ 6 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 e2e/bug-15102b/.gitignore create mode 100644 e2e/bug-15102b/composer.json diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 1ab8c24c1ee..316c7bb6603 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -142,6 +142,7 @@ jobs: ../../bin/phpstan analyze - script: | cd e2e/bug-15102b + composer install ../../bin/phpstan analyze - script: | cd e2e/bug-15102 diff --git a/e2e/bug-15102b/.gitignore b/e2e/bug-15102b/.gitignore new file mode 100644 index 00000000000..3a9875b460f --- /dev/null +++ b/e2e/bug-15102b/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/bug-15102b/bootstrap.php b/e2e/bug-15102b/bootstrap.php index f86d4b17ba3..c97c5c80d25 100644 --- a/e2e/bug-15102b/bootstrap.php +++ b/e2e/bug-15102b/bootstrap.php @@ -1,9 +1,10 @@ getFileName(); + if ($functionFileName === false) { + return false; + } + // PHP canonicalises the path before it reaches a stream wrapper - a `/./` segment, a // symlinked directory or an include-path-relative name all arrive resolved - so the // trapped paths compare directly against get_included_files(). $includedFiles = get_included_files(); foreach ($locatedFiles as $locatedFile) { + if ($locatedFile !== $functionFileName) { + continue; + } + if (in_array($locatedFile, $includedFiles, true)) { return true; }