Conversation
preg_match() has three outcomes -- 1, 0 and false -- but regex() folded false into the "no match" branch. An uncompilable pattern or an exhausted PCRE limit therefore leaked a raw PHP warning out of the library and then reported the misleading "The value ... does not match the expected pattern." Check for false separately and report the pattern together with preg_last_error_msg(), following the existing false === $x = nativeCall() handling in Assert::strlen(). The caller's custom message is intentionally not reused for that branch, since it describes a mismatch that never happened. Matching values and genuine mismatches are unaffected.
dualfroz
force-pushed
the
fix/regex-preg-match-failure
branch
from
September 5, 2026 22:59
e4ce4d0 to
ce2a73f
Compare
Collaborator
|
Honestly, this seems like the sort of thing that should be accounted for by tests in downstream usage. There are many places where this package assumes that the written (not user input) values are expected to be verified in downstream use. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Fixes #119 (one instance of it -- see "Scope" below).
Problem
Assert::regex()treats the return value ofpreg_match()as a boolean.preg_match()has three outcomes, not two:
1(match),0(no match) andfalse(the engine could notrun the match at all). Folding
falseinto the0branch produces two bad behaviours.1. A raw PHP warning escapes from inside the library, followed by a misleading exception.
on PHP 8.3.6 before this change:
The warning is emitted from library code the caller does not control, and the exception
then blames the value for what is actually a broken pattern.
2. A PCRE engine failure is silently reclassified as a failed assertion.
With
pcre.backtrack_limit=100:Here
preg_match()returnedfalsebecause it hit the backtrack limit -- it neverdetermined whether the value matches. The caller is told the value failed validation.
That is the more dangerous of the two: on a large input, a resource limit becomes an
indistinguishable "invalid input" error, and the real cause is invisible.
The same masking happens for
PREG_BAD_UTF8_ERROR,PREG_JIT_STACKLIMIT_ERROR, and so on.Root cause
src/Assert.php:1510!falseand!0are bothtrue, so the "engine failed" and "value did not match" casesare indistinguishable, and nothing suppresses or reports the warning
preg_match()raiseson its way out.
Solution
Separate the three outcomes, mirroring how this repository already handles a native
function that can return
false(Assert::strlen(),src/Assert.php:2590:if (false === $encoding = \mb_detect_encoding($value)) {).Now:
Notes on the two deliberate choices here:
$messageis not applied to the engine-failure branch. A custommessage such as
'The value %s is not a valid slug.'describes a mismatch. Reusing it fora pattern that never compiled would preserve exactly the confusion this change removes.
preg_last_error_msg()rather than from the raised warningtext. Capturing the warning text ("Compilation failed: missing closing parenthesis at
offset 1") would require
set_error_handler()/restore_error_handler(), and both arelisted in Psalm's
dictionaries/ImpureFunctionsList.php, so calling them inside a methodannotated
@psalm-pureis the wrong trade.preg_match()andpreg_last_error_msg()arenot in that list.
Behaviour on the normal paths is unchanged: a matching value still returns the value, and a
genuine mismatch still throws with the existing default or custom message. No public
signature changes, so no BC break.
How this was tested
Environment: PHP 8.3.6, Composer 2.10.2, on commit
2ccb7c2("Prepare for 2.4.1 release").Four tests were added to
tests/AssertTest.php, following the existing standalone-testconvention (
testResourceOfTypeCustomMessage,testEnumAssertionErrorMessage):testRegexRejectsAnUncompilablePatterntestRegexSuppressesTheWarningForAnUncompilablePatterntestRegexDoesNotUseTheCustomMessageForAnUncompilablePatterntestRegexRejectsAPatternExceedingTheBacktrackLimitEach new test fails without the source change and passes with it.
With
src/Assert.phpreverted to its original state and only the new tests applied:With the fix applied:
Full suite before the change:
Full suite after the change:
Same 2 notices and 56 skips in both runs -- they are pre-existing on a clean checkout
(the notices originate from
tests/AssertTest.php:618) and are unrelated to this change.The 4 added tests account for the entire delta.
One caveat on local verification:
composer run cs-checkandcomposer run static-analysiscould not be run on this machine.
friendsofphp/php-cs-fixer v3.92.3requires PHP >= 8.4via
symfony/console v8.0.3, andvimeo/psalm 6.14.3refuses to start withPsalm requires a PHP version ">= 8.3.16". You are running 8.3.6.The added code was insteadmatched by hand to the conventions already in the tree: Yoda comparisons (
false === $x, asin
src/Assert.php:829/897/2590), global classes imported rather than written with a leadingbackslash (as with
LogicException,Error,Exceptionintests/AssertTest.php), andconcatenation without surrounding spaces. CI will be the real check on both of those gates.