From 48fc01808050795009e09d83ac4445b06230ae07 Mon Sep 17 00:00:00 2001 From: Darren Carreras <283775510+carrerasdarren-cell@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:50:29 -0400 Subject: [PATCH 1/2] cpio: test synthetic newc inode values Exercise large inode values that collide after 32-bit truncation, hardlink identity, and distinct device IDs. Update bsdcpio expectations so successful synthesis produces no truncation warnings. --- cpio/test/test_format_newc.c | 40 +----------- libarchive/test/test_write_format_cpio_newc.c | 63 +++++++++++++++++++ 2 files changed, 65 insertions(+), 38 deletions(-) diff --git a/cpio/test/test_format_newc.c b/cpio/test/test_format_newc.c index 7363fc14..54e04225 100644 --- a/cpio/test/test_format_newc.c +++ b/cpio/test/test_format_newc.c @@ -113,42 +113,6 @@ DEFINE_TEST(test_format_newc) /* Setup result message. */ memset(result, 0, sizeof(result)); - if (is_LargeInode("file1")) { - strncat(result, - "bsdcpio: file1: large inode number truncated: ", - sizeof(result) - strlen(result) -1); - strncat(result, strerror(ERANGE), - sizeof(result) - strlen(result) -1); - strncat(result, "\n", - sizeof(result) - strlen(result) -1); - } - if (canSymlink() && is_LargeInode("symlink")) { - strncat(result, - "bsdcpio: symlink: large inode number truncated: ", - sizeof(result) - strlen(result) -1); - strncat(result, strerror(ERANGE), - sizeof(result) - strlen(result) -1); - strncat(result, "\n", - sizeof(result) - strlen(result) -1); - } - if (is_LargeInode("dir")) { - strncat(result, - "bsdcpio: dir: large inode number truncated: ", - sizeof(result) - strlen(result) -1); - strncat(result, strerror(ERANGE), - sizeof(result) - strlen(result) -1); - strncat(result, "\n", - sizeof(result) - strlen(result) -1); - } - if (is_LargeInode("hardlink")) { - strncat(result, - "bsdcpio: hardlink: large inode number truncated: ", - sizeof(result) - strlen(result) -1); - strncat(result, strerror(ERANGE), - sizeof(result) - strlen(result) -1); - strncat(result, "\n", - sizeof(result) - strlen(result) -1); - } /* Record some facts about what we just created: */ now = time(NULL); /* They were all created w/in last two seconds. */ @@ -225,7 +189,7 @@ DEFINE_TEST(test_format_newc) /* "symlink" pointing to "file1" */ assert(is_hex(e, 110)); assertEqualMem(e + 0, "070701", 6); /* Magic */ - assert(is_hex(e + 6, 8)); /* ino */ + assert(ino != from_hex(e + 6, 8)); /* ino */ #if defined(_WIN32) && !defined(__CYGWIN__) /* Mode: Group members bits and others bits do not work. */ assertEqualInt(0xa180, from_hex(e + 14, 8) & 0xffc0); @@ -258,7 +222,7 @@ DEFINE_TEST(test_format_newc) /* "dir" */ assert(is_hex(e, 110)); assertEqualMem(e + 0, "070701", 6); /* Magic */ - assert(is_hex(e + 6, 8)); /* ino */ + assert(ino != from_hex(e + 6, 8)); /* ino */ #if defined(_WIN32) && !defined(__CYGWIN__) /* Group members bits and others bits do not work. */ assertEqualInt(0x41c0, from_hex(e + 14, 8) & 0xffc0); /* Mode */ diff --git a/libarchive/test/test_write_format_cpio_newc.c b/libarchive/test/test_write_format_cpio_newc.c index 786b5d73..5f72b308 100644 --- a/libarchive/test/test_write_format_cpio_newc.c +++ b/libarchive/test/test_write_format_cpio_newc.c @@ -42,6 +42,24 @@ is_hex(const char *p, size_t l) return (1); } +static void +write_newc_entry(struct archive *a, const char *path, int64_t dev, int64_t ino, + int nlink) +{ + struct archive_entry *entry; + + assert((entry = archive_entry_new()) != NULL); + archive_entry_set_pathname(entry, path); + archive_entry_set_mode(entry, AE_IFREG | 0644); + archive_entry_set_size(entry, 0); + archive_entry_set_dev(entry, dev); + archive_entry_set_ino64(entry, ino); + archive_entry_set_nlink(entry, nlink); + assertEqualIntA(a, ARCHIVE_OK, archive_write_header(a, entry)); + assertEqualIntA(a, ARCHIVE_OK, archive_write_finish_entry(a)); + archive_entry_free(entry); +} + /* * Detailed verification that cpio 'newc' archives are written with * the correct format. @@ -205,3 +223,48 @@ DEFINE_TEST(test_write_format_cpio_newc) free(buff); } + +DEFINE_TEST(test_write_format_cpio_newc_large_inode) +{ + struct archive *a; + char *buff; + const char *first, *again, *other, *small_inode, *different_device; + size_t buffsize = 10000; + size_t used; + + buff = malloc(buffsize); + assert(buff != NULL); + assert((a = archive_write_new()) != NULL); + assertEqualIntA(a, ARCHIVE_OK, archive_write_set_format_cpio_newc(a)); + assertEqualIntA(a, ARCHIVE_OK, archive_write_add_filter_none(a)); + assertEqualIntA(a, ARCHIVE_OK, + archive_write_open_memory(a, buff, buffsize, &used)); + + write_newc_entry(a, "first", 12, INT64_C(0x100000001), 2); + write_newc_entry(a, "again", 12, INT64_C(0x100000001), 2); + write_newc_entry(a, "other", 12, INT64_C(0x200000001), 2); + write_newc_entry(a, "small", 12, 1, 1); + write_newc_entry(a, "dev13", 13, INT64_C(0x100000001), 2); + assertEqualIntA(a, ARCHIVE_OK, archive_write_close(a)); + assertEqualInt(ARCHIVE_OK, archive_write_free(a)); + + /* Each entry is a 110-byte header plus a padded six-byte name. */ + first = buff + 6; + again = buff + 116 + 6; + other = buff + 232 + 6; + small_inode = buff + 348 + 6; + different_device = buff + 464 + 6; + assertEqualMem(first, again, 8); + assert(memcmp(first, other, 8) != 0); + assert(memcmp(first, small_inode, 8) != 0); + assert(memcmp(first, different_device, 8) != 0); + assert(memcmp(other, small_inode, 8) != 0); + assert(memcmp(other, different_device, 8) != 0); + assert(memcmp(small_inode, different_device, 8) != 0); + assert(memcmp(first, "00000000", 8) != 0); + assert(memcmp(other, "00000000", 8) != 0); + assert(memcmp(small_inode, "00000000", 8) != 0); + assert(memcmp(different_device, "00000000", 8) != 0); + + free(buff); +} From 1803922e495a5dc92935fe150879406aab6b7a74 Mon Sep 17 00:00:00 2001 From: Darren Carreras <283775510+carrerasdarren-cell@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:50:29 -0400 Subject: [PATCH 2/2] cpio: synthesize newc inode values The newc format stores only 32-bit inode values. Assign unique synthetic values from each full device and inode identity. This avoids collisions after truncation while preserving hardlink groups. Fixes #3314. --- .../archive_write_set_format_cpio_newc.c | 104 +++++++++++++++++- 1 file changed, 98 insertions(+), 6 deletions(-) diff --git a/libarchive/archive_write_set_format_cpio_newc.c b/libarchive/archive_write_set_format_cpio_newc.c index b9de7d36..710a6865 100644 --- a/libarchive/archive_write_set_format_cpio_newc.c +++ b/libarchive/archive_write_set_format_cpio_newc.c @@ -41,6 +41,7 @@ #include "archive.h" #include "archive_entry.h" #include "archive_entry_locale.h" +#include "archive_integer.h" #include "archive_private.h" #include "archive_write_private.h" #include "archive_write_set_format_private.h" @@ -62,6 +63,17 @@ struct cpio { uint64_t entry_bytes_remaining; int padding; + int64_t ino_next; + + struct { + int64_t old_devmajor; + int64_t old_devminor; + int64_t old_ino; + int64_t new_ino; + } *ino_list; + size_t ino_list_size; + size_t ino_list_next; + struct archive_string_conv *opt_sconv; struct archive_string_conv *sconv_default; int init_default_conversion; @@ -162,6 +174,86 @@ archive_write_newc_options(struct archive_write *a, const char *key, return (ARCHIVE_WARN); } +/* + * The newc format stores 32-bit inode numbers and relies on them to identify + * hardlinked files. Generate unique in-range values so distinct 64-bit inode + * numbers cannot collide when written to the archive. + */ +static int64_t +synthesize_ino_value(struct archive_write *a, struct archive_entry *entry) +{ + struct cpio *cpio = a->format_data; + int64_t devmajor = archive_entry_devmajor(entry); + int64_t devminor = archive_entry_devminor(entry); + int64_t ino = archive_entry_ino64(entry); + int64_t ino_new; + size_t i; + + if (ino == 0) + return (0); + + /* Directory link counts do not represent separate archive entries. */ + if (archive_entry_nlink(entry) < 2 || + archive_entry_filetype(entry) == AE_IFDIR) { + if (cpio->ino_next == UINT32_MAX) { + archive_set_error(&a->archive, ERANGE, + "No available inode values for cpio format"); + return (ARCHIVE_FATAL); + } + return (++cpio->ino_next); + } + + /* TODO: Revisit the flat list if large hardlink sets become slow. */ + for (i = 0; i < cpio->ino_list_next; ++i) { + if (cpio->ino_list[i].old_devmajor == devmajor && + cpio->ino_list[i].old_devminor == devminor && + cpio->ino_list[i].old_ino == ino) + return (cpio->ino_list[i].new_ino); + } + + if (cpio->ino_next == UINT32_MAX) { + archive_set_error(&a->archive, ERANGE, + "No available inode values for cpio format"); + return (ARCHIVE_FATAL); + } + ino_new = ++cpio->ino_next; + + if (cpio->ino_list_size <= cpio->ino_list_next) { + size_t newsize, size; + + if (cpio->ino_list_size < 512) + newsize = 512; + else if (archive_ckd_mul_size(&newsize, + cpio->ino_list_size, 2)) { + archive_set_error(&a->archive, ENOMEM, + "No memory for inode translation table"); + return (ARCHIVE_FATAL); + } + if (archive_ckd_mul_size(&size, + newsize, sizeof(cpio->ino_list[0]))) { + archive_set_error(&a->archive, ENOMEM, + "No memory for inode translation table"); + return (ARCHIVE_FATAL); + } + void *newlist = realloc(cpio->ino_list, size); + if (newlist == NULL) { + archive_set_error(&a->archive, ENOMEM, + "No memory for inode translation table"); + return (ARCHIVE_FATAL); + } + + cpio->ino_list_size = newsize; + cpio->ino_list = newlist; + } + + cpio->ino_list[cpio->ino_list_next].old_devmajor = devmajor; + cpio->ino_list[cpio->ino_list_next].old_devminor = devminor; + cpio->ino_list[cpio->ino_list_next].old_ino = ino; + cpio->ino_list[cpio->ino_list_next].new_ino = ino_new; + ++cpio->ino_list_next; + return (ino_new); +} + static struct archive_string_conv * get_sconv(struct archive_write *a) { @@ -267,15 +359,14 @@ write_header(struct archive_write *a, struct archive_entry *entry) format_hex(archive_entry_devminor(entry), h + c_devminor_offset, c_devminor_size); - ino = archive_entry_ino64(entry); - if (ino > 0xffffffff) { - archive_set_error(&a->archive, ERANGE, - "large inode number truncated"); - ret_final = ARCHIVE_WARN; + ino = synthesize_ino_value(a, entry); + if (ino < 0) { + ret_final = (int)ino; + goto exit_write_header; } /* TODO: Set ret_final to ARCHIVE_WARN if any of these overflow. */ - format_hex(ino & 0xffffffff, h + c_ino_offset, c_ino_size); + format_hex(ino, h + c_ino_offset, c_ino_size); format_hex(archive_entry_mode(entry), h + c_mode_offset, c_mode_size); format_hex(archive_entry_uid(entry), h + c_uid_offset, c_uid_size); format_hex(archive_entry_gid(entry), h + c_gid_offset, c_gid_size); @@ -442,6 +533,7 @@ archive_write_newc_free(struct archive_write *a) { struct cpio *cpio = a->format_data; + free(cpio->ino_list); free(cpio); a->format_data = NULL; return (ARCHIVE_OK);