Repository: libarchive/libarchive (2026-04-19T20:23:08Z)

Issue:
Heap Out-of-bounds Read in ISO9660 Virtual Directory Construction (isoent_tree)
#### Description
### Summary
A heap-based out-of-bounds read occurs in the ISO9660 writer during virtual parent directory construction. Specifically, in `isoent_tree()`, the function computes a substring length (`fn - p + l`) that may equal zero, causing an empty `archive_string` to be created. The subsequent code unconditionally evaluates `as.s[as.length - 1]`, which becomes a read from `as.s[-1]`, accessing one byte before the heap buffer. This leads to a deterministic crash when handling attacker-controlled pathnames, resulting in denial-of-service in applications that generate ISO9660 images from untrusted input.

We identified this bug in commit https://github.com/libarchive/libarchive/commit/a819cc28299dca6cf455af074a1a7939efbfb13c. We privately reported this bug when the affected commit was still the latest version. As it has never been publicly disclosed or patched, we believe the vulnerability remains present in the current release.

### Details
The vulnerable code resides in isoent_tree() within archive_write_set_format_iso9660.c:
```c
			archive_string_init(&as);
			archive_strncat(&as, p, fn - p + l);
			if (as.s[as.length-1] == '/') { // here
				as.s[as.length-1] = '\0';
				as. length--;
			}
```
Here:

1. `fn` and `p` are pointers within the parent directory path string.
2. `l` is the length returned by `get_path_component()`.

Under certain malformed pathnames, the previous loop breaks when get_path_component() returns 0, leaving:
```
l == 0
fn == p
fn - p + l == 0
```
This causes:
`archive_strncat(&as, p, 0);`
to produce an empty string (as.length == 0). Immediately afterward, the code unconditionally checks:
`as.s[as.length - 1]`
which evaluates to:
`as.s[-1]`
reading 1 byte before the heap-allocated buffer.

