[cocoa] AVStreamDataParser accepts media segments not preceded by an init segment
Component: WebCore Media Source Extensions | 4ed0007
LayoutTests/media/media-source/media-source-append-media-before-init-expected.txt
LayoutTests/media/media-source/media-source-append-media-before-init.html
WebKit의 Cocoa 플랫폼 Media Source Extensions 구현은 ISO-BMFF 파싱을 AVFoundation의 private API인 AVStreamDataParser에 위임합니다. MSE 스펙의 Segment Parser Loop는 엄격한 순서를 요구합니다. Initialization segment(ftyp+moov)가 media segment(moof+mdat)보다 먼저 와야 한다는 규칙입니다. 그러나 AVStreamDataParser는 이를 강제하지 않아서, init segment 없이 media data가 먼저 들어와도 그대로 받아들이고 CMFormatDescription이 null인 CMSampleBuffer를 생성하게 됩니다. 이렇게 만들어진 CMSampleBuffer는 이후 decode 단계에서 실패합니다. 스트림 중간에 format이 바뀌는 경우, 즉 abort()나 changeType() 호출 없이 새로운 ftyp가 도착하는 경우에는 CoreMedia 내부의 MoofManifold에서 -16046 오류가 발생하는데, 이 오류는 조용히 무시되어 parser가 비정상 상태에 남게 됩니다. 이번 commit은 SourceBufferParserAVFObjC와 AVStreamDataParser 사이에 ISOBMFFPreParser를 새로 삽입합니다. 이 pre-parser는 appendData() 호출 경계를 넘나들며 ISO-BMFF box header를 순회하되, box 내용은 파싱하지 않습니다. 이를 통해 init segment 이전에 추가된 media segment를 거부하고, 스트림 중간에 새로운 ftyp가 나타나면 AVStreamDataParserStreamDataDiscontinuity 신호를 주입합니다. SourceBufferParserAVFObjC의 AppendFlags::Discontinuity 관련 배관 코드는 원래 존재했지만, 스트림 중간 재초기화 경로에서는 한 번도 트리거된 적이 없었습니다. Pre-parser는 기존의 ISOBox::peekBox 대신 BitReader를 사용합니다. ISOBox::peekBox 경로는 JSC::DataView를 필요로 하고 Gigacage를 거치는 반면, SharedBuffer의 내용은 Gigacage로 할당되지 않기 때문입니다. 이 조합은 EXC_BAD_ACCESS로 fault를 일으킵니다.
Before:
appendData(attacker data)
│
▼
SourceBufferParserAVFObjC
│ (no ordering check)
▼
AVStreamDataParser ──► CMSampleBuffer (null CMFormatDescription → decode failure)
After:
appendData(attacker data)
│
▼
SourceBufferParserAVFObjC
│
▼
ISOBMFFPreParser (stateful box-header scanner)
│
├─[media seg, no prior init]──► parsing error → SourceBuffer error event
│
├─[new ftyp mid-stream]──► SPLIT append at ftyp offset
│ ├─► data[0..ftyp_offset) ──► AVStreamDataParser (original flags)
│ └─► data[ftyp_offset..) ──► AVStreamDataParser (Discontinuity flag)
│
└─[normal]──────────────────────► AVStreamDataParser ──► valid CMSampleBuffers
Significance
Apple 플랫폼의 MSE ingestion 경로에 새로운 stateful binary parser가 자리잡게 되었으며, attacker가 공급한 데이터가 AVFoundation에 도달하기 전에 이 parser를 거쳐 처리됩니다. 그동안 사용되지 않던 AppendFlags::Discontinuity 코드 경로도 abort()/changeType() 밖에서는 처음으로 활성화됩니다. 실제 사용자 관점에서는 Twitch의 광고-본편 전환 구간에서 플레이어가 멈추는 문제가 이번 변경의 동기가 되었습니다.
Audit directions
다섯 가지 표면이 모두 attacker가 공급한 바이트를 다룹니다. Box size field decoding: pre-parser는 ISO-BMFF의 세 가지 size 인코딩 방식을 모두 처리합니다. 표준 32비트, 64비트 extended(size field가 1이고 뒤에 8바이트 length가 붙는 형태), 그리고 open-ended(size가 0이면 "스트림의 나머지 전체"를 의미) 방식입니다. 64비트의 attacker-controlled size로부터 다음 box의 offset을 계산할 때 integer overflow가 발생하는 것이 전형적인 공격 패턴입니다. 32비트에서 64비트로 승격되는 경로와 size==0 sentinel 처리가 가장 눈여겨봐야 할 지점이며, 같은 삼중 구조는 트리 안의 다른 모든 box-walking parser에도 동일하게 적용됩니다.
Cross-boundary partial header state (m_pendingHeaderBytes): 두 번의 appendData() 호출에 걸쳐 나뉜 header는 byte-accumulation buffer를 통해 재조립됩니다. 소비된 바이트 수와 버퍼링된 바이트 수 사이에 off-by-one 오류가 있으면, parser가 파악하는 box 경계가 어긋날 수 있습니다. 이 경우 media segment를 init segment로, 혹은 그 반대로 잘못 분류하는 결과로 이어질 수 있습니다.
Append split logic: append 도중 ftyp를 감지하면, pre-parser는 byte offset을 계산해 SharedBuffer를 둘로 나눕니다. Split offset 계산에서 산술 오류가 발생하면, 특히 32비트 header가 8바이트이고 64비트 extended header가 16바이트로 서로 다른 점을 감안할 때, 정렬이 맞지 않는 데이터가 AVStreamDataParser로 전달될 수 있습니다.
Dead code path reactivation: AppendFlags::Discontinuity는 그동안 resetParserState() 밖에서는 도달한 적이 없는 코드였습니다. 따라서 해당 flag를 처리하는 하위 로직에 내재된 가정, 즉 이 flag가 도착했을 때 AVStreamDataParser가 어떤 상태에 있어야 하는지에 대한 가정은 스트림 중간 재초기화 시나리오에서 한 번도 검증된 적이 없습니다. 부분적인 media data 전송 이후에 Discontinuity를 보내는 경우, 예상치 못한 AVFoundation의 동작이 드러날 가능성이 있습니다.
Non-Gigacaged memory via BitReader: 이번 commit은 ISOBox::peekBox가 SharedBuffer 메모리에서 fault를 일으킨다는 점을 명시적으로 문서화하면서 대신 BitReader를 사용합니다. BitReader의 bounds checking에 허점이 있고, 예를 들어 잘려나간 64비트 size field로 인해 pre-parser가 남은 buffer 길이를 잘못 판단하는 경우, 이 경로는 Gigacage 안전망 없이 attacker 데이터를 그대로 다루게 됩니다. 같은 이유로 최근 DataView/Gigacage 경로에서 벗어난 다른 parser들 역시 동일한 의문을 안고 있습니다.