From 22dab6acc0bf46b1e898dd36a972ea6a85c06196 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Fri, 6 Nov 2026 21:40:40 +0800 Subject: [PATCH] ext/standard: Handle NUL bytes in version_compare() --- NEWS | 4 +- ext/standard/php_versioning.h | 1 + .../version_compare_null_bytes.phpt | 38 +++++------ ext/standard/versioning.c | 66 ++++++++++++++----- 4 files changed, 71 insertions(+), 38 deletions(-) diff --git a/NEWS b/NEWS index 78f010b74ce7..874d634fb58c 100644 --- a/NEWS +++ b/NEWS @@ -161,8 +161,8 @@ PHP NEWS - Standard: . Fixed quoted_printable_decode() truncating input at NUL bytes. (Weilin Du) - . Fixed version_compare() silently truncating version strings at NUL bytes by - rejecting such input. (Weilin Du) + . Fixed version_compare() silently truncating version strings at NUL bytes. + (Weilin Du) . Fixed bug #60110 (fclose(), file_put_contents(), copy() do not return false properly). (Jakub Zelenka, Ilija Tovilo) . Fixed three Windows-only proc_open() defects: an uninitialized diff --git a/ext/standard/php_versioning.h b/ext/standard/php_versioning.h index da3748079aa3..661cd3c6c580 100644 --- a/ext/standard/php_versioning.h +++ b/ext/standard/php_versioning.h @@ -19,5 +19,6 @@ PHPAPI char *php_canonicalize_version(const char *); PHPAPI int php_version_compare(const char *, const char *); +PHPAPI int php_version_compare_ex(const char *, size_t, const char *, size_t); #endif diff --git a/ext/standard/tests/versioning/version_compare_null_bytes.phpt b/ext/standard/tests/versioning/version_compare_null_bytes.phpt index 3a110f7cace1..e4b12cb46e41 100644 --- a/ext/standard/tests/versioning/version_compare_null_bytes.phpt +++ b/ext/standard/tests/versioning/version_compare_null_bytes.phpt @@ -1,26 +1,26 @@ --TEST-- -version_compare() rejects NUL bytes in version strings +version_compare() compares version strings after NUL bytes --FILE-- getMessage(), "\n"; - } -} -var_dump(version_compare('1.0', '1.0')); -var_dump(version_compare('1.0', '1.0', 'eq')); +var_dump(version_compare("1.0\0pl1", '1.0')); +var_dump(version_compare('1.0', "1.0\0pl1")); +var_dump(version_compare("1.0\0pl1", '1.0pl1')); +var_dump(version_compare("1.0\0dev", '1.0', 'lt')); +var_dump(version_compare("\0", '')); +var_dump(version_compare('', "\0")); +var_dump(version_compare("\0", "\0")); +var_dump(version_compare("\x001.0", '.1.0')); +var_dump(version_compare("1.0\0", '1.0')); +var_dump(version_compare("#\0pl1", '#.pl1')); ?> --EXPECT-- -ValueError: version_compare(): Argument #1 ($version1) must not contain any null bytes -ValueError: version_compare(): Argument #2 ($version2) must not contain any null bytes -ValueError: version_compare(): Argument #1 ($version1) must not contain any null bytes -ValueError: version_compare(): Argument #2 ($version2) must not contain any null bytes +int(1) +int(-1) int(0) bool(true) +int(1) +int(-1) +int(0) +int(0) +int(0) +int(0) diff --git a/ext/standard/versioning.c b/ext/standard/versioning.c index 2a51f66449c0..68e2b233ae1b 100644 --- a/ext/standard/versioning.c +++ b/ext/standard/versioning.c @@ -22,12 +22,11 @@ /* {{{ php_canonicalize_version() */ -PHPAPI char * -php_canonicalize_version(const char *version) +static char * +php_canonicalize_version_ex(const char *version, size_t len) { - size_t len = strlen(version); char *buf = safe_emalloc(len * 2, sizeof(char), 1), *q, lp, lq; - const char *p; + const char *p, *end = version + len; if (len == 0) { *buf = '\0'; @@ -37,8 +36,11 @@ php_canonicalize_version(const char *version) p = version; q = buf; *q++ = lp = *p++; + if (lp == '\0') { + q[-1] = '.'; + } - while (*p) { + while (p < end) { /* s/[-_+]/./g; * s/([^\d\.])([^\D\.])/$1.$2/g; * s/([^\D\.])([^\d\.])/$1.$2/g; @@ -48,7 +50,7 @@ php_canonicalize_version(const char *version) #define isspecialver(x) ((x)=='-'||(x)=='_'||(x)=='+') lq = *(q - 1); - if (isspecialver(*p)) { + if (*p == '\0' || isspecialver(*p)) { if (lq != '.') { *q++ = '.'; } @@ -78,6 +80,12 @@ php_canonicalize_version(const char *version) return buf; } +PHPAPI char * +php_canonicalize_version(const char *version) +{ + return php_canonicalize_version_ex(version, strlen(version)); +} + /* }}} */ /* {{{ compare_special_version_forms() */ @@ -123,8 +131,20 @@ static int compare_special_version_forms(char *form1, char *form2) /* }}} */ /* {{{ php_version_compare() */ +static char *php_copy_special_version(const char *version, size_t len) +{ + char *copy = estrndup(version, len); + + for (size_t i = 0; i < len; i++) { + if (copy[i] == '\0') { + copy[i] = '.'; + } + } + return copy; +} + PHPAPI int -php_version_compare(const char *orig_ver1, const char *orig_ver2) +php_version_compare_ex(const char *orig_ver1, size_t ver1_len, const char *orig_ver2, size_t ver2_len) { char *ver1; char *ver2; @@ -132,22 +152,28 @@ php_version_compare(const char *orig_ver1, const char *orig_ver2) long l1, l2; int compare = 0; - if (!*orig_ver1 || !*orig_ver2) { - if (!*orig_ver1 && !*orig_ver2) { + if (ver1_len == 0 || ver2_len == 0) { + if (ver1_len == 0 && ver2_len == 0) { return 0; } else { - return *orig_ver1 ? 1 : -1; + return ver1_len != 0 ? 1 : -1; } } if (orig_ver1[0] == '#') { - ver1 = estrdup(orig_ver1); + ver1 = php_copy_special_version(orig_ver1, ver1_len); } else { - ver1 = php_canonicalize_version(orig_ver1); + ver1 = php_canonicalize_version_ex(orig_ver1, ver1_len); } if (orig_ver2[0] == '#') { - ver2 = estrdup(orig_ver2); + ver2 = php_copy_special_version(orig_ver2, ver2_len); } else { - ver2 = php_canonicalize_version(orig_ver2); + ver2 = php_canonicalize_version_ex(orig_ver2, ver2_len); + } + if (!*ver1 || !*ver2) { + compare = *ver1 ? 1 : (*ver2 ? -1 : 0); + efree(ver1); + efree(ver2); + return compare; } p1 = n1 = ver1; p2 = n2 = ver2; @@ -204,6 +230,12 @@ php_version_compare(const char *orig_ver1, const char *orig_ver2) return compare; } +PHPAPI int +php_version_compare(const char *orig_ver1, const char *orig_ver2) +{ + return php_version_compare_ex(orig_ver1, strlen(orig_ver1), orig_ver2, strlen(orig_ver2)); +} + /* }}} */ /* {{{ Compares two "PHP-standardized" version number strings */ @@ -215,13 +247,13 @@ PHP_FUNCTION(version_compare) int compare; ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_PATH(v1, v1_len) - Z_PARAM_PATH(v2, v2_len) + Z_PARAM_STRING(v1, v1_len) + Z_PARAM_STRING(v2, v2_len) Z_PARAM_OPTIONAL Z_PARAM_STR_OR_NULL(op) ZEND_PARSE_PARAMETERS_END(); - compare = php_version_compare(v1, v2); + compare = php_version_compare_ex(v1, v1_len, v2, v2_len); if (!op) { RETURN_LONG(compare); }