From 7f0800714b551976c19d17435ee53f02a6f27e5a Mon Sep 17 00:00:00 2001 From: Pavel Nekrasov Date: Fri, 9 Oct 2026 17:50:56 +0300 Subject: [PATCH] ext/phar: Fix tar entry size handling on 32-bit systems Tar entry sizes at or above 2 GiB cannot be represented by the signed zend_off_t used by php_stream_seek() on 32-bit systems. In addition, rounding an entry size to a 512-byte boundary can overflow uint32_t, and unchecked php_stream_tell() failures can be converted to valid size_t values. Reject archive and entry sizes outside the supported stream offset range, check rounding before it occurs, and validate data skips against the archive length. Signed-off-by: Pavel Nekrasov Signed-off-by: Georgij Tsarin --- NEWS | 4 +++ ext/phar/tar.c | 56 +++++++++++++++++++++++++++---------- ext/phar/tests/gh24242.phpt | 39 ++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 14 deletions(-) create mode 100644 ext/phar/tests/gh24242.phpt diff --git a/NEWS b/NEWS index aaed43baa2c8..eea9f00ae726 100644 --- a/NEWS +++ b/NEWS @@ -41,6 +41,10 @@ PHP NEWS . Fixed a duplicate warning when ATTR_STATEMENT_CLASS is rejected on a persistent connection. (Ilia Alshanetsky) +- Phar: + . Fixed bug GH-24242 (Incorrect handling of large tar entry sizes on + 32-bit systems). (Pavel Nekrasov, Georgij Tsarin) + - Phpdbg: . Fix wrong command names. (ndossche, Weilin Du) diff --git a/ext/phar/tar.c b/ext/phar/tar.c index 1f0c3a472750..33d568862137 100644 --- a/ext/phar/tar.c +++ b/ext/phar/tar.c @@ -146,6 +146,22 @@ static uint32_t phar_tar_checksum(char *buf, size_t len) /* {{{ */ } /* }}} */ +static bool phar_tar_skip(php_stream *fp, uint32_t size, size_t totalsize) +{ + zend_off_t offset = php_stream_tell(fp); + + if (offset < 0 +#if SIZEOF_ZEND_LONG == 4 + || size > ZEND_LONG_MAX +#endif + || (size_t) offset > totalsize || size > totalsize - (size_t) offset) { + return false; + } + + return php_stream_seek(fp, size, SEEK_CUR) == 0 + && php_stream_tell(fp) == offset + (zend_off_t) size; +} + bool phar_is_tar(char *buf, char *fname) /* {{{ */ { tar_header *header = (tar_header *) buf; @@ -253,6 +269,8 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch char buf[512], *actual_alias = NULL, *p; phar_entry_info entry = {0}; size_t pos = 0, read, totalsize; + zend_off_t end; + php_stream_statbuf ssb; tar_header *hdr; uint32_t sum1, sum2, size, old; phar_archive_data *myphar, *actual; @@ -263,9 +281,17 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch *error = NULL; } - php_stream_seek(fp, 0, SEEK_END); - totalsize = php_stream_tell(fp); - php_stream_seek(fp, 0, SEEK_SET); + if ((php_stream_stat(fp, &ssb) == 0 && ssb.sb.st_size > ZEND_LONG_MAX) + || php_stream_seek(fp, 0, SEEK_END) != 0 + || (end = php_stream_tell(fp)) < 0 + || php_stream_seek(fp, 0, SEEK_SET) != 0) { + if (error) { + spprintf(error, 4096, "phar error: tar file \"%s\" exceeds the supported stream offset range", fname); + } + php_stream_close(fp); + return FAILURE; + } + totalsize = (size_t) end; read = php_stream_read(fp, buf, sizeof(buf)); if (read != sizeof(buf)) { @@ -317,7 +343,8 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch } } - if (!phar_tar_size(hdr->size, sizeof(hdr->size), &size)) { + if (!phar_tar_size(hdr->size, sizeof(hdr->size), &size) + || (uint64_t) size + 511 > (uint64_t) ZEND_LONG_MAX) { if (error) { spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (invalid entry size)", fname); } @@ -390,12 +417,17 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch goto bail; } myphar->sig_len = sig_len; - php_stream_seek(fp, curloc + 512, SEEK_SET); + if (curloc < 0 || (size_t) curloc > totalsize + || totalsize - (size_t) curloc < 512 + || php_stream_seek(fp, curloc + 512, SEEK_SET) != 0) { + if (error) { + spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (truncated)", fname); + } + goto bail; + } /* signature checked out, let's ensure this is the last file in the phar */ if (((hdr->typeflag == '\0') || (hdr->typeflag == TAR_FILE)) && size > 0) { - /* this is not good enough - seek succeeds even on truncated tars */ - php_stream_seek(fp, 512, SEEK_CUR); - if ((uint32_t)php_stream_tell(fp) > totalsize) { + if (!phar_tar_skip(fp, 512, totalsize)) { if (error) { spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (truncated)", fname); } @@ -461,9 +493,7 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch /* skip blank stuff */ size = ((size+511)&~511) - size; - /* this is not good enough - seek succeeds even on truncated tars */ - php_stream_seek(fp, size, SEEK_CUR); - if ((uint32_t)php_stream_tell(fp) > totalsize) { + if (!phar_tar_skip(fp, size, totalsize)) { efree(entry.filename); if (error) { spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (truncated)", fname); @@ -649,9 +679,7 @@ zend_result phar_parse_tarfile(php_stream* fp, char *fname, size_t fname_len, ch if (phar_tar_type_has_data(hdr->typeflag) && size > 0) { next: - /* this is not good enough - seek succeeds even on truncated tars */ - php_stream_seek(fp, size, SEEK_CUR); - if ((uint32_t)php_stream_tell(fp) > totalsize) { + if (!phar_tar_skip(fp, size, totalsize)) { if (error) { spprintf(error, 4096, "phar error: \"%s\" is a corrupted tar file (truncated)", fname); } diff --git a/ext/phar/tests/gh24242.phpt b/ext/phar/tests/gh24242.phpt new file mode 100644 index 000000000000..5ee98efc7c3c --- /dev/null +++ b/ext/phar/tests/gh24242.phpt @@ -0,0 +1,39 @@ +--TEST-- +GH-24242: Reject truncated tar entries whose size overflows a 32-bit stream offset +--EXTENSIONS-- +phar +--FILE-- +getFilename(), "\n"; + echo 'Size: ', $file->getSize(), "\n"; + } +} catch (UnexpectedValueException $e) { + echo $e->getMessage(), "\n"; +} +?> +--CLEAN-- + +--EXPECTF-- +phar error: "%sgh24242.tar" is a corrupted tar file (%s)