Project: libarchive (libarchive/libarchive), language: c++
Vulnerability instance: libarchive.cve-2024-57970
Sanitizer: address

================= Bug Report (1/1) ==================
## Source: GitHub Issue
## URL: https://github.com/libarchive/libarchive/issues/2415
## Description:
Issue: libarchive/libarchive#2415
Title: bsdtar AddressSanitizer: heap-buffer-overflow
State: closed
Created by: gbdngb12
Created at: 2024-11-24 08:54:48+00:00

Issue Body:
```bash
$ git rev-parse HEAD
e37d3781862573b79a76fec9dc374417e50a33c4
```
Add address Sanitizer `CMakeLists.txt`,`tar/CMakeLists.txt` 
```txt
# relaxed somewhat in final shipping versions.
IF (CMAKE_C_COMPILER_ID MATCHES "^GNU$" OR
    CMAKE_C_COMPILER_ID MATCHES "^Clang$" AND NOT MSVC)
  SET(CMAKE_REQUIRED_FLAGS "-Wall -Wformat -Wformat-security -fsanitize=address")
  ...
```

```txt
...
  # Add sanitizer option
  TARGET_COMPILE_OPTIONS(bsdtar PRIVATE -fsanitize=address)
  TARGET_LINK_OPTIONS(bsdtar PRIVATE -fsanitize=address)
...
```

# Build
```
$ cmake -S . -B linux_build
$ cd linux_build
$ make -j `nproc`
```

