OOB writes in the non-Cocoa AVIF, WebP, and JPEG XL decoders
Three decoders, three codec libraries, one out-of-bounds write
Component: WebCore image decoders | 3f8ee96
GTK와 WPE port는 AVIF, WebP, JPEG XL을 각각 libavif, libwebp, libjxl로 디코딩합니다. 반면 Cocoa port는 이 포맷들을 CoreGraphics로 처리하기 때문에 해당 코드가 실행되지 않습니다. 각 decoder는 먼저 container header를 파싱해 전체 canvas 크기를 읽습니다. 그다음 개별 frame을 codec 라이브러리에 전달하는데, 이 라이브러리는 frame 자체 bitstream에 들어 있는 크기를 기준으로 디코딩합니다. 두 값이 일치한다는 보장은 없으며, 조작된 파일에서는 특히 그렇습니다. ImageBackingStore는 decoder들이 픽셀을 기록하는 공용 buffer이며, 크기는 container가 선언한 값에 맞춰 잡힙니다. 여기에는 animation 재생 중 frame을 합성할 때 사용하는 frame별 rect도 함께 담겨 있습니다.
이 commit은 세 decoder 모두에서 발생하던 out-of-bounds write를 수정했습니다. WebP와 JPEG XL은 픽셀 복사 범위를 canvas가 선언한 rect로 제한하도록 변경되었습니다. AVIF는 더 엄격한 방식을 택해, header 크기와 libavif가 디코딩한 크기가 어긋나면 디코딩 자체를 실패 처리합니다. 함께 들어간 보조 수정이 두 가지 있습니다. 먼저 ImageBackingStore::create()는 allocation에 실패했을 때 사용할 수 없는 객체 대신 nullptr을 반환하도록 바뀌었습니다. 또한 copy constructor가 원본의 frame rect를 그대로 유지하도록 수정되었는데, 기존에는 이 값을 누락하고 있었습니다. 새로 추가된 unit test가 겨냥하는 동작이 바로 이 부분입니다.
Significance
서로 다른 decoder backend 세 곳이 동일한 결함을 공유했고, GTK와 WPE port에서는 <img> 하나를 디코딩하는 것만으로 신뢰할 수 없는 웹 콘텐츠에서 각각에 도달할 수 있습니다. 선언된 크기와 decode buffer의 크기가 일치해야 한다는 invariant는 어디에서도 일관되게 강제되지 않았습니다. 이런 형태이기 때문에 malformed input 한 종류가 서로 독립된 세 code path에서 heap corruption을 유발할 수 있었습니다.
Audit directions
앞으로 눈여겨볼 패턴은, destination buffer의 크기를 한쪽 기준으로 잡아 놓고 실제 기록 길이는 다른 쪽 값에서 가져오는 경우입니다. Narrow: 남아 있는 non-Cocoa decoder들, 즉 GIF, PNG/APNG, JPEG, BMP, ICO도 모두 container header를 먼저 읽고 frame을 디코딩하는 동일한 형태를 따릅니다. 각각에서 row 복사나 memcpy의 상한이 backing store 자체 rect가 아니라 header에 선언된 크기로 잡혀 있는지 점검할 필요가 있습니다. Wider: ImageBackingStore의 contract가 두 가지 방향으로 바뀌었고, 둘 다 호출하는 쪽에서 관찰됩니다. 모든 create() 호출 지점에서 이전에는 필요 없었던 null check가 들어가 있는지 살펴봐야 합니다. 또한 backing store를 복사하는 지점마다 frame rect가 초기화된다는 전제에 암묵적으로 기대던 코드가 없는지 확인이 필요합니다. Widest: 외부 codec 라이브러리를 가져다 쓰는 decoder는, 호출한 쪽이 요구하지 않은 크기를 그 라이브러리가 반환할 수 있다는 특성까지 함께 떠안게 됩니다. 리뷰에서 신호가 되는 지점은, 복사 루프의 상한이 destination 자체 크기가 아니라 입력에서 파싱한 struct 필드에서 오는 경우입니다.