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)