### PoC
libarchive version: commit https://github.com/libarchive/libarchive/commit/a819cc28299dca6cf455af074a1a7939efbfb13c
OS: Ubuntu 22.04.5 LTS
Compiler: clang version 14.0.0-1ubuntu1.1
configuration:
Build libarchive with ASan:
```
CC=clang
CXX=clang++
CFLAGS="-g -O1 -fsanitize=address"
CXXFLAGS="-g -O1 -fsanitize=address"
```
Compile the PoC:
```
clang -g -O1 -fsanitize=address \
    -I /path/to/libarchive \
    poc.c /path/to/libarchive.a \
    -lbz2 -llzma -lz -lxml2 -llz4 -lzstd \
    -o poc
```
Run the PoC:
```
./poc POC_FILE
```
The ASan report is:
```
=================================================================
==3156105==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60300000057f at pc 0x5650c3496174 bp 0x7fff614eff30 sp 0x7fff614eff28
READ of size 1 at 0x60300000057f thread T0
    #0 0x5650c3496173 in isoent_tree /root/libarchive/libarchive/archive_write_set_format_iso9660.c:5634:8
    #1 0x5650c348a975 in iso9660_write_header /root/libarchive/libarchive/archive_write_set_format_iso9660.c:1595:6
    #2 0x5650c347557e in _archive_write_header /root/libarchive/libarchive/archive_write.c:782:8
    #3 0x5650c3473046 in archive_write_header /root/libarchive/libarchive/archive_virtual.c:117:10
    #4 0x5650c34595e5 in main /root/fuzz/libarchive/minimal_poc.c:101:13
    #5 0x7fb428cdcd8f  (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f) (BuildId: 4f7b0c955c3d81d7cac1501a2498b69d1d82bfe7)
    #6 0x7fb428cdce3f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29e3f) (BuildId: 4f7b0c955c3d81d7cac1501a2498b69d1d82bfe7)
    #7 0x5650c339b6f4 in _start (/root/libarchive/build-fix0/poc+0x376f4) (BuildId: a2b4e8ebbbabacb058c7678672e070c2064cfc9d)

0x60300000057f is located 1 bytes to the left of 32-byte region [0x603000000580,0x6030000005a0)
allocated by thread T0 here:
    #0 0x5650c341e966 in __interceptor_realloc (/root/libarchive/build-fix0/poc+0xba966) (BuildId: a2b4e8ebbbabacb058c7678672e070c2064cfc9d)
    #1 0x5650c34614c3 in archive_string_ensure /root/libarchive/libarchive/archive_string.c:338:6

SUMMARY: AddressSanitizer: heap-buffer-overflow /root/libarchive/libarchive/archive_write_set_format_iso9660.c:5634:8 in isoent_tree
Shadow bytes around the buggy address:
  0x0c067fff8050: 00 00 00 00 fa fa 00 00 00 00 fa fa 00 00 00 00
  0x0c067fff8060: fa fa 00 00 00 00 fa fa 00 00 00 00 fa fa 00 00
  0x0c067fff8070: 00 00 fa fa 00 00 00 00 fa fa 00 00 00 00 fa fa
  0x0c067fff8080: 00 00 00 00 fa fa 00 00 00 00 fa fa 00 00 00 00
  0x0c067fff8090: fa fa fd fd fd fa fa fa 00 00 00 fa fa fa fd fd
=>0x0c067fff80a0: fd fa fa fa 00 00 00 00 fa fa 00 00 00 00 fa[fa]
  0x0c067fff80b0: 00 00 00 00 fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff80c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff80d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff80e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff80f0: 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
==3156105==ABORTING
```
poc.c:
```c
#include <archive.h>
#include <archive_entry.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static la_ssize_t write_callback(struct archive *a, void *client_data, 
                                  const void *buffer, size_t length) {
    return length;
}

int main(int argc, char **argv) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <crash_file>\n", argv[0]);
        return 1;
    }
    
    FILE *f = fopen(argv[1], "rb");
    if (!f) {
        perror("fopen");
        return 1;
    }
    
    fseek(f, 0, SEEK_END);
    long len = ftell(f);
    fseek(f, 0, SEEK_SET);
    
    unsigned char *buf = malloc(len);
    if (!buf) {
        fclose(f);
        return 1;
    }
    
    fread(buf, 1, len, f);
    fclose(f);
    
    struct archive *a = archive_write_new();
    if (!a) {
        free(buf);
        return 1;
    }
    
    int format = buf[0] % 5;
    switch (format) {
        case 0: archive_write_set_format_zip(a); break;
        case 1: archive_write_set_format_pax_restricted(a); break;
        case 2: archive_write_set_format_7zip(a); break;
        case 3: archive_write_set_format_gnutar(a); break;
        case 4: archive_write_set_format_iso9660(a); break;
    }
    
    int compression = buf[1] % 4;
    switch (compression) {
        case 0: archive_write_add_filter_gzip(a); break;
        case 1: archive_write_add_filter_bzip2(a); break;
        case 2: archive_write_add_filter_xz(a); break;
        case 3: break;
    }
    
    archive_write_open(a, NULL, NULL, write_callback, NULL);
    
    const uint8_t *ptr = buf + 2;
    size_t remaining = len - 2;
    
    while (remaining > 10) {
        struct archive_entry *entry = archive_entry_new();
        
        size_t name_len = ptr[0] % 100 + 1;
        if (name_len > remaining - 1) {
            name_len = remaining - 1;
        }
        
        char *pathname = malloc(name_len + 1);
        if (!pathname) {
            archive_entry_free(entry);
            break;
        }
        memcpy(pathname, ptr + 1, name_len);
        pathname[name_len] = '\0';
        
        archive_entry_set_pathname(entry, pathname);
        archive_entry_set_filetype(entry, AE_IFREG);
        archive_entry_set_perm(entry, 0644);
        
        ptr += name_len + 1;
        remaining -= name_len + 1;
        
        if (remaining < 2) {
            free(pathname);
            archive_entry_free(entry);
            break;
        }
        
        size_t content_len = (ptr[0] | (ptr[1] << 8)) % 1024;
        if (content_len > remaining - 2) {
            content_len = remaining - 2;
        }
        
        archive_entry_set_size(entry, content_len);
        
        if (archive_write_header(a, entry) == ARCHIVE_OK) {
            ptr += 2;
            remaining -= 2;
            
            if (content_len > 0 && content_len <= remaining) {
                archive_write_data(a, ptr, content_len);
                ptr += content_len;
                remaining -= content_len;
            }
        }
        
        free(pathname);
        archive_entry_free(entry);
        
        if (remaining < 10) break;
    }
    
    archive_write_close(a);
    archive_write_free(a);
    free(buf);
    
    printf("Test completed successfully\n");
    return 0;
}
```
[POC_FILE](https://github.com/TimChan2001/pocs/raw/refs/heads/main/poc-libarchive-oobr-1)


Discussion:
The PoC basically resolves to an entry of the type `dir0/..//dir0/dir1/file2` as added to test suite in https://github.com/libarchive/libarchive/pull/2968. Having a dot dot entry which points to the top directory with multiple slashes adjacent to it (`..//`) erroneously leads to an absolute path, which the path normalization is supposed to avoid/resolve.

Fixing the root cause avoids empty strings later on which would trigger this presented OOB read access.



Patch:
diff --git a/libarchive/archive_write_set_format_iso9660.c b/libarchive/archive_write_set_format_iso9660.c
index 359a27373..1e293588b 100644
--- a/libarchive/archive_write_set_format_iso9660.c
+++ b/libarchive/archive_write_set_format_iso9660.c
@@ -4910,20 +4910,29 @@ isofile_gen_utility_names(struct archive_write *a, struct isofile *file)
 				 *     --> 'dir/dir2/'
 				 */
 				char *rp = p -1;
+				size_t off;
+				for (off = 4; p[off] == '/'; off++)
+					;
 				while (rp >= dirname) {
 					if (*rp == '/')
 						break;
 					--rp;
 				}
 				if (rp > dirname) {
-					strcpy(rp, p+3);
+					memmove(rp + 1, p + off, strlen(p + off) + 1);
 					p = rp;
 				} else {
-					strcpy(dirname, p+4);
+					memmove(dirname, p + off, strlen(p + off) + 1);
 					p = dirname;
 				}
 			} else
 				p++;