# extract tar using bsdtar
```bash
$ ./bin/bsdtar -xvf ./heap_of.tar 
=================================================================
==25253==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x61a000000b13 at pc 0x7eb518cd5aa7 bp 0x7fffe21a4150 sp 0x7fffe21a38f8
READ of size 1172 at 0x61a000000b13 thread T0
    #0 0x7eb518cd5aa6 in __interceptor_strlen ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:389
    #1 0x56e474ef680a in archive_mstring_copy_mbs /root/libarchive/libarchive/archive_string.c:4096
    #2 0x56e474e9f33e in archive_entry_set_link /root/libarchive/libarchive/archive_entry.c:1172
    #3 0x56e474ee0bff in header_gnu_longlink /root/libarchive/libarchive/archive_read_support_format_tar.c:1149
    #4 0x56e474ee0167 in tar_read_header /root/libarchive/libarchive/archive_read_support_format_tar.c:829
    #5 0x56e474edf7e8 in archive_read_format_tar_read_header /root/libarchive/libarchive/archive_read_support_format_tar.c:546
    #6 0x56e474ea6aff in _archive_read_next_header2 /root/libarchive/libarchive/archive_read.c:646
    #7 0x56e474ea6bed in _archive_read_next_header /root/libarchive/libarchive/archive_read.c:684
    #8 0x56e474ef7e8c in archive_read_next_header /root/libarchive/libarchive/archive_virtual.c:148
    #9 0x56e474e8ef4a in read_archive /root/libarchive/tar/read.c:241
    #10 0x56e474e8e100 in tar_mode_x /root/libarchive/tar/read.c:93
    #11 0x56e474e8b341 in main /root/libarchive/tar/bsdtar.c:993
    #12 0x7eb518a98d8f  (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f)
    #13 0x7eb518a98e3f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29e3f)
    #14 0x56e474e85764 in _start (/root/libarchive/linux_build/bin/bsdtar+0x10764)

0x61a000000b13 is located 0 bytes to the right of 1171-byte region [0x61a000000680,0x61a000000b13)
allocated by thread T0 here:
    #0 0x7eb518d4cc38 in __interceptor_realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:164
    #1 0x56e474eeee94 in archive_string_ensure /root/libarchive/libarchive/archive_string.c:316
    #2 0x56e474ee0e41 in read_bytes_to_string /root/libarchive/libarchive/archive_read_support_format_tar.c:1225
    #3 0x56e474ee1016 in read_body_to_string /root/libarchive/libarchive/archive_read_support_format_tar.c:1277
    #4 0x56e474ee0be9 in header_gnu_longlink /root/libarchive/libarchive/archive_read_support_format_tar.c:1148
    #5 0x56e474ee0167 in tar_read_header /root/libarchive/libarchive/archive_read_support_format_tar.c:829
    #6 0x56e474edf7e8 in archive_read_format_tar_read_header /root/libarchive/libarchive/archive_read_support_format_tar.c:546
    #7 0x56e474ea6aff in _archive_read_next_header2 /root/libarchive/libarchive/archive_read.c:646
    #8 0x56e474ea6bed in _archive_read_next_header /root/libarchive/libarchive/archive_read.c:684
    #9 0x56e474ef7e8c in archive_read_next_header /root/libarchive/libarchive/archive_virtual.c:148
    #10 0x56e474e8ef4a in read_archive /root/libarchive/tar/read.c:241
    #11 0x56e474e8e100 in tar_mode_x /root/libarchive/tar/read.c:93
    #12 0x56e474e8b341 in main /root/libarchive/tar/bsdtar.c:993
    #13 0x7eb518a98d8f  (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f)

SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:389 in __interceptor_strlen
Shadow bytes around the buggy address:
  0x0c347fff8110: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c347fff8120: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c347fff8130: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c347fff8140: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c347fff8150: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c347fff8160: 00 00[03]fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c347fff8170: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c347fff8180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c347fff8190: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c347fff81a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c347fff81b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==25253==ABORTING

```
[heap_of.zip](https://github.com/user-attachments/files/17892588/heap_of.zip)




Comments:
Comment by kientzle on 2024-11-24 17:48:03+00:00:
Thank you!  It looks like `header_gnu_longlink` is missing an error check.  Please try this patch and let me know if it fixes this for you:
```
diff --git a/libarchive/archive_read_support_format_tar.c b/libarchive/archive_read_support_format_tar.c
index 14c15734..1fc4764c 100644
--- a/libarchive/archive_read_support_format_tar.c
+++ b/libarchive/archive_read_support_format_tar.c
@@ -1146,6 +1146,9 @@ header_gnu_longlink(struct archive_read *a, struct tar *tar,
        struct archive_string linkpath;
        archive_string_init(&linkpath);
        err = read_body_to_string(a, tar, &linkpath, h, unconsumed);
+       if (err != ARCHIVE_OK) {
+         return (ARCHIVE_FATAL);
+       }
        archive_entry_set_link(entry, linkpath.s);
        archive_string_free(&linkpath);
        return (err);
```

---

Comment by gbdngb12 on 2024-12-02 09:50:17+00:00:
```bash
$ grep -A 20 -B 5 'header_gnu_longlink' ./libarchive/archive_read_support_format_tar.c

                    struct archive_entry *, const void *);
static int      header_pax_extension(struct archive_read *, struct tar *,
                    struct archive_entry *, const void *, size_t *);
static int      header_pax_global(struct archive_read *, struct tar *,
                    struct archive_entry *, const void *h, size_t *);
static int      header_gnu_longlink(struct archive_read *, struct tar *,
                    struct archive_entry *, const void *h, size_t *);
static int      header_gnu_longname(struct archive_read *, struct tar *,
                    struct archive_entry *, const void *h, size_t *);
static int      is_mac_metadata_entry(struct archive_entry *entry);
static int      read_mac_metadata_blob(struct archive_read *,
                    struct archive_entry *, size_t *);
static int      header_volume(struct archive_read *, struct tar *,
                    struct archive_entry *, const void *h, size_t *);
static int      header_ustar(struct archive_read *, struct tar *,
                    struct archive_entry *, const void *h);
static int      header_gnutar(struct archive_read *, struct tar *,
                    struct archive_entry *, const void *h, size_t *);
static int      archive_read_format_tar_bid(struct archive_read *, int);
static int      archive_read_format_tar_options(struct archive_read *,
                    const char *, const char *);
static int      archive_read_format_tar_cleanup(struct archive_read *);
static int      archive_read_format_tar_read_data(struct archive_read *a,
                    const void **buff, size_t *size, int64_t *offset);
static int      archive_read_format_tar_skip(struct archive_read *a);
static int      archive_read_format_tar_read_header(struct archive_read *,
--
                case 'K': /* Long link name (GNU tar, others) */
                        if (seen_headers & seen_K_header) {
                                return (ARCHIVE_FATAL);
                        }
                        seen_headers |= seen_K_header;
                        err2 = header_gnu_longlink(a, tar, entry, h, unconsumed);
                        break;
                case 'L': /* Long filename (GNU tar, others) */
                        if (seen_headers & seen_L_header) {
                                return (ARCHIVE_FATAL);
                        }
                        seen_headers |= seen_L_header;
                        err2 = header_gnu_longname(a, tar, entry, h, unconsumed);
                        break;
                case 'V': /* GNU volume header */
                        if (seen_headers & seen_V_header) {
                                return (ARCHIVE_FATAL);
                        }
                        seen_headers |= seen_V_header;
                        err2 = header_volume(a, tar, entry, h, unconsumed);
                        break;
                case 'X': /* Used by SUN tar; same as 'x'. */
                        if (seen_headers & seen_x_header) {
                                return (ARCHIVE_FATAL);
                        }
                        seen_headers |= seen_x_header;
--

/*
 * Interpret 'K' long linkname header.
 */
static int
header_gnu_longlink(struct archive_read *a, struct tar *tar,
    struct archive_entry *entry, const void *h, size_t *unconsumed)
{
        int err;

        struct archive_string linkpath;
        archive_string_init(&linkpath);
        err = read_body_to_string(a, tar, &linkpath, h, unconsumed);
        if (err != ARCHIVE_OK) {
                return (ARCHIVE_FATAL);
        }
        archive_entry_set_link(entry, linkpath.s);
        archive_string_free(&linkpath);
        return (err);
}

```


```bash
$ make -j `nproc` 
$ ./bin/bsdtar -xvf ./heap_of.tar
bsdtar: Truncated archive detected while reading metadata
bsdtar: Error exit delayed from previous errors.

=================================================================
==2817==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 1171 byte(s) in 1 object(s) allocated from:
    #0 0x7cf83590fc38 in __interceptor_realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:164
    #1 0x5d288d0a9ea1 in archive_string_ensure /root/libarchive/libarchive/archive_string.c:316
    #2 0x5d288d09be4e in read_bytes_to_string /root/libarchive/libarchive/archive_read_support_format_tar.c:1228
    #3 0x5d288d09c023 in read_body_to_string /root/libarchive/libarchive/archive_read_support_format_tar.c:1280
    #4 0x5d288d09bbe9 in header_gnu_longlink /root/libarchive/libarchive/archive_read_support_format_tar.c:1148
    #5 0x5d288d09b167 in tar_read_header /root/libarchive/libarchive/archive_read_support_format_tar.c:829
    #6 0x5d288d09a7e8 in archive_read_format_tar_read_header /root/libarchive/libarchive/archive_read_support_format_tar.c:546
    #7 0x5d288d061aff in _archive_read_next_header2 /root/libarchive/libarchive/archive_read.c:646
    #8 0x5d288d061bed in _archive_read_next_header /root/libarchive/libarchive/archive_read.c:684
    #9 0x5d288d0b2e99 in archive_read_next_header /root/libarchive/libarchive/archive_virtual.c:148
    #10 0x5d288d049f4a in read_archive /root/libarchive/tar/read.c:241
    #11 0x5d288d049100 in tar_mode_x /root/libarchive/tar/read.c:93
    #12 0x5d288d046341 in main /root/libarchive/tar/bsdtar.c:993
    #13 0x7cf83565bd8f  (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f)

SUMMARY: AddressSanitizer: 1171 byte(s) leaked in 1 allocation(s).
```
I applied the patch and tested it, but... could this be a sanitizer issue?

---

Comment by kientzle on 2024-12-03 22:07:26+00:00:
Ouch.  I forgot to release the string in the error path.  Please try this version:

```
diff --git a/libarchive/archive_read_support_format_tar.c b/libarchive/archive_read_support_format_tar.c
index 14c15734..1fc4764c 100644
--- a/libarchive/archive_read_support_format_tar.c
+++ b/libarchive/archive_read_support_format_tar.c
@@ -1146,6 +1146,9 @@ header_gnu_longlink(struct archive_read *a, struct tar *tar,
        struct archive_string linkpath;
        archive_string_init(&linkpath);
        err = read_body_to_string(a, tar, &linkpath, h, unconsumed);
+       if (err != ARCHIVE_OK) {
+         archive_string_free(&linkpath);
+         return (ARCHIVE_FATAL);
+       }
        archive_entry_set_link(entry, linkpath.s);
        archive_string_free(&linkpath);
        return (err);
```


---

Comment by gbdngb12 on 2024-12-03 23:15:09+00:00:
```bash
$ grep -A 20 -B 5 'header_gnu_longlink' ./libarchive/archive_read_support_format_tar.c
                    struct archive_entry *, const void *);
static int      header_pax_extension(struct archive_read *, struct tar *,
                    struct archive_entry *, const void *, size_t *);
static int      header_pax_global(struct archive_read *, struct tar *,
                    struct archive_entry *, const void *h, size_t *);
static int      header_gnu_longlink(struct archive_read *, struct tar *,
                    struct archive_entry *, const void *h, size_t *);
static int      header_gnu_longname(struct archive_read *, struct tar *,
                    struct archive_entry *, const void *h, size_t *);
static int      is_mac_metadata_entry(struct archive_entry *entry);
static int      read_mac_metadata_blob(struct archive_read *,
                    struct archive_entry *, size_t *);
static int      header_volume(struct archive_read *, struct tar *,
                    struct archive_entry *, const void *h, size_t *);
static int      header_ustar(struct archive_read *, struct tar *,
                    struct archive_entry *, const void *h);
static int      header_gnutar(struct archive_read *, struct tar *,
                    struct archive_entry *, const void *h, size_t *);
static int      archive_read_format_tar_bid(struct archive_read *, int);
static int      archive_read_format_tar_options(struct archive_read *,
                    const char *, const char *);
static int      archive_read_format_tar_cleanup(struct archive_read *);
static int      archive_read_format_tar_read_data(struct archive_read *a,
                    const void **buff, size_t *size, int64_t *offset);
static int      archive_read_format_tar_skip(struct archive_read *a);
static int      archive_read_format_tar_read_header(struct archive_read *,
--
                case 'K': /* Long link name (GNU tar, others) */
                        if (seen_headers & seen_K_header) {
                                return (ARCHIVE_FATAL);
                        }
                        seen_headers |= seen_K_header;
                        err2 = header_gnu_longlink(a, tar, entry, h, unconsumed);
                        break;
                case 'L': /* Long filename (GNU tar, others) */
                        if (seen_headers & seen_L_header) {
                                return (ARCHIVE_FATAL);
                        }
                        seen_headers |= seen_L_header;
                        err2 = header_gnu_longname(a, tar, entry, h, unconsumed);
                        break;
                case 'V': /* GNU volume header */
                        if (seen_headers & seen_V_header) {
                                return (ARCHIVE_FATAL);
                        }
                        seen_headers |= seen_V_header;
                        err2 = header_volume(a, tar, entry, h, unconsumed);
                        break;
                case 'X': /* Used by SUN tar; same as 'x'. */
                        if (seen_headers & seen_x_header) {
                                return (ARCHIVE_FATAL);
                        }
                        seen_headers |= seen_x_header;
--

/*
 * Interpret 'K' long linkname header.
 */
static int
header_gnu_longlink(struct archive_read *a, struct tar *tar,
    struct archive_entry *entry, const void *h, size_t *unconsumed)
{
        int err;

        struct archive_string linkpath;
        archive_string_init(&linkpath);
        err = read_body_to_string(a, tar, &linkpath, h, unconsumed);
        if (err != ARCHIVE_OK) {
                archive_string_free(&linkpath);
                return (ARCHIVE_FATAL);
        }
        archive_entry_set_link(entry, linkpath.s);
        archive_string_free(&linkpath);
        return (err);
}

```

```bash
$ make -j `nproc`
$ ./bin/bsdtar -xvf ./heap_of.tar
bsdtar: Truncated archive detected while reading metadata
bsdtar: Error exit delayed from previous errors.
```
It seems to be working well.


---

Comment by kientzle on 2024-12-04 18:44:05+00:00:
PR #2422 has a slightly simpler version of this fix.  Thank you so much for reporting this and helping to test the fix!

---

Comment by kientzle on 2024-12-10 05:10:53+00:00:
Fix merged.  Please let us know if you find any other problems.

---

Comment by theta682 on 2025-02-20 22:31:23+00:00:
https://nvd.nist.gov/vuln/detail/CVE-2024-57970 has been assigned to this issue. Can we have an official release with the fix?

---

Comment by ljavorsk on 2025-02-25 14:24:42+00:00:
Hi, I'm fixing this in Fedora/CentOS Stream distributions, and we have a version of `3.5.3` in CentOS Stream 9.

From the codebase [1], I believe this issue was never present as there are the `if` checks for the `err`. Can anyone please verify my statement?

[1] https://github.com/libarchive/libarchive/blob/v3.5.3/libarchive/archive_read_support_format_tar.c#L1063

PS: When I tried to reproduce this bug in the released version `3.7.7` that doesn't include the fix introduced by #2422 , the `bsdtar` failed with the exact same output as the version built from `master` where this fix is already included.
Not sure how to interpret this.

---

Comment by samueloph on 2025-02-26 14:10:43+00:00:
@ljavorsk FWIW [Debian](https://security-tracker.debian.org/tracker/CVE-2024-57970) pointed https://github.com/libarchive/libarchive/commit/2d8a5760c5ec553283a95a1aaca746f6eb472d0f#diff-dc0935b631736ce1d3ad08da0faa4b9466d60027ea2ce0a6cd38ac7965a66eb6R1149 (v3.7.5) as the commit introducing the vulnerability. 

---

Comment by ljavorsk on 2025-02-26 14:18:58+00:00:
@samueloph I agree with this commit being the one that introduced it, which makes any older version than 3.7.5 not affected. However, the NVD states [1] that every version same or lower than 3.7.7 (by the use of words `libarchive through 3.7.7...`) is affected which makes it a bit chaotic.

[1] https://nvd.nist.gov/vuln/detail/CVE-2024-57970

Attachments:
https://github.com/user-attachments/files/17892588/heap_of.zip

Commit References:
dc0935b631736ce1d3ad08da0faa4b9466d60027
e37d3781862573b79a76fec9dc374417e50a33c4
2d8a5760c5ec553283a95a1aaca746f6eb472d0f

Build script:
#!/bin/bash -eu
# Minimized build script with only core build commands
set -eu
cd $SRC/libarchive
mkdir -p build2
cd build2
cmake -DDONT_FAIL_ON_CRC_ERROR=ON -DENABLE_WERROR=OFF ../
make -j$(nproc)
mkdir -p ./uudecoded

Dockerfile:
FROM hwiwonlee/secb.base:latest
RUN apt-get update && apt-get install -y make autoconf libtool pkg-config libbz2-dev liblzo2-dev liblzma-dev liblz4-dev libz-dev libssl-dev libacl1-dev libattr1-dev lrzip liblzo2-dev liblz4-tool lzop zstd lcab genisoimage jlha-utils rar default-jdk sharutils
RUN git clone https://github.com/libarchive/libarchive libarchive
RUN git -C libarchive checkout b439d586f53911c84be5e380445a8a259e19114c
WORKDIR $SRC/libarchive
COPY build.sh $SRC/

Additional files:
filename: libarchive_fuzzer.cc
content: // Copyright 2016 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
#include <stddef.h>
#include <stdint.h>
#include <vector>

#include "archive.h"
#include "archive_entry.h"

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len) {
  struct archive *a = archive_read_new();

  archive_read_support_filter_all(a);
  archive_read_support_format_all(a);
  archive_read_support_format_empty(a);
  archive_read_support_format_raw(a);
  archive_read_support_format_gnutar(a);

  if (ARCHIVE_OK != archive_read_set_options(a, "zip:ignorecrc32,tar:read_concatenated_archives,tar:mac-ext")) {
    return 0;
  }

  archive_read_add_passphrase(a, "secret");

  if (ARCHIVE_OK != archive_read_open_memory(a, buf, len)) {
    archive_read_free(a);
    return 0;
  }

  while(1) {
    std::vector<uint8_t> data_buffer(getpagesize(), 0);
    struct archive_entry *entry;
    int ret = archive_read_next_header(a, &entry);
    if (ret == ARCHIVE_EOF || ret == ARCHIVE_FATAL)
      break;
    if (ret == ARCHIVE_RETRY)
      continue;

    (void)archive_entry_pathname(entry);
    (void)archive_entry_pathname_utf8(entry);
    (void)archive_entry_pathname_w(entry);

    (void)archive_entry_atime(entry);
    (void)archive_entry_birthtime(entry);
    (void)archive_entry_ctime(entry);
    (void)archive_entry_dev(entry);
    (void)archive_entry_digest(entry, ARCHIVE_ENTRY_DIGEST_SHA1);
    (void)archive_entry_filetype(entry);
    (void)archive_entry_gid(entry);
    (void)archive_entry_is_data_encrypted(entry);
    (void)archive_entry_is_encrypted(entry);
    (void)archive_entry_is_metadata_encrypted(entry);
    (void)archive_entry_mode(entry);
    (void)archive_entry_mtime(entry);
    (void)archive_entry_size(entry);
    (void)archive_entry_uid(entry);

    ssize_t r;
    while ((r = archive_read_data(a, data_buffer.data(),
            data_buffer.size())) > 0)
      ;
    if (r == ARCHIVE_FATAL)
      break;
  }

  archive_read_has_encrypted_entries(a);
  archive_read_format_capabilities(a);
  archive_file_count(a);
  archive_seek_data(a, 0, SEEK_SET);

  archive_read_free(a);
  return 0;
}