From 47fed3eeec23cf137d64ace71304ee8de29c11f1 Mon Sep 17 00:00:00 2001 From: PurHur Date: Mon, 13 Jul 2026 17:38:15 +0000 Subject: [PATCH] Language: fix unpack() inline pack + enum offset call-arg slots (#8866) Wire unpack('i', pack(...), E::A) so the nested pack EXEC_RETURN feeds argument #2 and the hoisted enum feeds argument #3, matching Zend's TypeError on the offset operand instead of misreporting argument #2. Co-authored-by: Cursor --- lib/Compiler.php | 173 +++++++++++++++++- .../stdlib/unpack_enum_offset_typeerror.phpt | 15 ++ .../maintainer_gap_unpack_enum_offset.php | 13 ++ test/unit/InlineCallArgProducerSlotTest.php | 22 +++ 4 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 test/compliance/cases/stdlib/unpack_enum_offset_typeerror.phpt create mode 100644 test/repro/maintainer_gap_unpack_enum_offset.php diff --git a/lib/Compiler.php b/lib/Compiler.php index 77ec77bfe43..0234bb618dc 100755 --- a/lib/Compiler.php +++ b/lib/Compiler.php @@ -16906,7 +16906,10 @@ private function slotForHoistedClassConstFetchCallArg( if (!$this->callArgUsesHoistedEnumPreludeSlot($callArg)) { return null; } - if ($this->nestedFuncCallFeedsDeadInlineCallArgZero($block, $callOp, $argIndex)) { + if ( + $this->nestedFuncCallFeedsDeadInlineCallArgZero($block, $callOp, $argIndex) + || $this->nestedFuncCallFeedsDeadInlineCallArg($block, $callOp, $argIndex) + ) { return null; } $preludeProducer = $this->hoistedPreludeProducerForCallArgIndex($callOp, $argIndex, $block); @@ -22020,7 +22023,10 @@ private function hoistedPreludeProducerForCallArgIndex(Op $callOp, int $argIndex if (null === $ordinal) { return null; } - if ($this->nestedFuncCallFeedsDeadInlineCallArgZero($block, $callOp, $argIndex)) { + if ( + $this->nestedFuncCallFeedsDeadInlineCallArgZero($block, $callOp, $argIndex) + || $this->nestedFuncCallFeedsDeadInlineCallArg($block, $callOp, $argIndex) + ) { return null; } $producers = $this->hoistedPreludeProducersImmediatelyBeforeCall($callOp, $block); @@ -22037,6 +22043,18 @@ private function hoistedPreludeProducerForCallArgIndex(Op $callOp, int $argIndex // tempnam(g(), E::A) — nested FuncCall feeds arg #0, not trailing enum (#10303, #16558). return null; } + $nestedIndex = array_search($nestedForArgZero, $block->orig->children, true); + if (\is_int($nestedIndex)) { + $targetArg = $this->siblingMultiArgFuncCallProducerTargetArgIndex( + $nestedIndex, + $callIndex, + $block->orig->children + ); + if (null !== $targetArg && $targetArg === $argIndex) { + // unpack('i', pack(...), E::A) — middle arg is nested FuncCall (#8866). + return null; + } + } $sole = $producers[0] ?? null; return $sole instanceof Op\Expr ? $sole : null; @@ -31800,6 +31818,109 @@ private function compileArrayPadInlineHaystackCallArgSends( return array_merge($producerOps, $sends); } + /** + * unpack('i', pack('i', 1), E::A) — inline pack string + trailing enum offset (#8866). + * + * @param list $args + * + * @return list|null + */ + private function compileUnpackInlinePackEnumOffsetCallArgSends( + array $args, + Block $block, + Op $cfgCallOp + ): ?array { + if (null === $block->orig || !\is_array($cfgCallOp->args ?? null)) { + return null; + } + if ('unpack' !== $this->resolveCfgFuncCallName($cfgCallOp)) { + return null; + } + if (3 !== \count($cfgCallOp->args)) { + return null; + } + $stringArg = $cfgCallOp->args[1] ?? null; + $offsetArg = $cfgCallOp->args[2] ?? null; + if ( + !$this->callArgIsDeadInlineTemporary($stringArg) + || !$this->callArgUsesHoistedEnumPreludeSlot($offsetArg) + || !$this->isEmbeddedCallLiteralArg($cfgCallOp->args[0] ?? null) + ) { + return null; + } + $producers = $this->precedingInlineCallArgProducersBeforeCfgOp($block->orig->children, $cfgCallOp); + $packProducer = null; + $enumProducer = null; + foreach ($producers as $producer) { + if ($producer instanceof Op\Expr\FuncCall || $producer instanceof Op\Expr\NsFuncCall) { + $packProducer = $producer; + } elseif ($producer instanceof Op\Expr\ClassConstFetch) { + $enumProducer = $producer; + } + } + if (null === $packProducer || null === $enumProducer) { + return null; + } + $callIndex = $this->cfgCallOpIndex($block, $cfgCallOp); + $packIndex = array_search($packProducer, $block->orig->children, true); + if (!\is_int($callIndex) || !\is_int($packIndex)) { + return null; + } + $producerOps = []; + if (null === $block->slotForOperand($enumProducer->result)) { + foreach ($this->compileExpr($enumProducer, $block) as $op) { + $producerOps[] = $op; + } + } + $enumSlot = $block->slotForOperand($enumProducer->result); + if (null === $enumSlot) { + return null; + } + if (null === $block->slotForOperand($packProducer->result)) { + $prevForce = $this->forceDeferredSiblingCallReturnSlot; + $this->forceDeferredSiblingCallReturnSlot = true; + try { + foreach ($this->compileExpr($packProducer, $block) as $op) { + $producerOps[] = $op; + } + } finally { + $this->forceDeferredSiblingCallReturnSlot = $prevForce; + } + } + $packSlot = $this->slotForInlineFuncCallProducerExecReturnByCfgIndex( + $block, + $packIndex, + $block->orig->children + ) ?? $this->slotForLastEmittedInlineCallResultBeforePendingFuncCall($block) + ?? $block->slotForOperand($packProducer->result); + if (null === $packSlot) { + return null; + } + $sends = []; + foreach ($args as $argIndex => $arg) { + $valueSlot = match ((int) $argIndex) { + 1 => (string) $packSlot, + 2 => (string) $enumSlot, + default => null, + }; + $literalProbe = $cfgCallOp->args[(int) $argIndex] ?? $arg; + if (null === $valueSlot && $this->isEmbeddedCallLiteralArg($literalProbe)) { + $valueSlot = (string) $this->freshLiteralConstantSlot($literalProbe, $block); + } + if (null === $valueSlot) { + $valueSlot = $this->compileOperand($arg, $block, true); + } + $sends[] = new OpCode( + OpCode::TYPE_ARG_SEND, + $valueSlot, + $this->callArgNameSlot($arg, $block), + $this->callArgUnpack($arg) ? 1 : null + ); + } + + return array_merge($producerOps, $sends); + } + /** * array_pad([1], 4, 0, ArrayPadType::Positive) — inline Array_ + trailing pad_type ClassConstFetch (#17240). * @@ -33097,6 +33218,41 @@ private function nestedFuncCallFeedsDeadInlineCallArgZero(Block $block, Op $call return 2 === \count($callOp->args ?? []); } + /** + * unpack('i', pack(...), E::A) — nested FuncCall feeds a middle dead-temp arg, not enum (#8866). + */ + private function nestedFuncCallFeedsDeadInlineCallArg(Block $block, Op $callOp, int $argIndex): bool + { + if (null === $block->orig) { + return false; + } + $callIndex = $this->cfgCallOpIndex($block, $callOp); + if (null === $callIndex) { + return false; + } + $nested = $this->nestedFuncCallProducerBeforeTrailingConstFetchPreludes( + $callOp, + $callIndex, + $block->orig->children + ); + if ( + !($nested instanceof Op\Expr\FuncCall || $nested instanceof Op\Expr\NsFuncCall) + ) { + return false; + } + $nestedIndex = array_search($nested, $block->orig->children, true); + if (!\is_int($nestedIndex)) { + return false; + } + $targetArg = $this->siblingMultiArgFuncCallProducerTargetArgIndex( + $nestedIndex, + $callIndex, + $block->orig->children + ); + + return null !== $targetArg && $targetArg === $argIndex; + } + /** Stmt-level side-effect builtins — not hoisted multi-arg producers (#16451, #16480). */ private function isStatementLevelSideEffectFuncCall(Op\Expr $call): bool { @@ -36294,6 +36450,15 @@ private function compileCallArgRuntimeEnumConstFetchOps( if (null !== $this->findInlineArrayProducerForCallArg($arg, $block, $cfgCallOp)) { return []; } + if ( + null !== $cfgCallOp + && ( + $this->nestedFuncCallFeedsDeadInlineCallArgZero($block, $cfgCallOp, $argIndex) + || $this->nestedFuncCallFeedsDeadInlineCallArg($block, $cfgCallOp, $argIndex) + ) + ) { + return []; + } // register_shutdown_function(fn(...), E::A) — arg #0 is hoisted Closure, not enum prelude (#5751). if ( null !== $cfgCallOp @@ -39774,6 +39939,10 @@ protected function compileCallArgSends( if (null !== $arrayPadEnumLengthSends) { return $arrayPadEnumLengthSends; } + $unpackPackEnumSends = $this->compileUnpackInlinePackEnumOffsetCallArgSends($args, $block, $cfgCallOp); + if (null !== $unpackPackEnumSends) { + return $unpackPackEnumSends; + } $extractSends = $this->compileExtractInlineMultiArgCallArgSends($args, $block, $cfgCallOp); if (null !== $extractSends) { return $extractSends; diff --git a/test/compliance/cases/stdlib/unpack_enum_offset_typeerror.phpt b/test/compliance/cases/stdlib/unpack_enum_offset_typeerror.phpt new file mode 100644 index 00000000000..6ae2de03bc3 --- /dev/null +++ b/test/compliance/cases/stdlib/unpack_enum_offset_typeerror.phpt @@ -0,0 +1,15 @@ +--TEST-- +stdlib unpack() $offset — enum case operand TypeError (#8866, ext/standard/pack.c) +--FILE-- +getMessage(), "\n"; +} +?> +--EXPECT-- +TypeError: unpack(): Argument #3 ($offset) must be of type int, E given diff --git a/test/repro/maintainer_gap_unpack_enum_offset.php b/test/repro/maintainer_gap_unpack_enum_offset.php new file mode 100644 index 00000000000..6f76f0a8d4a --- /dev/null +++ b/test/repro/maintainer_gap_unpack_enum_offset.php @@ -0,0 +1,13 @@ +getMessage(), "\n"; +} diff --git a/test/unit/InlineCallArgProducerSlotTest.php b/test/unit/InlineCallArgProducerSlotTest.php index 9e932e3c4a9..09fbbdb4c04 100644 --- a/test/unit/InlineCallArgProducerSlotTest.php +++ b/test/unit/InlineCallArgProducerSlotTest.php @@ -8474,4 +8474,26 @@ public function testCurlFileBuiltinRegisterCompilesWithoutAssignInCallRhsOperand $block = $runtime->parseAndCompileFile($path); self::assertNotNull($block); } + + /** Issue #8866 — unpack('i', pack('i', 1), E::A) wires pack EXEC_RETURN to arg #1, enum to arg #2. */ + public function testUnpackInlinePackEnumOffsetCallArgSlots(): void + { + $code = <<<'PHP' +getMessage(), "\n"; +} +PHP; + ob_start(); + $runtime = new Runtime(); + $runtime->run($runtime->parseAndCompile($code, 'unpack_inline_pack_enum.php')); + $out = ob_get_clean(); + self::assertSame( + 'TypeError: unpack(): Argument #3 ($offset) must be of type int, E given', + trim($out) + ); + } }