From 1e2896f2edc61087305b2f78042806e187d71e89 Mon Sep 17 00:00:00 2001
From: Apex Security Research <apex-security-research@proton.me>
Date: Sat, 31 Oct 2026 00:13:32 -0700
Subject: [PATCH] sparse: reject a sparse map larger than the archive member

GNU tar reads a PAX sparse member two different ways.  src/list.c:1484
calls sparse_skim_file, which skips archive_file_size - dumped_size bytes
and never looks at the sparse map.  src/extract.c:1463 calls
sparse_extract_file, which reads numbytes bytes for every region of that
map.  sparse_add_map (src/sparse.c:197) appends a region without a bound,
and the PAX decoders in src/xheader.c only check that the region count
stays inside the array, so nothing compares the total of the map against
the size the member header declares.

When the map asks for more bytes than the member holds, extract mode
consumes more of the archive than list mode and hands skim_file a negative
size.  skim_file ignores a negative size: seek_archive (src/buffer.c:1094)
returns 0 at "size <= skipped" and the copy loop in skim_file
(src/list.c:1417) is "while (size > 0)" at 1434.  The two modes then look
for the next header in different places, so "tar -t" and "tar -x" describe
different archives, and both exit 0 with no diagnostic.

tar's own writer keeps the invariant this patch checks: sparse_scan_file_seek
sums every region into archive_file_size.  The test goes in
tar_sparse_decode_header, which both readers call, so list mode and extract
mode consume the same bytes and stay in step.  It reuses the message that
src/sparse.c already has for a bad sparse member, so no new string needs
translation.

* src/sparse.c (tar_sparse_decode_header): Fail when the sparse map is
larger than the archive member.
---
 src/sparse.c | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

diff --git a/src/sparse.c b/src/sparse.c
index 727f187..d6d2c7d 100644
--- a/src/sparse.c
+++ b/src/sparse.c
@@ -154,8 +154,20 @@ tar_sparse_dump_header (struct tar_sparse_file *file)
 static bool
 tar_sparse_decode_header (struct tar_sparse_file *file)
 {
-  if (file->optab->decode_header)
-    return file->optab->decode_header (file);
+  if (file->optab->decode_header && !file->optab->decode_header (file))
+    return false;
+  /* The archive holds only the data regions, so the member must cover the
+     whole map.  Without this test sparse_skim_file skips archive_file_size
+     bytes without reading the map while sparse_extract_file reads it all.  */
+  off_t total = 0;
+  for (idx_t i = 0; i < file->stat_info->sparse_map_avail; i++)
+    if (ckd_add (&total, total, file->stat_info->sparse_map[i].numbytes)
+	|| total > file->stat_info->archive_file_size - file->dumped_size)
+      {
+	paxerror (0, _("%s: invalid sparse archive member"),
+		  file->stat_info->orig_file_name);
+	return false;
+      }
   return true;
 }
 
-- 
2.54.0 (Apple Git-157)

