fix(webp): fix exif chunk bounds handling (#5175)
Fixes bounds handling for WebP EXIF metadata chunks. Before this change, the WebP reader always skipped the first 6 bytes of an EXIF chunk before calling `decode_exif()`: ```cpp bytes + 6 size - 6 ``` That assumed every WebP EXIF chunk was at least 6 bytes long and always started with the optional Exif\0\0 prefix. A malformed WebP file with a shorter EXIF chunk could therefore advance the pointer past the chunk payload and underflow the remaining size passed to the EXIF parser. This is reachable through normal OpenImageIO image-open paths, including tools such as iinfo, iconvert, and oiiotool, when opening a malformed WebP file. The fix changes the WebP reader to treat the EXIF chunk as a bounded byte span. Before calling decode_exif(), it now verifies that the payload contains enough bytes for a TIFF header, either at the start of the chunk or after an optional Exif\0\0 prefix. The WebP reader no longer performs an unconditional 6-byte skip; the shared EXIF decoder already handles the optional prefix safely. ### Tests Added WebP regression coverage for malformed short EXIF chunks. The testsuite/webp test now generates WebP files with EXIF chunk payload lengths: - 0 bytes - 4 bytes: Exif - 5 bytes: Exif\0 - 6 bytes: Exif\0\0 - 13 bytes: Exif\0\0 plus a truncated TIFF header Each generated file is opened through oiiotool to verify that malformed EXIF metadata does not crash or read past the chunk buffer. Validated locally with: python3 -m py_compile testsuite/webp/src/make-short-exif-webp.py git diff --check cmake --build build-webp --target oiiotool -j 8 Also ran direct oiiotool smoke tests for all generated malformed WebP inputs. Assisted-by: Codex GPT5.5xHigh --------- Signed-off-by: Vlad (Kuzmin) Erium <libalias@gmail.com>
This commit is contained in:
@@ -15,6 +15,19 @@ OIIO_PLUGIN_NAMESPACE_BEGIN
|
||||
namespace webp_pvt {
|
||||
|
||||
|
||||
static bool
|
||||
webp_exif_payload_has_tiff_header(cspan<uint8_t> exif)
|
||||
{
|
||||
const size_t exif_header_size = 6;
|
||||
const bool has_exif_header = exif.size() >= exif_header_size
|
||||
&& exif[0] == 'E' && exif[1] == 'x'
|
||||
&& exif[2] == 'i' && exif[3] == 'f'
|
||||
&& exif[4] == 0 && exif[5] == 0;
|
||||
const size_t tiff_header_offset = has_exif_header ? exif_header_size : 0;
|
||||
return exif.size() >= tiff_header_offset + sizeof(TIFFHeader);
|
||||
}
|
||||
|
||||
|
||||
class WebpInput final : public ImageInput {
|
||||
public:
|
||||
WebpInput() {}
|
||||
@@ -181,9 +194,9 @@ WebpInput::open(const std::string& name, ImageSpec& spec,
|
||||
WebPChunkIterator chunk_iter;
|
||||
if (m_demux_flags & EXIF_FLAG
|
||||
&& WebPDemuxGetChunk(m_demux, "EXIF", 1, &chunk_iter)) {
|
||||
decode_exif(string_view((const char*)chunk_iter.chunk.bytes + 6,
|
||||
chunk_iter.chunk.size - 6),
|
||||
m_spec);
|
||||
cspan<uint8_t> exif_span(chunk_iter.chunk.bytes, chunk_iter.chunk.size);
|
||||
if (webp_exif_payload_has_tiff_header(exif_span))
|
||||
decode_exif(exif_span, m_spec);
|
||||
WebPDemuxReleaseChunkIterator(&chunk_iter);
|
||||
}
|
||||
if (m_demux_flags & XMP_FLAG
|
||||
|
||||
@@ -325,8 +325,11 @@ def testtex_command (file: str, extraargs: str="", silent: bool=False, concat: b
|
||||
|
||||
|
||||
# Construct a command that will run iconvert and append its output to out.txt
|
||||
def iconvert (args: str, silent: bool=False, concat: bool=True, failureok: bool=False) -> str:
|
||||
def iconvert (args: str, silent: bool=False, concat: bool=True,
|
||||
failureok: bool=False, successmessage: str="") -> str:
|
||||
cmd = (oiio_app("iconvert") + " " + args)
|
||||
if successmessage:
|
||||
cmd = "(" + cmd + " && echo " + successmessage + ")"
|
||||
if not silent :
|
||||
cmd += redirect
|
||||
if failureok :
|
||||
|
||||
@@ -24,3 +24,8 @@ rgba.webp : 64 x 64, 4 channel, uint8 webp
|
||||
channel list: R, G, B, A
|
||||
oiio:ColorSpace: "srgb_rec709_scene"
|
||||
oiio:UnassociatedAlpha: 1
|
||||
short-exif-len0.webp-ok
|
||||
short-exif-len4.webp-ok
|
||||
short-exif-len5.webp-ok
|
||||
short-exif-len6.webp-ok
|
||||
short-exif-len13.webp-ok
|
||||
|
||||
@@ -14,3 +14,16 @@ for f in files:
|
||||
|
||||
command += oiiotool ("--create 64x64 4 -o rgba.webp")
|
||||
command += info_command ("rgba.webp", "--iconfig oiio:UnassociatedAlpha 1", safematch=True)
|
||||
|
||||
|
||||
command += oiiotool ("--create 1x1 3 -d uint8 -o base-short-exif.webp")
|
||||
command += run_app (pythonbin + " src/make-short-exif-webp.py", silent=True)
|
||||
short_exif_files = [
|
||||
"short-exif-len0.webp",
|
||||
"short-exif-len4.webp",
|
||||
"short-exif-len5.webp",
|
||||
"short-exif-len6.webp",
|
||||
"short-exif-len13.webp",
|
||||
]
|
||||
for f in short_exif_files:
|
||||
command += iconvert(f + " out.null", successmessage=f + "-ok")
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright Contributors to the OpenImageIO project.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# https://github.com/AcademySoftwareFoundation/OpenImageIO
|
||||
|
||||
import struct
|
||||
|
||||
|
||||
BASE = "base-short-exif.webp"
|
||||
EXIF_FLAG = 0x08
|
||||
|
||||
CASES = [
|
||||
("short-exif-len0.webp", b""),
|
||||
("short-exif-len4.webp", b"Exif"),
|
||||
("short-exif-len5.webp", b"Exif\x00"),
|
||||
("short-exif-len6.webp", b"Exif\x00\x00"),
|
||||
("short-exif-len13.webp", b"Exif\x00\x00II*\x00\x08\x00\x00"),
|
||||
]
|
||||
|
||||
|
||||
def read_chunks(data):
|
||||
if len(data) < 12 or data[:4] != b"RIFF" or data[8:12] != b"WEBP":
|
||||
raise RuntimeError("%s is not a RIFF WebP file" % BASE)
|
||||
|
||||
chunks = []
|
||||
offset = 12
|
||||
while offset < len(data):
|
||||
if offset + 8 > len(data):
|
||||
raise RuntimeError("truncated WebP chunk header")
|
||||
fourcc = data[offset : offset + 4]
|
||||
size = struct.unpack_from("<I", data, offset + 4)[0]
|
||||
begin = offset + 8
|
||||
end = begin + size
|
||||
if end > len(data):
|
||||
raise RuntimeError("truncated WebP chunk payload")
|
||||
chunks.append((fourcc, data[begin:end]))
|
||||
offset = end + (size & 1)
|
||||
return chunks
|
||||
|
||||
|
||||
def write_chunk(fourcc, payload):
|
||||
chunk = fourcc + struct.pack("<I", len(payload)) + payload
|
||||
if len(payload) & 1:
|
||||
chunk += b"\x00"
|
||||
return chunk
|
||||
|
||||
|
||||
def make_webp(exif_payload, image_chunks):
|
||||
vp8x_payload = bytes((EXIF_FLAG, 0, 0, 0, 0, 0, 0, 0, 0, 0))
|
||||
chunks = write_chunk(b"VP8X", vp8x_payload)
|
||||
chunks += write_chunk(b"EXIF", exif_payload)
|
||||
for fourcc, payload in image_chunks:
|
||||
if fourcc not in (b"VP8X", b"EXIF"):
|
||||
chunks += write_chunk(fourcc, payload)
|
||||
body = b"WEBP" + chunks
|
||||
return b"RIFF" + struct.pack("<I", len(body)) + body
|
||||
|
||||
|
||||
with open(BASE, "rb") as input_file:
|
||||
base_data = input_file.read()
|
||||
|
||||
image_chunks = read_chunks(base_data)
|
||||
|
||||
for filename, exif_payload in CASES:
|
||||
with open(filename, "wb") as output_file:
|
||||
output_file.write(make_webp(exif_payload, image_chunks))
|
||||
Reference in New Issue
Block a user