Commit 808685f5763 for php
commit 808685f57633c2bc68c77001feeb4df83f97541b
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Sat Oct 3 09:54:36 2026 -0400
ext/standard: Reject incomplete sha1_file reads
Return false after a negative read or a zero read without EOF instead of
returning the digest of a prefix. Finalize the context and close the stream
on the failure path.
Closes GH-24099
diff --git a/NEWS b/NEWS
index 326a9fe7986..931bdc47686 100644
--- a/NEWS
+++ b/NEWS
@@ -199,6 +199,8 @@ PHP NEWS
(Ilia Alshanetsky)
- Standard:
+ . Fixed sha1_file() returning a digest for incomplete data after a stream
+ read failure. (Ilia Alshanetsky)
. Fixed three Windows-only proc_open() defects: an uninitialized
PROCESS_INFORMATION, an indeterminate comspec pointer after a failed
lookup, and an unchecked CreateFileA() failure. (Ilia Alshanetsky)
diff --git a/ext/standard/sha1.c b/ext/standard/sha1.c
index 95f2b54d0fd..7dd801c0151 100644
--- a/ext/standard/sha1.c
+++ b/ext/standard/sha1.c
@@ -84,10 +84,16 @@ PHP_FUNCTION(sha1_file)
PHP_SHA1Update(&context, buf, n);
}
+ bool failed = n < 0 || !php_stream_eof(stream);
+
PHP_SHA1Final(digest, &context);
php_stream_close(stream);
+ if (failed) {
+ RETURN_FALSE;
+ }
+
if (raw_output) {
RETURN_STRINGL((char *) digest, 20);
} else {
diff --git a/ext/standard/tests/strings/sha1_file_stream_error.phpt b/ext/standard/tests/strings/sha1_file_stream_error.phpt
new file mode 100644
index 00000000000..751cf5e7240
--- /dev/null
+++ b/ext/standard/tests/strings/sha1_file_stream_error.phpt
@@ -0,0 +1,77 @@
+--TEST--
+sha1_file() returns false when a stream read fails
+--FILE--
+<?php
+class TestStream
+{
+ public $context;
+ private string $mode;
+ private int $position = 0;
+
+ public function stream_open(string $path): bool
+ {
+ $this->mode = substr($path, strlen('test://'));
+
+ return true;
+ }
+
+ public function stream_read(int $count): string|false
+ {
+ $position = $this->position++;
+ if ($this->mode === 'error') {
+ return false;
+ }
+ if ($this->mode === 'empty' || $this->mode === 'stalled') {
+ return '';
+ }
+ if ($this->mode === 'short') {
+ return substr('prefix', $position * 2, 2);
+ }
+ if ($position === 0) {
+ return 'prefix';
+ }
+
+ return $this->mode === 'prefix-error' ? false : '';
+ }
+
+ public function stream_eof(): bool
+ {
+ return $this->mode === 'empty'
+ || ($this->mode === 'eof' && $this->position > 1)
+ || ($this->mode === 'short' && $this->position >= 3);
+ }
+}
+
+stream_wrapper_register('test', TestStream::class);
+
+foreach (['error', 'prefix-error', 'stalled', 'prefix-stalled', 'empty', 'eof', 'short'] as $mode) {
+ echo "$mode: ";
+ var_dump(sha1_file("test://$mode"));
+ echo "$mode (raw): ";
+ $result = sha1_file("test://$mode", true);
+ var_dump(is_string($result) ? bin2hex($result) : $result);
+}
+
+echo "directory: ";
+var_dump(@sha1_file(__DIR__));
+echo "directory (raw): ";
+$result = @sha1_file(__DIR__, true);
+var_dump(is_string($result) ? bin2hex($result) : $result);
+?>
+--EXPECT--
+error: bool(false)
+error (raw): bool(false)
+prefix-error: bool(false)
+prefix-error (raw): bool(false)
+stalled: bool(false)
+stalled (raw): bool(false)
+prefix-stalled: bool(false)
+prefix-stalled (raw): bool(false)
+empty: string(40) "da39a3ee5e6b4b0d3255bfef95601890afd80709"
+empty (raw): string(40) "da39a3ee5e6b4b0d3255bfef95601890afd80709"
+eof: string(40) "b4ebfe34d0fa97f0dd2bb1234fad8f59805f4e8d"
+eof (raw): string(40) "b4ebfe34d0fa97f0dd2bb1234fad8f59805f4e8d"
+short: string(40) "b4ebfe34d0fa97f0dd2bb1234fad8f59805f4e8d"
+short (raw): string(40) "b4ebfe34d0fa97f0dd2bb1234fad8f59805f4e8d"
+directory: bool(false)
+directory (raw): bool(false)