Commit d3c95d1745d for php.net

commit d3c95d1745de99b39034f8147f4659182143e6e9
Author: Gina Peter Banyard <girgias@php.net>
Date:   Thu Jul 17 22:33:01 2025 +0100

    ext/standard: Throw ValueError for filenames with null bytes

    This should never happen in the first place

diff --git a/NEWS b/NEWS
index cb70bda237f..d90af7f5e64 100644
--- a/NEWS
+++ b/NEWS
@@ -13,6 +13,13 @@ PHP                                                                        NEWS
     error handler instead of emitting a warning and continuing with an empty
     string. (Weilin Du)

+- Standard:
+  . The following functions now raise a ValueError when the $filename argument
+    contains NUL bytes: fileperms(), fileinode(), filesize(), fileowner(),
+    filegroup(), fileatime(), filemtime(), filectime(), filetype(),
+    is_writable(), is_readable(), is_executable(), is_file(), is_dir(),
+    is_link(), file_exists(), lstat(), stat(). (Girgias)
+
 30 Jul 2026, PHP 8.6.0alpha3

 - Core:
diff --git a/UPGRADING b/UPGRADING
index 599ae2290d6..b516f24db97 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -228,6 +228,26 @@ PHP 8.6 UPGRADE NOTES
     bytes.
   . base_convert(), bindex(), hexdec() and octdec() now raise a notice when
     they cannot precisely convert the given number.
+  . The following functions now raise a ValueError when the $filename argument
+    contains NUL bytes:
+    - fileperms()
+    - fileinode()
+    - filesize()
+    - fileowner()
+    - filegroup()
+    - fileatime()
+    - filemtime()
+    - filectime()
+    - filetype()
+    - is_writable()
+    - is_readable()
+    - is_executable()
+    - is_file()
+    - is_dir()
+    - is_link()
+    - file_exists()
+    - lstat()
+    - stat()

 - Sysvshm:
   . shm_attach() now raises a ValueError when the $key argument is outside the
