Integer underflow in the OpenType VORG table size computation
Component: WebCore Font Parsing | 78b8d62
OpenType table은 @font-face로 들어온 font 바이트를 그대로 파싱해 구성됩니다. 이 바이트는 내용에 제약이 없고, attacker가 통제할 수 있는 입력에 해당합니다. VORGTable은 OpenType의 VORG(Vertical Origin) table을 C에서 흔히 쓰는 방식으로 모델링합니다. 고정 크기 구조체 끝에 원소 하나짜리 배열 vertOriginYMetrics[1]을 두는 형태이며, 실제 원소 개수는 font 데이터에서 결정됩니다. 그리고 requiredSize()가 이 table에 실제로 필요한 바이트 수를 계산합니다. 호출자는 배열을 읽기 전에 이 값으로 bounds check를 수행하게 됩니다. 다만 개수를 attacker가 통제하는 상황에서는 이런 산술이 미묘하게 틀리기 쉽습니다.
이번 commit은 VORGTable::requiredSize()(OpenTypeVerticalData.cpp)의 계산식을 변경했습니다. 기존의 sizeof(*this) + sizeof(VertOriginYMetrics) * (numVertOriginYMetrics - 1) 대신 offsetof 기반의 식을 사용하며, 새 식은 numVertOriginYMetrics가 0인 경우에도 underflow가 발생하지 않습니다. 함께 regression test도 추가되었습니다. 테스트에는 GSUB FeatureList table이 레코드 중간에서 잘려 있는 font를 사용합니다.
Significance
기존 식은 개수가 0일 때 underflow가 발생했습니다. 다만 이 구조체에는 뒤쪽 padding이 없었던 덕분에, wraparound된 값이 우연히 상쇄되어 올바른 offset으로 되돌아왔습니다. 그래서 이번 변경은 실제로 성립하던 bounds check 우회를 막은 것이 아니라 hardening에 가깝습니다. 아무도 명시해 두지 않은 레이아웃 특성에 정확성을 기대고 있던 취약한 관용구를 걷어낸 셈입니다. 추가된 테스트가 실제 out-of-bounds read 보호를 고정해 주기는 합니다. 다만 그 대상은 이번 변경이 아니라, GSUB 파싱의 findFeature OOB read를 고친 별개의 이미 반영된 수정입니다.
Audit directions
Narrow: 나머지 OpenType table 모델에도 동일한 sizeof(*this) + sizeof(Element) * (count - 1) 관용구가 남아 있는지 훑어볼 필요가 있습니다. 각 사례가 맞게 동작하는 이유는 해당 구조체의 padding이 우연히 들어맞았기 때문일 뿐이며, 찾아야 할 신호는 크기 계산식 안의 - 1과 그 피연산자가 font 바이트에서 온 값인지 여부입니다. Wider: 더 일반화하면, 부호 없는 개수가 0일 때 뺄셈이 0 아래로 내려가는 trailing array 크기 산술이 핵심 패턴입니다. 이런 코드는 font 파싱에만 국한되지 않고, flexible array member 구조체의 크기를 외부 데이터로 결정하는 모든 곳에 나타납니다. 그래서 검색 기준은 offsetof가 올바른 형태이고, 오래된 코드베이스가 같은 계산을 sizeof에서 원소 하나를 뺀 식으로 적어 두었을 가능성입니다. Widest: 우연히 맞아떨어진 정확성은 그 자체로 하나의 점검 범주에 해당합니다. 두 개의 오류가 서로 상쇄되어 맞는 식은 누군가 필드를 하나 추가하는 순간 틀리게 됩니다. 그래서 리뷰에서 눈여겨볼 신호는, 정확성을 설명하려면 구조체 padding까지 따져야 하는 산술 표현 그 자체입니다.