From 29b019850de65b5aee4467dfee5fec4b9beafeea Mon Sep 17 00:00:00 2001 From: PurHur Date: Thu, 21 May 2026 16:53:50 +0000 Subject: [PATCH] Fix AOT/JIT nested $_FILES field access for web uploads (closes #87). Compile $_FILES['field'] as a child hashtable and allow chained ['name'] reads through boxed value slots; add AOT/compliance coverage for multipart uploads. Co-authored-by: Cursor --- lib/JIT.php | 1 + lib/JIT/Variable.php | 49 ++++++++++----- test/aot/RuntimeSuperglobalRefreshTest.php | 60 +++++++++++++++++++ .../compliance/cases/stdlib/files_nested.phpt | 19 ++++++ test/fixtures/aot/cases/web_files_nested.phpt | 22 +++++++ test/real/cases/web_files_nested.phpt | 18 ++++++ 6 files changed, 154 insertions(+), 15 deletions(-) create mode 100644 test/compliance/cases/stdlib/files_nested.phpt create mode 100644 test/fixtures/aot/cases/web_files_nested.phpt create mode 100644 test/real/cases/web_files_nested.phpt diff --git a/lib/JIT.php b/lib/JIT.php index 4647453097d..6f2329dafbe 100644 --- a/lib/JIT.php +++ b/lib/JIT.php @@ -826,6 +826,7 @@ private function assignOperand(Operand $result, Variable $value): void { $valueRef, $this->context->helper->loadValue($value) ); + $result->valueBoxHashtable = true; return; case Variable::TYPE_VALUE: diff --git a/lib/JIT/Variable.php b/lib/JIT/Variable.php index 614fb32d283..23d2bd68a1c 100755 --- a/lib/JIT/Variable.php +++ b/lib/JIT/Variable.php @@ -73,6 +73,9 @@ final class Variable { /** String literal value when this variable represents a constant string operand. */ public ?string $compileTimeString = null; + /** {@see __value__} slot holds a nested {@see __hashtable__} (e.g. $_FILES['field']). */ + public bool $valueBoxHashtable = false; + private static int $lvalueCounter = 0; public int $nextFreeElement = 0; @@ -419,13 +422,12 @@ public function dimFetch(self $dim, ?Type $expectedType = null, bool $forWrite = return HashTableHelper::writableStringKeyValueBox($this->context, $ht, $key); } - if (null !== $expectedType && Type::TYPE_ARRAY === $expectedType->type) { + if ('_FILES' === $this->superglobalName && !$forWrite) { $childHt = $this->context->builder->call( $this->context->lookupFunction('__hashtable__readStringKeyHashtable'), $ht, $key ); - return new Variable( $this->context, self::TYPE_HASHTABLE, @@ -433,28 +435,26 @@ public function dimFetch(self $dim, ?Type $expectedType = null, bool $forWrite = $childHt ); } - if (null !== $expectedType && Type::TYPE_STRING === $expectedType->type) { - $valPtr = $this->context->builder->call( - $this->context->lookupFunction('__hashtable__readStringKeyValue'), + if (null !== $expectedType && Type::TYPE_ARRAY === $expectedType->type) { + $childHt = $this->context->builder->call( + $this->context->lookupFunction('__hashtable__readStringKeyHashtable'), $ht, $key ); - $str = $this->context->builder->call( - $this->context->lookupFunction('__value__readString'), - $valPtr - ); - $owned = $this->context->builder->call( - $this->context->lookupFunction('__string__separate'), - $str - ); return new Variable( $this->context, - self::TYPE_STRING, + self::TYPE_HASHTABLE, self::KIND_VALUE, - $owned + $childHt ); } + if (null !== $expectedType && Type::TYPE_STRING === $expectedType->type) { + $this->context->refcount->addref($ht); + $boxed = HashTableHelper::readStringKeyToValueBox($this->context, $ht, $key); + + return $boxed; + } $this->context->refcount->addref($ht); $boxed = HashTableHelper::readStringKeyToValueBox($this->context, $ht, $key); @@ -491,6 +491,25 @@ public function dimFetch(self $dim, ?Type $expectedType = null, bool $forWrite = ); } return HashTableHelper::readIndexedToValueBox($this->context, $ht, $index); + case self::TYPE_VALUE: + if (!$this->valueBoxHashtable || self::TYPE_STRING !== $dim->type) { + throw new \LogicException( + 'Array dim fetch on __value__ requires a nested hashtable in this compiler build' + ); + } + $valPtr = JitValueBox::pointer($this->context, $this->value); + $childHt = $this->context->builder->call( + $this->context->lookupFunction('__value__readHashtable'), + $valPtr + ); + $htVar = new Variable( + $this->context, + self::TYPE_HASHTABLE, + self::KIND_VALUE, + $childHt + ); + + return $htVar->dimFetch($dim, $expectedType, $forWrite); default: if (!($this->type & self::IS_NATIVE_ARRAY)) { throw new \LogicException("Unsupported dim fetch on " . self::getStringType($this->type)); diff --git a/test/aot/RuntimeSuperglobalRefreshTest.php b/test/aot/RuntimeSuperglobalRefreshTest.php index 9240f83c885..5488a50537f 100644 --- a/test/aot/RuntimeSuperglobalRefreshTest.php +++ b/test/aot/RuntimeSuperglobalRefreshTest.php @@ -328,6 +328,66 @@ public function testMultipartPostBody(): void @unlink($bodyFile); } + /** + * multipart upload populates nested $_FILES for AOT JIT dim fetch (issue #87). + */ + public function testNestedFilesFieldAot(): void + { + $source = <<<'PHP' +assertNotFalse($outfile); + unlink($outfile); + + $repoRoot = dirname(__DIR__, 2); + $env = $this->llvmProcessEnv($repoRoot); + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + + $compile = proc_open( + array_merge( + self::llvmEnvPrefix(), + self::phpCommand(), + [$this->compileBin, '-o', $outfile] + ), + $descriptorSpec, + $pipes, + $repoRoot, + $env + ); + fwrite($pipes[0], $source); + fclose($pipes[0]); + $compileErr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($compile); + $this->assertFileExists($outfile, trim($compileErr !== false ? $compileErr : '')); + + $runEnv = $env; + $runEnv['REQUEST_METHOD'] = 'POST'; + $runEnv['REQUEST_BODY'] = "--phpcFileB\r\n" + ."Content-Disposition: form-data; name=\"doc\"; filename=\"f.txt\"\r\n" + ."Content-Type: text/plain\r\n\r\n" + ."bytes\r\n" + ."--phpcFileB--\r\n"; + $runEnv['CONTENT_TYPE'] = 'multipart/form-data; boundary=phpcFileB'; + $runEnv['SCRIPT_NAME'] = '/example.php'; + $runEnv['REQUEST_URI'] = '/example.php'; + $out = $this->runBinary($outfile, $runEnv); + $body = $this->cgiBody($out); + $this->assertStringContainsString('f.txt', $body); + + @unlink($outfile); + } + public function testHttpsSchemeFromCgiEnvironment(): void { $source = <<<'PHP' diff --git a/test/compliance/cases/stdlib/files_nested.phpt b/test/compliance/cases/stdlib/files_nested.phpt new file mode 100644 index 00000000000..0e7cfc29e6f --- /dev/null +++ b/test/compliance/cases/stdlib/files_nested.phpt @@ -0,0 +1,19 @@ +--TEST-- +stdlib nested $_FILES access (issue #87) +--ENV-- +REQUEST_METHOD=POST +CONTENT_TYPE=multipart/form-data; boundary=phpcJitFileB +--POST-- +--phpcJitFileB +Content-Disposition: form-data; name="doc"; filename="photo.txt" +Content-Type: text/plain + +filedata +--phpcJitFileB-- +--FILE-- +