From 9de3bb9d71b62d611adb9b65d8db849e0efa98ab Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Wed, 7 Oct 2026 13:28:46 -0700 Subject: [PATCH 1/2] ext/fileinfo: fix use after free when streams reconstruct file info In `php_fileinfo_from_path()`, add an option to delay retrieving the `magic_set` until it is needed, after user callbacks are run, by passing in the `zval` holding the magic, rather than the magic set directly. Additionally, reload local magic variables after calling `php_fileinfo_from_path()`, since user callbacks may have run. --- ext/fileinfo/fileinfo.c | 17 ++++- .../tests/fileinfo-controlled-free.phpt | 75 +++++++++++++++++++ ...nfo-http-notification-reconstruct-uaf.phpt | 67 +++++++++++++++++ ...leinfo-no-stat-stream-reconstruct-uaf.phpt | 58 ++++++++++++++ 4 files changed, 213 insertions(+), 4 deletions(-) create mode 100644 ext/fileinfo/tests/fileinfo-controlled-free.phpt create mode 100644 ext/fileinfo/tests/fileinfo-http-notification-reconstruct-uaf.phpt create mode 100644 ext/fileinfo/tests/fileinfo-no-stat-stream-reconstruct-uaf.phpt diff --git a/ext/fileinfo/fileinfo.c b/ext/fileinfo/fileinfo.c index 36247b7196f4..704d3024d415 100644 --- a/ext/fileinfo/fileinfo.c +++ b/ext/fileinfo/fileinfo.c @@ -229,9 +229,9 @@ PHP_FUNCTION(finfo_set_flags) } /* }}} */ -static const char* php_fileinfo_from_path(struct magic_set *magic, const zend_string *path, php_stream_context *context) +static const char* php_fileinfo_from_path(struct magic_set *magic, const zval *self, const zend_string *path, php_stream_context *context) { - ZEND_ASSERT(magic != NULL); + ZEND_ASSERT((magic != NULL) ^ (self != NULL)); ZEND_ASSERT(path); ZEND_ASSERT(ZSTR_LEN(path) != 0); ZEND_ASSERT(!zend_str_has_nul_byte(path)); @@ -266,6 +266,9 @@ static const char* php_fileinfo_from_path(struct magic_set *magic, const zend_st } } if (!ret_val) { + if (magic == NULL) { + magic = Z_FINFO_P(self)->magic; + } ret_val = magic_stream(magic, stream); if (UNEXPECTED(ret_val == NULL)) { php_error_docref(NULL, E_WARNING, "Failed identify data %d:%s", magic_errno(magic), magic_error(magic)); @@ -313,7 +316,13 @@ PHP_FUNCTION(finfo_file) magic_setflags(magic, options); } - const char *ret_val = php_fileinfo_from_path(magic, path, context); + const char *ret_val = php_fileinfo_from_path(NULL, self, path, context); + + if (!Z_FINFO_P(self)->magic) { + zend_throw_error(NULL, "Invalid finfo object"); + RETURN_THROWS(); + } + magic = Z_FINFO_P(self)->magic; /* Restore options */ if (options) { @@ -425,7 +434,7 @@ PHP_FUNCTION(mime_content_type) const char *ret_val; if (path) { php_stream_context *context = php_stream_context_get_default(false); - ret_val = php_fileinfo_from_path(magic, path, context); + ret_val = php_fileinfo_from_path(magic, NULL, path, context); } else { /* remember stream position for restoration */ zend_off_t current_stream_pos = php_stream_tell(stream); diff --git a/ext/fileinfo/tests/fileinfo-controlled-free.phpt b/ext/fileinfo/tests/fileinfo-controlled-free.phpt new file mode 100644 index 000000000000..298ab7372b2e --- /dev/null +++ b/ext/fileinfo/tests/fileinfo-controlled-free.phpt @@ -0,0 +1,75 @@ +--TEST-- +Controlled free with re-entrant file info constructor +--EXTENSIONS-- +fileinfo +--CREDITS-- +Calif.io +--FILE-- +__construct( + FILEINFO_MIME_TYPE, + dirname(__DIR__) . '/fixtures/does-not-exist.magic', + ); + } catch (Throwable $error) { + echo "constructor-failed\n"; + } + + for ($i = 0; $i < 128; ++$i) { + $GLOBALS['spray'][] = str_repeat('A', 279); + } + echo "same-bin-spray-complete\n"; + } + return true; + } + + public function stream_stat(): false + { + return false; + } + + public function stream_read(int $count): string + { + $data = "%PDF-1.7\n1 0 obj\n<<>>\nendobj\n"; + $chunk = substr($data, $this->offset, $count); + $this->offset += strlen($chunk); + return $chunk; + } + + public function stream_eof(): bool + { + return $this->offset >= 32; + } +} + +stream_wrapper_register('auditcontrolled', AuditFileinfoControlledStream::class); + +$spray = []; +$finfo = new finfo(FILEINFO_MIME_TYPE); +var_dump($finfo->file('auditcontrolled://attacker-controlled-upload')); + +?> +--EXPECTF-- +stream-open-release +constructor-failed +same-bin-spray-complete + +Warning: finfo::file(): Failed identify data 22:Magic database is not open in %s on line %d + +Fatal error: Uncaught Error: Invalid finfo object in %s:%d +Stack trace: +#0 %s(%d): finfo->file('auditcontrolled...') +#1 {main} + thrown in %s on line %d diff --git a/ext/fileinfo/tests/fileinfo-http-notification-reconstruct-uaf.phpt b/ext/fileinfo/tests/fileinfo-http-notification-reconstruct-uaf.phpt new file mode 100644 index 000000000000..85782f6a123c --- /dev/null +++ b/ext/fileinfo/tests/fileinfo-http-notification-reconstruct-uaf.phpt @@ -0,0 +1,67 @@ +--TEST-- +Fileinfo reconstruction UAF with re-entrant file info constructor (HTTP notification) +--EXTENSIONS-- +fileinfo +--CREDITS-- +Calif.io +--SKIPIF-- + +--FILE-- + static function ( + int $notificationCode, + int $severity, + ?string $message, + int $messageCode, + int $bytesTransferred, + int $bytesMax, + ) use (&$reentered, &$spray, $finfo): void { + if ($reentered) { + return; + } + $reentered = true; + echo "http-notification-reconstruct:$notificationCode\n"; + try { + $finfo->__construct( + FILEINFO_MIME_TYPE, + dirname(__DIR__) . '/fixtures/does-not-exist.magic', + ); + } catch (Throwable $error) { + echo "constructor-failed\n"; + } + for ($i = 0; $i < 128; ++$i) { + $spray[] = str_repeat('A', 279); + } + echo "same-bin-spray-complete\n"; + }, +]); + +php_cli_server_start(); +$url = "http://" . PHP_CLI_SERVER_ADDRESS; +var_dump($finfo->file($url, 0, $context)); + +?> +--EXPECTF-- +http-notification-reconstruct:2 +constructor-failed +same-bin-spray-complete + +Warning: finfo::file(): Failed identify data 22:Magic database is not open in %s on line %d + +Fatal error: Uncaught Error: Invalid finfo object in %s:%d +Stack trace: +#0 %s(%d): finfo->file('http://localhos...', 0, Resource id #%d) +#1 {main} + thrown in %s on line %d diff --git a/ext/fileinfo/tests/fileinfo-no-stat-stream-reconstruct-uaf.phpt b/ext/fileinfo/tests/fileinfo-no-stat-stream-reconstruct-uaf.phpt new file mode 100644 index 000000000000..fe6a90dc32c3 --- /dev/null +++ b/ext/fileinfo/tests/fileinfo-no-stat-stream-reconstruct-uaf.phpt @@ -0,0 +1,58 @@ +--TEST-- +Fileinfo reconstruction UAF with re-entrant file info constructor (stream stat) +--EXTENSIONS-- +fileinfo +--CREDITS-- +Calif.io +--ENV-- +USE_ZEND_ALLOC=0 +--FILE-- +__construct(FILEINFO_MIME_TYPE); + } + return true; + } + + public function stream_stat(): false + { + return false; + } + + public function stream_read(int $count): string + { + $data = "%PDF-1.7\n1 0 obj\n<<>>\nendobj\n"; + $chunk = substr($data, $this->offset, $count); + $this->offset += strlen($chunk); + return $chunk; + } + + public function stream_eof(): bool + { + return $this->offset >= 32; + } + +} + +stream_wrapper_register('auditfinfo', AuditFileinfoStream::class); + +$finfo = new finfo(FILEINFO_MIME_TYPE); +var_dump($finfo->file('auditfinfo://attacker-controlled-upload')); + +?> +--EXPECTF-- +stream-open-reconstruct + +Warning: finfo::file(): AuditFileinfoStream::stream_cast is not implemented! in %s on line %d +string(15) "application/pdf" From ebda76ebf4dfe9fc726108d054753ee858fe9604 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Wed, 7 Oct 2026 14:24:03 -0700 Subject: [PATCH 2/2] Add url_stat() handlers --- ext/fileinfo/tests/fileinfo-controlled-free.phpt | 4 ++++ .../tests/fileinfo-no-stat-stream-reconstruct-uaf.phpt | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/ext/fileinfo/tests/fileinfo-controlled-free.phpt b/ext/fileinfo/tests/fileinfo-controlled-free.phpt index 298ab7372b2e..399b9e6a421f 100644 --- a/ext/fileinfo/tests/fileinfo-controlled-free.phpt +++ b/ext/fileinfo/tests/fileinfo-controlled-free.phpt @@ -40,6 +40,10 @@ final class AuditFileinfoControlledStream return false; } + public function url_stat(string $path, int $flags): array|false { + return false; + } + public function stream_read(int $count): string { $data = "%PDF-1.7\n1 0 obj\n<<>>\nendobj\n"; diff --git a/ext/fileinfo/tests/fileinfo-no-stat-stream-reconstruct-uaf.phpt b/ext/fileinfo/tests/fileinfo-no-stat-stream-reconstruct-uaf.phpt index fe6a90dc32c3..cfed52e056fd 100644 --- a/ext/fileinfo/tests/fileinfo-no-stat-stream-reconstruct-uaf.phpt +++ b/ext/fileinfo/tests/fileinfo-no-stat-stream-reconstruct-uaf.phpt @@ -30,6 +30,10 @@ final class AuditFileinfoStream return false; } + public function url_stat(string $path, int $flags): array|false { + return false; + } + public function stream_read(int $count): string { $data = "%PDF-1.7\n1 0 obj\n<<>>\nendobj\n";