diff --git a/ext/standard/filestat.c b/ext/standard/filestat.c
index 7dcfecf1f87..de4fcd82a62 100644
--- a/ext/standard/filestat.c
+++ b/ext/standard/filestat.c
@@ -996,7 +996,7 @@ ZEND_NAMED_FUNCTION(name) { \
 	zend_string *filename; \
 	\
 	ZEND_PARSE_PARAMETERS_START(1, 1) \
-		Z_PARAM_STR(filename) \
+		Z_PARAM_PATH_STR(filename) \
 	ZEND_PARSE_PARAMETERS_END(); \
 	\
 	php_stat(filename, funcnum, return_value); \
diff --git a/ext/standard/tests/file/bug39863.phpt b/ext/standard/tests/file/bug39863.phpt
index 88c569473a7..2a0ca01ea65 100644
--- a/ext/standard/tests/file/bug39863.phpt
+++ b/ext/standard/tests/file/bug39863.phpt
@@ -6,7 +6,11 @@
 <?php

 $filename = __FILE__ . chr(0). ".ridiculous";
-var_dump(file_exists($filename));
+try {
+    var_dump(file_exists($filename));
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
 ?>
 --EXPECT--
-bool(false)
+ValueError: file_exists(): Argument #1 ($filename) must not contain any null bytes
diff --git a/ext/standard/tests/file/filegroup_null_byte.phpt b/ext/standard/tests/file/filegroup_null_byte.phpt
new file mode 100644
index 00000000000..4cb80399c61
--- /dev/null
+++ b/ext/standard/tests/file/filegroup_null_byte.phpt
@@ -0,0 +1,14 @@
+--TEST--
+filegroup() with filenames with null bytes
+--FILE--
+<?php
+
+try {
+    var_dump(filegroup("file_with_null_byte.tmp\0"));
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+ValueError: filegroup(): Argument #1 ($filename) must not contain any null bytes
diff --git a/ext/standard/tests/file/filegroup_variation3.phpt b/ext/standard/tests/file/filegroup_variation3.phpt
index e844be07721..94562850f58 100644
--- a/ext/standard/tests/file/filegroup_variation3.phpt
+++ b/ext/standard/tests/file/filegroup_variation3.phpt
@@ -26,10 +26,6 @@
   "//filegroup_variation3//filegroup_variation3.tmp",
   "/filegroup_variation3/*.tmp",
   "filegroup_variation3/filegroup*.tmp",
-
-  /* Testing Binary safe */
-  "/filegroup_variation3/filegroup_variation3.tmp".chr(0),
-  "/filegroup_variation3/filegroup_variation3.tmp\0"
 );

 $count = 1;
@@ -74,13 +70,5 @@

 Warning: filegroup(): stat failed for %s/filegroup_variation3/filegroup*.tmp in %s on line %d
 bool(false)
-- Iteration 7 -
-
-Warning: filegroup(): Filename contains null byte in %s on line %d
-bool(false)
-- Iteration 8 -
-
-Warning: filegroup(): Filename contains null byte in %s on line %d
-bool(false)

 *** Done ***
diff --git a/ext/standard/tests/file/fileinode_null_byte.phpt b/ext/standard/tests/file/fileinode_null_byte.phpt
new file mode 100644
index 00000000000..48b8272d97c
--- /dev/null
+++ b/ext/standard/tests/file/fileinode_null_byte.phpt
@@ -0,0 +1,14 @@
+--TEST--
+fileinode() with filenames with null bytes
+--FILE--
+<?php
+
+try {
+    var_dump(fileinode("file_with_null_byte.tmp\0"));
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+ValueError: fileinode(): Argument #1 ($filename) must not contain any null bytes
diff --git a/ext/standard/tests/file/fileinode_variation3.phpt b/ext/standard/tests/file/fileinode_variation3.phpt
index 92357a3619e..506984d4177 100644
--- a/ext/standard/tests/file/fileinode_variation3.phpt
+++ b/ext/standard/tests/file/fileinode_variation3.phpt
@@ -25,10 +25,6 @@
   "//fileinode_variation3//fileinode_variation3.tmp",
   "/fileinode_variation3/*.tmp",
   "fileinode_variation3/fileinode*.tmp",
-
-  /* Testing Binary safe */
-  "/fileinode_variation3/fileinode_variation3.tmp".chr(0),
-  "/fileinode_variation3/fileinode_variation3.tmp\0"
 );

 $count = 1;
@@ -73,13 +69,5 @@

 Warning: fileinode(): stat failed for %s/fileinode_variation3/fileinode*.tmp in %s on line %d
 bool(false)
-- Iteration 7 -
-
-Warning: fileinode(): Filename contains null byte in %s on line %d
-bool(false)
-- Iteration 8 -
-
-Warning: fileinode(): Filename contains null byte in %s on line %d
-bool(false)

 *** Done ***
diff --git a/ext/standard/tests/file/fileowner_null_byte.phpt b/ext/standard/tests/file/fileowner_null_byte.phpt
new file mode 100644
index 00000000000..909ac0fe310
--- /dev/null
+++ b/ext/standard/tests/file/fileowner_null_byte.phpt
@@ -0,0 +1,14 @@
+--TEST--
+fileowner() with filenames with null bytes
+--FILE--
+<?php
+
+try {
+    var_dump(fileowner("file_with_null_byte.tmp\0"));
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+ValueError: fileowner(): Argument #1 ($filename) must not contain any null bytes
diff --git a/ext/standard/tests/file/fileowner_variation3.phpt b/ext/standard/tests/file/fileowner_variation3.phpt
index 63ba6936aaa..e6360b47571 100644
--- a/ext/standard/tests/file/fileowner_variation3.phpt
+++ b/ext/standard/tests/file/fileowner_variation3.phpt
@@ -26,10 +26,6 @@
   "//fileowner_variation3//fileowner_variation3.tmp",
   "/fileowner_variation3/*.tmp",
   "fileowner_variation3/fileowner*.tmp",
-
-  /* Testing Binary safe */
-  "/fileowner_variation3/fileowner_variation3.tmp".chr(0),
-  "/fileowner_variation3/fileowner_variation3.tmp\0"
 );

 $count = 1;
@@ -74,13 +70,5 @@

 Warning: fileowner(): stat failed for %s/fileowner_variation3/fileowner*.tmp in %s on line %d
 bool(false)
-- Iteration 7 -
-
-Warning: fileowner(): Filename contains null byte in %s on line %d
-bool(false)
-- Iteration 8 -
-
-Warning: fileowner(): Filename contains null byte in %s on line %d
-bool(false)

 *** Done ***
diff --git a/ext/standard/tests/file/fileperms_null_byte.phpt b/ext/standard/tests/file/fileperms_null_byte.phpt
new file mode 100644
index 00000000000..1530d0e5bf9
--- /dev/null
+++ b/ext/standard/tests/file/fileperms_null_byte.phpt
@@ -0,0 +1,14 @@
+--TEST--
+fileperms() with filenames with null bytes
+--FILE--
+<?php
+
+try {
+    var_dump(fileperms("file_with_null_byte.tmp\0"));
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+ValueError: fileperms(): Argument #1 ($filename) must not contain any null bytes
diff --git a/ext/standard/tests/file/fileperms_variation3.phpt b/ext/standard/tests/file/fileperms_variation3.phpt
index 5e981e9b863..6c1da17d6bf 100644
--- a/ext/standard/tests/file/fileperms_variation3.phpt
+++ b/ext/standard/tests/file/fileperms_variation3.phpt
@@ -25,10 +25,6 @@
   "//fileperms_variation3//fileperms_variation3.tmp",
   "/fileperms_variation3/*.tmp",
   "fileperms_variation3/fileperms*.tmp",
-
-  /* Testing Binary safe */
-  "/fileperms_variation3/fileperms_variation3.tmp".chr(0),
-  "/fileperms_variation3/fileperms_variation3.tmp\0"
 );

 $count = 1;
@@ -73,13 +69,5 @@

 Warning: fileperms(): stat failed for %s/fileperms_variation3/fileperms*.tmp in %s on line %d
 bool(false)
-- Iteration 7 -
-
-Warning: fileperms(): Filename contains null byte in %s on line %d
-bool(false)
-- Iteration 8 -
-
-Warning: fileperms(): Filename contains null byte in %s on line %d
-bool(false)

 *** Done ***
diff --git a/ext/standard/tests/file/is_dir_null_byte.phpt b/ext/standard/tests/file/is_dir_null_byte.phpt
new file mode 100644
index 00000000000..a49ccc0e5c4
--- /dev/null
+++ b/ext/standard/tests/file/is_dir_null_byte.phpt
@@ -0,0 +1,14 @@
+--TEST--
+is_dir() with filenames with null bytes
+--FILE--
+<?php
+
+try {
+    var_dump(is_dir("file_with_null_byte.tmp\0"));
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+ValueError: is_dir(): Argument #1 ($filename) must not contain any null bytes
diff --git a/ext/standard/tests/file/is_dir_variation4.phpt b/ext/standard/tests/file/is_dir_variation4.phpt
index d6e64efe211..30285804a33 100644
--- a/ext/standard/tests/file/is_dir_variation4.phpt
+++ b/ext/standard/tests/file/is_dir_variation4.phpt
@@ -23,10 +23,6 @@
   "./is_dir_variation4//",
   ".//is_dir_variation4//",
   "is_dir_vari*",
-
-  /* Testing Binary safe */
-  "./is_dir_variation4/".chr(0),
-  "is_dir_variation4\0"
 );

 $count = 1;
@@ -75,10 +71,4 @@
 -- Iteration 8 --
 bool(false)

--- Iteration 9 --
-bool(false)
-
--- Iteration 10 --
-bool(false)
-
 *** Done ***
diff --git a/ext/standard/tests/file/is_executable_null_byte.phpt b/ext/standard/tests/file/is_executable_null_byte.phpt
new file mode 100644
index 00000000000..7c26d53061d
--- /dev/null
+++ b/ext/standard/tests/file/is_executable_null_byte.phpt
@@ -0,0 +1,14 @@
+--TEST--
+is_executable() with filenames with null bytes
+--FILE--
+<?php
+
+try {
+    var_dump(is_executable("file_with_null_byte.tmp\0"));
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+ValueError: is_executable(): Argument #1 ($filename) must not contain any null bytes
diff --git a/ext/standard/tests/file/is_executable_variation1.phpt b/ext/standard/tests/file/is_executable_variation1.phpt
index a85b9a2ef02..289abb1a265 100644
--- a/ext/standard/tests/file/is_executable_variation1.phpt
+++ b/ext/standard/tests/file/is_executable_variation1.phpt
@@ -33,11 +33,6 @@
   "$file_path/is_executable_variation1/*.tmp",
   "$file_path/is_executable_variation1/b*.tmp",

-  /* Testing Binary safe */
-  "$file_path/is_executable_variation1".chr(0)."bar.temp",
-  "$file_path".chr(0)."is_executable_variation1/bar.temp",
-  "$file_path/is_executable_variation1x000/",
-
   /* Testing directories */
   ".",  // current directory, exp: bool(true)
   "$file_path/is_executable_variation1"  // temp directory, exp: bool(true)
@@ -76,13 +71,7 @@
 -- Iteration 5 --
 bool(false)
 -- Iteration 6 --
-bool(false)
--- Iteration 7 --
-bool(false)
--- Iteration 8 --
-bool(false)
--- Iteration 9 --
 bool(true)
--- Iteration 10 --
+-- Iteration 7 --
 bool(true)
 Done
diff --git a/ext/standard/tests/file/is_file_null_byte.phpt b/ext/standard/tests/file/is_file_null_byte.phpt
new file mode 100644
index 00000000000..f544d7bcf58
--- /dev/null
+++ b/ext/standard/tests/file/is_file_null_byte.phpt
@@ -0,0 +1,14 @@
+--TEST--
+is_file() with filenames with null bytes
+--FILE--
+<?php
+
+try {
+    var_dump(is_file("file_with_null_byte.tmp\0"));
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+ValueError: is_file(): Argument #1 ($filename) must not contain any null bytes
diff --git a/ext/standard/tests/file/is_file_variation4.phpt b/ext/standard/tests/file/is_file_variation4.phpt
index 1afc34dd03b..9d98b7fd90d 100644
--- a/ext/standard/tests/file/is_file_variation4.phpt
+++ b/ext/standard/tests/file/is_file_variation4.phpt
@@ -23,10 +23,6 @@
   "//is_file_variation4//is_file_variation4.tmp",
   "/is_file_variation4/*.tmp",
   "is_file_variation4/is_file*.tmp",
-
-  /* Testing Binary safe */
-  "/is_file_variation4/is_file_variation4.tmp".chr(0),
-  "/is_file_variation4/is_file_variation4.tmp\0"
 );

 $count = 1;
@@ -65,9 +61,5 @@
 bool(false)
 - Iteration 6 -
 bool(false)
-- Iteration 7 -
-bool(false)
-- Iteration 8 -
-bool(false)

 *** Done ***
diff --git a/ext/standard/tests/file/is_readable_null_byte.phpt b/ext/standard/tests/file/is_readable_null_byte.phpt
new file mode 100644
index 00000000000..711700f4e7e
--- /dev/null
+++ b/ext/standard/tests/file/is_readable_null_byte.phpt
@@ -0,0 +1,14 @@
+--TEST--
+is_readable() with filenames with null bytes
+--FILE--
+<?php
+
+try {
+    var_dump(is_readable("file_with_null_byte.tmp\0"));
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+ValueError: is_readable(): Argument #1 ($filename) must not contain any null bytes
diff --git a/ext/standard/tests/file/is_readable_variation1.phpt b/ext/standard/tests/file/is_readable_variation1.phpt
index 9c25213f8ab..8548a840ccb 100644
--- a/ext/standard/tests/file/is_readable_variation1.phpt
+++ b/ext/standard/tests/file/is_readable_variation1.phpt
@@ -32,11 +32,6 @@
   "$file_path/is_readable_variation1/*.tmp",
   "$file_path/is_readable_variation1/b*.tmp",

-  /* Testing Binary safe */
-  "$file_path/is_readable_variation1".chr(0)."bar.tmp",
-  "$file_path".chr(0)."is_readable_variation1/bar.tmp",
-  "$file_path".chr(0)."is_readable_variation1/bar.tmp",
-
   /* Testing directories */
   ".",  // current directory, exp: bool(true)
   "$file_path/is_readable_variation1"  // temp directory, exp: bool(true)
@@ -77,13 +72,7 @@
 -- Iteration 6 --
 bool(false)
 -- Iteration 7 --
-bool(false)
--- Iteration 8 --
-bool(false)
--- Iteration 9 --
-bool(false)
--- Iteration 10 --
 bool(true)
--- Iteration 11 --
+-- Iteration 8 --
 bool(true)
 Done
diff --git a/ext/standard/tests/file/is_writable_null_byte.phpt b/ext/standard/tests/file/is_writable_null_byte.phpt
new file mode 100644
index 00000000000..319db04cc15
--- /dev/null
+++ b/ext/standard/tests/file/is_writable_null_byte.phpt
@@ -0,0 +1,14 @@
+--TEST--
+is_writable() with filenames with null bytes
+--FILE--
+<?php
+
+try {
+    var_dump(is_writable("file_with_null_byte.tmp\0"));
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+ValueError: is_writable(): Argument #1 ($filename) must not contain any null bytes
diff --git a/ext/standard/tests/file/is_writable_variation1.phpt b/ext/standard/tests/file/is_writable_variation1.phpt
index 80695d6d45a..181b1ba4e26 100644
--- a/ext/standard/tests/file/is_writable_variation1.phpt
+++ b/ext/standard/tests/file/is_writable_variation1.phpt
@@ -31,11 +31,6 @@
   "$file_path/is_writable_variation1/*.tmp",
   "$file_path/is_writable_variation1/b*.tmp",

-  /* Testing Binary safe */
-  "$file_path/is_writable_variation1".chr(0)."bar.tmp",
-  "$file_path".chr(0)."is_writable_variation1/bar.tmp",
-  "$file_path".chr(0)."is_writable_variation1/bar.tmp",
-
   /* Testing directories */
   ".",  // current directory, exp: bool(true)
   "$file_path/is_writable_variation1"  // temp directory, exp: bool(true)
@@ -87,18 +82,9 @@
 bool(false)
 bool(false)
 -- Iteration 7 --
-bool(false)
-bool(false)
--- Iteration 8 --
-bool(false)
-bool(false)
--- Iteration 9 --
-bool(false)
-bool(false)
--- Iteration 10 --
 bool(true)
 bool(true)
--- Iteration 11 --
+-- Iteration 8 --
 bool(true)
 bool(true)
 Done