From 6ce8dbe2a9ebd94b3b240de719670ace7b19b374 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 29 Oct 2026 10:01:20 -0400 Subject: [PATCH] ext/standard: Handle file_put_contents() truncation failures Fail locked replacements when truncation fails, before writing data or consuming the source stream. Preserve writes to native nonregular files, which may support locking and writing without supporting truncation. --- NEWS | 2 + ext/standard/file.c | 20 ++++- .../file_put_contents_lock_ex_device.phpt | 11 +++ ...put_contents_lock_ex_truncate_failure.phpt | 88 +++++++++++++++++++ 4 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 ext/standard/tests/file/file_put_contents_lock_ex_device.phpt create mode 100644 ext/standard/tests/file/file_put_contents_lock_ex_truncate_failure.phpt diff --git a/NEWS b/NEWS index 9e93da5c8d88..53440c2058a0 100644 --- a/NEWS +++ b/NEWS @@ -230,6 +230,8 @@ PHP NEWS properties). (Lazizbek Ergashev) - Standard: + . Fixed file_put_contents() with LOCK_EX ignoring truncation failures on + regular files. (Ilia Alshanetsky) . Fixed bug GH-24308 (Request shutdown calls closelog() even when syslog was not opened by PHP). (lazerg) . Fixed bug GH-24399 (usleep() and time_nanosleep() ignore queued APCs on diff --git a/ext/standard/file.c b/ext/standard/file.c index 933747ef8cf9..ff7bbbdd741e 100644 --- a/ext/standard/file.c +++ b/ext/standard/file.c @@ -493,8 +493,24 @@ PHP_FUNCTION(file_put_contents) RETURN_FALSE; } - if (mode[0] == 'c') { - php_stream_truncate_set_size(stream, 0); + if (mode[0] == 'c' && php_stream_truncate_set_size(stream, 0) != PHP_STREAM_OPTION_RETURN_OK) { + bool non_regular = false; + if (php_stream_is(stream, PHP_STREAM_IS_STDIO)) { +#ifdef PHP_WIN32 + php_socket_t fd; + if (php_stream_cast(stream, PHP_STREAM_AS_FD, (void **) &fd, 0) == SUCCESS) { + DWORD file_type = GetFileType((HANDLE) _get_osfhandle((int) fd)); + non_regular = file_type == FILE_TYPE_CHAR || file_type == FILE_TYPE_PIPE; + } +#else + php_stream_statbuf ssb; + non_regular = php_stream_stat(stream, &ssb) == 0 && !S_ISREG(ssb.sb.st_mode); +#endif + } + if (!non_regular) { + php_stream_close(stream); + RETURN_FALSE; + } } switch (Z_TYPE_P(data)) { diff --git a/ext/standard/tests/file/file_put_contents_lock_ex_device.phpt b/ext/standard/tests/file/file_put_contents_lock_ex_device.phpt new file mode 100644 index 000000000000..2e219364a6aa --- /dev/null +++ b/ext/standard/tests/file/file_put_contents_lock_ex_device.phpt @@ -0,0 +1,11 @@ +--TEST-- +file_put_contents() LOCK_EX permits writes to a device that cannot be truncated +--FILE-- + +--EXPECT-- +int(4) +int(0) diff --git a/ext/standard/tests/file/file_put_contents_lock_ex_truncate_failure.phpt b/ext/standard/tests/file/file_put_contents_lock_ex_truncate_failure.phpt new file mode 100644 index 000000000000..9f78595d42e9 --- /dev/null +++ b/ext/standard/tests/file/file_put_contents_lock_ex_truncate_failure.phpt @@ -0,0 +1,88 @@ +--TEST-- +file_put_contents() LOCK_EX stops writing after a wrapper truncation failure +--FILE-- + 'data', 'empty' => '', 'array' => ['data'], 'stream' => $source] as $label => $data) { + echo "$label:\n"; + var_dump(file_put_contents('file:///test', $data, LOCK_EX)); +} +echo "source position: ", ftell($source), "\n"; +fclose($source); + +echo "successful truncation:\n"; +FailingTruncateWrapper::$truncateResult = true; +var_dump(file_put_contents('file:///test', 'data', LOCK_EX)); + +echo "append:\n"; +FailingTruncateWrapper::$truncateResult = false; +var_dump(file_put_contents('file:///test', 'data', FILE_APPEND | LOCK_EX)); +stream_wrapper_restore('file'); +?> +--EXPECT-- +bool(true) +bool(true) +string: +truncate: 0 +close +bool(false) +empty: +truncate: 0 +close +bool(false) +array: +truncate: 0 +close +bool(false) +stream: +truncate: 0 +close +bool(false) +source position: 0 +successful truncation: +truncate: 0 +write +close +int(4) +append: +write +close +int(4)