Merge pull request #2991 from kientzle/kientzle-tar-timestamp-hardening

[tar] Harden timestamp parsing

(cherry picked from commit ca7f0bfa93)
This commit is contained in:
Tim Kientzle authored and Martin Matuska committed 2026-06-23 10:24:10 +02:00
1 parent fa35c91f89
commit d6e8e98886
7 files changed
+341 -18

No files matched your search

+4
View File
@@ -554,6 +554,8 @@ libarchive_test_SOURCES= \
libarchive/test/test_read_format_tar_pax_g_large.c \
libarchive/test/test_read_format_tar_pax_large_attr.c \
libarchive/test/test_read_format_tar_pax_negative_time.c \
libarchive/test/test_read_format_tar_pax_timestamps.c \
libarchive/test/test_read_format_tar_timestamp_overflow.c \
libarchive/test/test_read_format_tbz.c \
libarchive/test/test_read_format_tgz.c \
libarchive/test/test_read_format_tlz.c \
@@ -1021,6 +1023,8 @@ libarchive_test_EXTRA_DIST=\
libarchive/test/test_read_format_tar_pax_g_large.tar.uu \
libarchive/test/test_read_format_tar_pax_large_attr.tar.Z.uu \
libarchive/test/test_read_format_tar_pax_negative_time.tar.uu \
libarchive/test/test_read_format_tar_pax_timestamps.tar.uu \
libarchive/test/test_read_format_tar_timestamp_overflow.tar.uu \
libarchive/test/test_read_format_ustar_filename_cp866.tar.Z.uu \
libarchive/test/test_read_format_ustar_filename_eucjp.tar.Z.uu \
libarchive/test/test_read_format_ustar_filename_koi8r.tar.Z.uu \
+118 -18
View File
@@ -248,6 +248,36 @@ static const size_t fflags_limit = 512; /* Longest fflags */
static const size_t acl_limit = 131072; /* Longest textual ACL: 128kiB */
static const int64_t entry_limit = 0xfffffffffffffffLL; /* 2^60 bytes = 1 ExbiByte */
/*
* There's no standard for TIME_T_MAX. So we compute it
* here. TODO: Move this to configure time, but be careful
* about cross-compile environments.
*/
static time_t
get_time_t_max(void)
{
#if defined(TIME_T_MAX)
return TIME_T_MAX;
#else
/* ISO C allows time_t to be a floating-point type,
but POSIX requires an integer type. The following
should work on any system that follows the POSIX
conventions. */
if (((time_t)0) < ((time_t)-1)) {
/* Time_t is unsigned */
return (~(time_t)0);
} else {
/* Time_t is signed. */
/* Assume it's the same as int64_t or int32_t */
if (sizeof(time_t) == sizeof(int64_t)) {
return (time_t)INT64_MAX;
} else {
return (time_t)INT32_MAX;
}
}
#endif
}
int
archive_read_support_format_gnutar(struct archive *a)
{
@@ -1369,7 +1399,12 @@ header_common(struct archive_read *a, struct tar *tar,
archive_entry_set_gid(entry, tar_atol(header->gid, sizeof(header->gid)));
}
if (!archive_entry_mtime_is_set(entry)) {
archive_entry_set_mtime(entry, tar_atol(header->mtime, sizeof(header->mtime)), 0);
int64_t t64 = tar_atol(header->mtime, sizeof(header->mtime));
time_t t = (time_t)t64;
if ((int64_t)t != t64) { /* time_t overflowed */
t = get_time_t_max();
}
archive_entry_set_mtime(entry, t, 0);
}
/* Reconcile the size info. */
@@ -2268,7 +2303,7 @@ pax_attribute_SCHILY_acl(struct archive_read *a, struct tar *tar,
}
static int
pax_attribute_read_time(struct archive_read *a, size_t value_length, int64_t *ps, long *pn, int64_t *unconsumed) {
pax_attribute_read_time(struct archive_read *a, size_t value_length, __LA_TIME_T *ps, long *pn, int64_t *unconsumed) {
struct archive_string as;
int r;
@@ -2288,12 +2323,16 @@ pax_attribute_read_time(struct archive_read *a, size_t value_length, int64_t *ps
return (r);
}
pax_time(as.s, archive_strlen(&as), ps, pn);
int64_t sec = 0;
pax_time(as.s, archive_strlen(&as), &sec, pn);
archive_string_free(&as);
if (*ps == INT64_MIN) {
if (sec == INT64_MIN) {
*ps = 0;
*pn = 0;
return (ARCHIVE_WARN);
} else {
*ps = (__LA_TIME_T)sec;
}
return (ARCHIVE_OK);
}
@@ -2509,8 +2548,13 @@ pax_attribute(struct archive_read *a, struct tar *tar, struct archive_entry *ent
*/
if (key_length == 12 && memcmp(key, "creationtime", 12) == 0) {
/* LIBARCHIVE.creationtime */
if ((err = pax_attribute_read_time(a, value_length, &t, &n, unconsumed)) == ARCHIVE_OK) {
archive_entry_set_birthtime(entry, t, n);
__LA_TIME_T sec = 0;
if ((err = pax_attribute_read_time(a, value_length, &sec, &n, unconsumed)) == ARCHIVE_OK) {
archive_entry_set_birthtime(entry, sec, n);
} else {
archive_set_error(&a->archive,
ARCHIVE_ERRNO_MISC,
"Ignoring malformed pax creationtime");
}
return (err);
}
@@ -2738,16 +2782,26 @@ pax_attribute(struct archive_read *a, struct tar *tar, struct archive_entry *ent
break;
case 'a':
if (key_length == 5 && memcmp(key, "atime", 5) == 0) {
if ((err = pax_attribute_read_time(a, value_length, &t, &n, unconsumed)) == ARCHIVE_OK) {
archive_entry_set_atime(entry, t, n);
__LA_TIME_T sec = 0;
if ((err = pax_attribute_read_time(a, value_length, &sec, &n, unconsumed)) == ARCHIVE_OK) {
archive_entry_set_atime(entry, sec, n);
} else {
archive_set_error(&a->archive,
ARCHIVE_ERRNO_MISC,
"Ignoring malformed pax atime");
}
return (err);
}
break;
case 'c':
if (key_length == 5 && memcmp(key, "ctime", 5) == 0) {
if ((err = pax_attribute_read_time(a, value_length, &t, &n, unconsumed)) == ARCHIVE_OK) {
archive_entry_set_ctime(entry, t, n);
__LA_TIME_T sec = 0;
if ((err = pax_attribute_read_time(a, value_length, &sec, &n, unconsumed)) == ARCHIVE_OK) {
archive_entry_set_ctime(entry, sec, n);
} else {
archive_set_error(&a->archive,
ARCHIVE_ERRNO_MISC,
"Ignoring malformed pax ctime");
}
return (err);
} else if (key_length == 7 && memcmp(key, "charset", 7) == 0) {
@@ -2819,8 +2873,13 @@ pax_attribute(struct archive_read *a, struct tar *tar, struct archive_entry *ent
break;
case 'm':
if (key_length == 5 && memcmp(key, "mtime", 5) == 0) {
if ((err = pax_attribute_read_time(a, value_length, &t, &n, unconsumed)) == ARCHIVE_OK) {
archive_entry_set_mtime(entry, t, n);
__LA_TIME_T sec;
if ((err = pax_attribute_read_time(a, value_length, &sec, &n, unconsumed)) == ARCHIVE_OK) {
archive_entry_set_mtime(entry, sec, n);
} else {
archive_set_error(&a->archive,
ARCHIVE_ERRNO_MISC,
"Ignoring malformed pax mtime");
}
return (err);
}
@@ -2887,7 +2946,8 @@ pax_attribute(struct archive_read *a, struct tar *tar, struct archive_entry *ent
/*
* Parse a decimal time value, which may include a fractional portion
*
* Sets ps to INT64_MIN on error.
* Sets ps to INT64_MIN on error, including syntax issues such as non-digits,
* or a time value that's outside the range of time_t.
*/
static void
pax_time(const char *p, size_t length, int64_t *ps, long *pn)
@@ -2928,21 +2988,61 @@ pax_time(const char *p, size_t length, int64_t *ps, long *pn)
*ps = s * sign;
#if ARCHIVE_VERSION_NUMBER < 4000000
/* Libarchive 4.0 will have __LA_TIME_T == int64_t, so
this will be unnecessary. */
/* Test whether it overflows __LA_TIME_T */
__LA_TIME_T sec = (__LA_TIME_T)*ps;
if ((int64_t)sec != *ps) {
*ps = INT64_MIN;
*pn = 0;
return;
}
#endif
/* Calculate nanoseconds. */
*pn = 0;
if (length <= 0 || *p != '.')
if (length <= 0) {
return;
}
/* Skip `.` */
if (*p != '.') {
*ps = INT64_MIN;
*pn = 0;
return;
}
++p;
--length;
l = 100000000UL;
do {
if (length <= 0) {
return;
}
if (*p >= '0' && *p <= '9') {
*pn += (*p - '0') * l;
} else {
*ps = INT64_MIN;
*pn = 0;
return;
}
++p;
--length;
if (length > 0 && *p >= '0' && *p <= '9')
*pn += (*p - '0') * l;
else
break;
} while (l /= 10);
/* Ignore resolution beyond nanoseconds,
but verify it's all decimal digits. */
while (length > 0) {
if (*p < '0' || *p > '9') {
*ps = INT64_MIN;
*pn = 0;
return;
}
++p;
--length;
}
}
/*
+2
View File
@@ -188,6 +188,8 @@ IF(ENABLE_TEST)
test_read_format_tar_pax_g_large.c
test_read_format_tar_pax_large_attr.c
test_read_format_tar_pax_negative_time.c
test_read_format_tar_pax_timestamps.c
test_read_format_tar_timestamp_overflow.c
test_read_format_tbz.c
test_read_format_tgz.c
test_read_format_tlz.c
@@ -0,0 +1,64 @@
/*-
* Copyright (c) 2025 Tobias Stoeckmann
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "test.h"
/*
* Read a pax formatted tar archive that has a negative modification time.
*/
DEFINE_TEST(test_read_format_tar_pax_timestamps)
{
char name[] = "test_read_format_tar_pax_timestamps.tar";
struct archive_entry *ae;
struct archive *a;
assert((a = archive_read_new()) != NULL);
assertEqualIntA(a, ARCHIVE_OK, archive_read_support_filter_all(a));
assertEqualIntA(a, ARCHIVE_OK, archive_read_support_format_all(a));
extract_reference_file(name);
assertEqualIntA(a, ARCHIVE_OK, archive_read_open_filename(a, name, 10240));
/* Read first entry. */
assertEqualIntA(a, ARCHIVE_WARN, archive_read_next_header(a, &ae));
assertEqualString("empty", archive_entry_pathname(ae));
assertEqualInt(224165920, archive_entry_mtime(ae));
assertEqualInt(0, archive_entry_mtime_nsec(ae));
assertEqualInt(0, archive_entry_uid(ae));
assertEqualString("root", archive_entry_uname(ae));
assertEqualInt(0, archive_entry_gid(ae));
assertEqualString("root", archive_entry_gname(ae));
assertEqualInt(0100644, archive_entry_mode(ae));
assertEqualInt(archive_entry_is_encrypted(ae), 0);
assertEqualIntA(a, archive_read_has_encrypted_entries(a), ARCHIVE_READ_FORMAT_ENCRYPTION_UNSUPPORTED);
/* Verify the end-of-archive. */
assertEqualIntA(a, ARCHIVE_EOF, archive_read_next_header(a, &ae));
/* Verify that the format detection worked. */
assertEqualInt(archive_filter_code(a, 0), ARCHIVE_FILTER_NONE);
assertEqualInt(archive_format(a), ARCHIVE_FORMAT_TAR_PAX_INTERCHANGE);
assertEqualInt(ARCHIVE_OK, archive_read_close(a));
assertEqualInt(ARCHIVE_OK, archive_read_free(a));
}
@@ -0,0 +1,60 @@
begin 644 test_read_format_tar_pax_timestamps.tar
M4&%X2&5A9&5R+V5M<'1Y````````````````````````````````````````
M````````````````````````````````````````````````````````````
M`````````````#`P,#8T-"``,#`P,#`P(``P,#`P,#`@`#`P,#`P,#`P,#,W
M(#`P,#`P,#`P,#`P(#`Q-#(P-0`@>`````````````````````````!N````
M````````````````````````````````````````````````````````````
M``````````````````````````````````````````!U<W1A<@`P,')O;W0`
M````````````````````````````````````<F]O=```````````````````
M```````````````````P,#`P,#`@`#`P,#`P,"``````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M```````````````````````S,2!M=&EM93TQ-S0X,#@Y-#8T+CDU,3DR.#0V
M-V$*````````````````````````````````````````````````````````
M````````````````````````````````````````````````````;@``````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M`````````````````````````````````````````````&5M<'1Y````````
M````````````````````````````````````````````````````````````
M```````````````````````````````````````````````````````````P
M,#`V-#0@`#`P,#`P,"``,#`P,#`P(``P,#`P,#`P,#`P,""```````````U<
M@"`P,3$U-S4`(#``````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````=7-T87(`,#!R;V]T````````````````
M`````````````````````')O;W0`````````````````````````````````
M````,#`P,#`P(``P,#`P,#`@````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
H````````````````````````````````````````````````````````
`
end
@@ -0,0 +1,55 @@
/*-
* Copyright (c) 2026 Tim Kientzle
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "test.h"
/*
* Read a tar archive that has an oversized timestamp value.
* This should not cause an infinite loop.
*/
DEFINE_TEST(test_read_format_tar_timestamp_overflow)
{
char name[] = "test_read_format_tar_timestamp_overflow.tar";
struct archive_entry *ae;
struct archive *a;
assert((a = archive_read_new()) != NULL);
assertEqualIntA(a, ARCHIVE_OK, archive_read_support_filter_all(a));
assertEqualIntA(a, ARCHIVE_OK, archive_read_support_format_all(a));
extract_reference_file(name);
assertEqualIntA(a, ARCHIVE_OK, archive_read_open_filename(a, name, 10240));
/* Read first entry. */
assertEqualIntA(a, ARCHIVE_OK, archive_read_next_header(a, &ae));
assertEqualString("tmp/f.txt", archive_entry_pathname(ae));
/* 15155542402 should be parsed correctly if time_t is 64-bit,
* or handled gracefully if not. */
(void)archive_entry_mtime(ae);
/* Verify the end-of-archive. */
assertEqualIntA(a, ARCHIVE_EOF, archive_read_next_header(a, &ae));
assertEqualInt(ARCHIVE_OK, archive_read_close(a));
assertEqualInt(ARCHIVE_OK, archive_read_free(a));
}
@@ -0,0 +1,38 @@
begin 600 test_read_format_tar_timestamp_overflow.tar
M=&UP+V8N='AT````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M`````````````#`P,#`V-``````````````````````````````````P,#$V
M`#$U,34U-30R-#`R`#`Q,C8Q-``@,```````````````````````````````
M``````````````````````````````#_````````````````````````````
M``````````````````````````````````````````!U<W1A<B`@`&UA<W1E
M<@``````````````````````````````````;6%S=&5R````````````````
M````````````````````````````````````````````````````````````
M```````````````````````````````````!`````````````````````#0`
M,#`P,3<U,``P,#`Q-S4P`#`P,#`P,#``````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
&````````
`
end