[php-src] Issue #24578: Function JIT: wrong result (null) from a string-scanning loop with strcspn() after a comment-start byte
| From: | samad-aghaei | Date: | Thu, 22 Oct 2026 20:22:00 +0000 |
| Subject: | [php-src] Issue #24578: Function JIT: wrong result (null) from a string-scanning loop with strcspn() after a comment-start byte | ||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-253115@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/24578
Author: samad-aghaei
### Description
With
opcache.jit=function, the static method Repro::interpolate() below
returns null for two inputs that it handles correctly with
opcache.jit=tracing and opcache.jit=disable. The two inputs contain a byte
that may start a comment (- or /) but no placeholder, so the scanner
should copy the whole string.
The following code:
```php
<?php
declare(strict_types=1); final class Repro {
public static function skipComment(string $sql, int $position): int {
$byte = $sql[$position]; $next = $sql[$position + 1] ?? '';
if ($byte === '/' && $next === '*') { $end = strpos($sql,
'*/', $position + 2); return $end === false ? strlen($sql) : $end + 2; }
$dashes = $byte === '-' && $next === '-' &&
strspn($sql[$position + 2] ?? "\n", " \t\r\n") === 1;
if ($byte !== '#' && !$dashes) return $position; $end = strpos($sql,
"\n", $position);
return $end === false ? strlen($sql) : $end + 1; }
public static function skipQuoted(string $sql, int $position): int {
$quote = $sql[$position]; $length = strlen($sql); $stops = $quote === '' ?
'' : $quote . '\\'; $position++;
while ($position < $length) { $position += strcspn($sql, $stops, $position); if
($position >= $length) break;
if ($sql[$position] === '\\') $position += 2; elseif (($sql[$position + 1] ??
'') === $quote) $position += 2; else return $position + 1; }
return $length; }
public static function literal(mixed $value): ?string {
if ($value === null) return 'NULL'; if (is_bool($value)) return $value ?
'1' : '0'; if (is_int($value)) return (string) $value;
if (is_float($value)) return is_finite($value) ? var_export($value, true) : null; if
(!is_string($value)) return null;
return $value === '' ? "''" : "'" .
addslashes($value) . "'"; }
public static function interpolate(string $sql, array $params): ?string {
if (count($params) > 65535 || !array_is_list($params)) return null;
$length = strlen($sql); $count = count($params); $text = ''; $used = 0; $copied =
0; $position = 0;
while ($position < $length) { $position += strcspn($sql, "?'\"`#-/",
$position); if ($position >= $length) break; $byte = $sql[$position];
if ($byte === '?') { $literal = $used < $count ? self::literal(value:
$params[$used]) : null; if ($literal === null) return null;
$text .= substr($sql, $copied, $position - $copied) . $literal; $used++;
$position++; $copied = $position;
} elseif ($byte === "'" || $byte === '"' || $byte ===
'`') $position = self::skipQuoted(sql: $sql, position: $position);
else { $after = self::skipComment(sql: $sql, position: $position); $position = $after
=== $position ? $position + 1 : $after; }
}
return $used === $count ? $text . substr($sql, $copied) : null;
}
}
echo json_encode([Repro::interpolate('SELECT ? AS v', [3]),
Repro::interpolate('SELECT 5 -', []), Repro::interpolate('SELECT 5 / 2', [])]),
"\n";
```
Run with:
```sh
php -d opcache.enable_cli=1 -d opcache.file_update_protection=0 -d opcache.jit=function repro.php
```
Resulted in this output:
```
["SELECT 3 AS v",null,null]
```
But I expected this output instead (what opcache.jit=tracing and
opcache.jit=disable print):
```
["SELECT 3 AS v","SELECT 5 -","SELECT 5 \/ 2"]
```
Reproduces on PHP 8.5.12 and on master (8.7.0-dev, da7da45a44eb07ac999f5183b232a40d5f857131). The
script is a reduced version of a SQL placeholder interpolator.
### PHP Version
PHP 8.5.12 (cli) (NTS), built from source; also master 8.7.0-dev at
da7da45a44eb07ac999f5183b232a40d5f857131
### Operating System
Amazon Linux 2023, aarch64 (container)