+		} else if (p == dirname && p[0] == '.' && p[1] == '.' && p[2] == '/') {
+			size_t off;
+			for (off = 3; p[off] == '/'; off++)
+				;
+			memmove(dirname, p + off, strlen(p + off) + 1);
+			p = dirname;
 		} else
 			p++;
 	}


Test patch:
diff --git a/libarchive/test/test_write_format_iso9660.c b/libarchive/test/test_write_format_iso9660.c
index 7b455794c..7df5a2605 100644
--- a/libarchive/test/test_write_format_iso9660.c
+++ b/libarchive/test/test_write_format_iso9660.c
@@ -154,7 +154,7 @@ DEFINE_TEST(test_write_format_iso9660)
 	archive_entry_set_birthtime(ae, 3, 30);
 	archive_entry_set_ctime(ae, 4, 40);
 	archive_entry_set_mtime(ae, 5, 50);
-	archive_entry_copy_pathname(ae, "dir0/dir1/file1");
+	archive_entry_copy_pathname(ae, "dir0/../../dir0/dir1/file1");
 	archive_entry_set_mode(ae, AE_IFREG | 0755);
 	archive_entry_set_size(ae, 8);
 	archive_entry_set_nlink(ae, 1);
@@ -170,7 +170,7 @@ DEFINE_TEST(test_write_format_iso9660)
 	archive_entry_set_birthtime(ae, 3, 30);
 	archive_entry_set_ctime(ae, 4, 40);
 	archive_entry_set_mtime(ae, 5, 50);
-	archive_entry_copy_pathname(ae, "dir0/dir1/file2");
+	archive_entry_copy_pathname(ae, "dir0/..//dir0/dir1/file2");
 	archive_entry_set_mode(ae, AE_IFREG | 0755);
 	archive_entry_set_size(ae, 8);
 	archive_entry_set_nlink(ae, 1);