[php-src] Issue #24578: Function JIT: wrong result (null) from a string-scanning loop with strcspn() after a comment-start byte

From: 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)

« previous php.bugs (#253115) next »