From: Tim Kientzle Date: Sat, 2 May 2026 20:58:45 +0000 (-0700) Subject: [tar] Harden timestamp parsing X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d1d37fcbd7f34cb2171a1d60a545142e0e3028ff;p=thirdparty%2Flibarchive.git [tar] Harden timestamp parsing Improves the parsing of timestamps in a couple of ways: * Saturate when timestamps exceed the range of time_t. In particular, this provides more rational behavior on systems with 32-bit time_t. * Validate the format of overlong pax timestamps. We previously failed to check that high-resolution timestamps had only digits in the fractional part. We now notice and ignore those with a warning. --- diff --git a/Makefile.am b/Makefile.am index b19e6837c..30272333b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 \ @@ -1019,6 +1021,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 \ diff --git a/libarchive/archive_read_support_format_tar.c b/libarchive/archive_read_support_format_tar.c index 98800ec31..ac5e61977 100644 --- a/libarchive/archive_read_support_format_tar.c +++ b/libarchive/archive_read_support_format_tar.c @@ -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 int64_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; + } } /* diff --git a/libarchive/test/CMakeLists.txt b/libarchive/test/CMakeLists.txt index 2d2ff013f..16031faef 100644 --- a/libarchive/test/CMakeLists.txt +++ b/libarchive/test/CMakeLists.txt @@ -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 diff --git a/libarchive/test/test_read_format_tar_pax_timestamps.c b/libarchive/test/test_read_format_tar_pax_timestamps.c new file mode 100644 index 000000000..7850cbee8 --- /dev/null +++ b/libarchive/test/test_read_format_tar_pax_timestamps.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)); +} diff --git a/libarchive/test/test_read_format_tar_pax_timestamps.tar.uu b/libarchive/test/test_read_format_tar_pax_timestamps.tar.uu new file mode 100644 index 000000000..089705c23 --- /dev/null +++ b/libarchive/test/test_read_format_tar_pax_timestamps.tar.uu @@ -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