From dcbf1e0ededa95849f098d154a25876ed5754bcf Mon Sep 17 00:00:00 2001
From: Tobias Stoeckmann <stoeckmann@users.noreply.github.com>
Date: Tue, 15 Apr 2025 06:02:17 +0200
Subject: [PATCH] Do not skip past EOF while reading (#2584)

Make sure to not skip past end of file for better error messages. One
such example is now visible with rar testsuite. You can see the
difference already by an actually not useless use of cat:

```
$ cat .../test_read_format_rar_ppmd_use_after_free.rar | bsdtar -t
bsdtar: Archive entry has empty or unreadable filename ... skipping.
bsdtar: Archive entry has empty or unreadable filename ... skipping.
bsdtar: Truncated input file (needed 119 bytes, only 0 available)
bsdtar: Error exit delayed from previous errors.
```

compared to

```
$ bsdtar -tf .../test_read_format_rar_ppmd_use_after_free.rar
bsdtar: Archive entry has empty or unreadable filename ... skipping.
bsdtar: Archive entry has empty or unreadable filename ... skipping.
bsdtar: Error exit delayed from previous errors.
```

Since the former cannot lseek, the error is a different one
(ARCHIVE_FATAL vs ARCHIVE_EOF). The piped version states explicitly that
truncation occurred, while the latter states EOF because the skip past
the end of file was successful.

Signed-off-by: Tobias Stoeckmann <tobias@stoeckmann.org>

CVE: CVE-2025-5918
Upstream-Status: Backport [https://github.com/libarchive/libarchive/commit/dcbf1e0ededa95849f098d154a25876ed5754bcf]
Signed-off-by: Peter Marko <peter.marko@siemens.com>
---
 libarchive/archive_read_open_fd.c       | 13 +++++++---
 libarchive/archive_read_open_file.c     | 33 +++++++++++++++++++------
 libarchive/archive_read_open_filename.c | 16 +++++++++---
 libarchive/test/test_read_format_rar.c  |  6 ++---
 4 files changed, 50 insertions(+), 18 deletions(-)

diff --git a/libarchive/archive_read_open_fd.c b/libarchive/archive_read_open_fd.c
index 3fd536d5..dc7c9e52 100644
--- a/libarchive/archive_read_open_fd.c
+++ b/libarchive/archive_read_open_fd.c
@@ -53,6 +53,7 @@ __FBSDID("$FreeBSD: head/lib/libarchive/archive_read_open_fd.c 201103 2009-12-28
 struct read_fd_data {
 	int	 fd;
 	size_t	 block_size;
+	int64_t	 size;
 	char	 use_lseek;
 	void	*buffer;
 };
@@ -96,6 +97,7 @@ archive_read_open_fd(struct archive *a, int fd, size_t block_size)
 	if (S_ISREG(st.st_mode)) {
 		archive_read_extract_set_skip_file(a, st.st_dev, st.st_ino);
 		mine->use_lseek = 1;
+		mine->size = st.st_size;
 	}
 #if defined(__CYGWIN__) || defined(_WIN32)
 	setmode(mine->fd, O_BINARY);
@@ -152,9 +154,14 @@ file_skip(struct archive *a, void *client_data, int64_t request)
 	if (skip == 0)
 		return (0);
 
-	if (((old_offset = lseek(mine->fd, 0, SEEK_CUR)) >= 0) &&
-	    ((new_offset = lseek(mine->fd, skip, SEEK_CUR)) >= 0))
-		return (new_offset - old_offset);
+	if ((old_offset = lseek(mine->fd, 0, SEEK_CUR)) >= 0) {
+		if (old_offset >= mine->size ||
+		    skip > mine->size - old_offset) {
+			/* Do not seek past end of file. */
+			errno = ESPIPE;
+		} else if ((new_offset = lseek(mine->fd, skip, SEEK_CUR)) >= 0)
+			return (new_offset - old_offset);
+	}
 
 	/* If seek failed once, it will probably fail again. */
 	mine->use_lseek = 0;
diff --git a/libarchive/archive_read_open_file.c b/libarchive/archive_read_open_file.c
index 2829b9a5..6ed18a0c 100644
--- a/libarchive/archive_read_open_file.c
+++ b/libarchive/archive_read_open_file.c
@@ -53,6 +53,7 @@ __FBSDID("$FreeBSD: head/lib/libarchive/archive_read_open_file.c 201093 2009-12-
 struct read_FILE_data {
 	FILE    *f;
 	size_t	 block_size;
+	int64_t	 size;
 	void	*buffer;
 	char	 can_skip;
 };
@@ -92,6 +93,7 @@ archive_read_open_FILE(struct archive *a, FILE *f)
 		archive_read_extract_set_skip_file(a, st.st_dev, st.st_ino);
 		/* Enable the seek optimization only for regular files. */
 		mine->can_skip = 1;
+		mine->size = st.st_size;
 	}
 
 #if defined(__CYGWIN__) || defined(_WIN32)
@@ -131,6 +133,7 @@ FILE_skip(struct archive *a, void *client_data, int64_t request)
 #else
 	long skip = (long)request;
 #endif
+	int64_t old_offset, new_offset;
 	int skip_bits = sizeof(skip) * 8 - 1;
 
 	(void)a; /* UNUSED */
@@ -154,19 +157,33 @@ FILE_skip(struct archive *a, void *client_data, int64_t request)
 
 #ifdef __ANDROID__
         /* fileno() isn't safe on all platforms ... see above. */
-	if (lseek(fileno(mine->f), skip, SEEK_CUR) < 0)
+	old_offset = lseek(fileno(mine->f), 0, SEEK_CUR);
 #elif HAVE_FSEEKO
-	if (fseeko(mine->f, skip, SEEK_CUR) != 0)
+	old_offset = ftello(mine->f);
 #elif HAVE__FSEEKI64
-	if (_fseeki64(mine->f, skip, SEEK_CUR) != 0)
+	old_offset = _ftelli64(mine->f);
 #else
-	if (fseek(mine->f, skip, SEEK_CUR) != 0)
+	old_offset = ftell(mine->f);
 #endif
-	{
-		mine->can_skip = 0;
-		return (0);
+	if (old_offset >= 0) {
+		if (old_offset < mine->size &&
+		    skip <= mine->size - old_offset) {
+#ifdef __ANDROID__
+			new_offset = lseek(fileno(mine->f), skip, SEEK_CUR);
+#elif HAVE__FSEEKI64
+			new_offset = _fseeki64(mine->f, skip, SEEK_CUR);
+#elif HAVE_FSEEKO
+			new_offset = fseeko(mine->f, skip, SEEK_CUR);
+#else
+			new_offset = fseek(mine->f, skip, SEEK_CUR);
+#endif
+			if (new_offset >= 0)
+				return (new_offset - old_offset);
+		}
 	}
-	return (request);
+
+	mine->can_skip = 0;
+	return (0);
 }
 
 /*
diff --git a/libarchive/archive_read_open_filename.c b/libarchive/archive_read_open_filename.c
index 3894b15c..5f5b3f1f 100644
--- a/libarchive/archive_read_open_filename.c
+++ b/libarchive/archive_read_open_filename.c
@@ -75,6 +75,7 @@ struct read_file_data {
 	size_t	 block_size;
 	void	*buffer;
 	mode_t	 st_mode;  /* Mode bits for opened file. */
+	int64_t	 size;
 	char	 use_lseek;
 	enum fnt_e { FNT_STDIN, FNT_MBS, FNT_WCS } filename_type;
 	union {
@@ -370,8 +371,10 @@ file_open(struct archive *a, void *client_data)
 	mine->st_mode = st.st_mode;
 
 	/* Disk-like inputs can use lseek(). */
-	if (is_disk_like)
+	if (is_disk_like) {
 		mine->use_lseek = 1;
+		mine->size = st.st_size;
+	}
 
 	return (ARCHIVE_OK);
 fail:
@@ -465,9 +468,14 @@ file_skip_lseek(struct archive *a, void *client_data, int64_t request)
 			skip = max_skip;
 	}
 
-	if ((old_offset = lseek(mine->fd, 0, SEEK_CUR)) >= 0 &&
-	    (new_offset = lseek(mine->fd, skip, SEEK_CUR)) >= 0)
-		return (new_offset - old_offset);
+	if ((old_offset = lseek(mine->fd, 0, SEEK_CUR)) >= 0) {
+		if (old_offset >= mine->size ||
+		    skip > mine->size - old_offset) {
+			/* Do not seek past end of file. */
+			errno = ESPIPE;
+		} else if ((new_offset = lseek(mine->fd, skip, SEEK_CUR)) >= 0)
+			return (new_offset - old_offset);
+	}
 
 	/* If lseek() fails, don't bother trying again. */
 	mine->use_lseek = 0;
diff --git a/libarchive/test/test_read_format_rar.c b/libarchive/test/test_read_format_rar.c
index dce567af..fce44a9d 100644
--- a/libarchive/test/test_read_format_rar.c
+++ b/libarchive/test/test_read_format_rar.c
@@ -3776,8 +3776,8 @@ DEFINE_TEST(test_read_format_rar_ppmd_use_after_free)
   assertA(ARCHIVE_OK == archive_read_next_header(a, &ae));
   assertA(archive_read_data(a, buf, sizeof(buf)) <= 0);
 
-  /* Test EOF */
-  assertA(1 == archive_read_next_header(a, &ae));
+  /* Test for truncation */
+  assertA(ARCHIVE_FATAL == archive_read_next_header(a, &ae));
 
   assertEqualIntA(a, ARCHIVE_OK, archive_read_close(a));
   assertEqualInt(ARCHIVE_OK, archive_read_free(a));
@@ -3803,7 +3803,7 @@ DEFINE_TEST(test_read_format_rar_ppmd_use_after_free2)
   assertA(archive_read_data(a, buf, sizeof(buf)) <= 0);
 
   /* Test EOF */
-  assertA(1 == archive_read_next_header(a, &ae));
+  assertA(ARCHIVE_FATAL == archive_read_next_header(a, &ae));
 
   assertEqualIntA(a, ARCHIVE_OK, archive_read_close(a));
   assertEqualInt(ARCHIVE_OK, archive_read_free(a));
