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:
Vlad Erium
2026-05-04 21:52:27 -07:00
committed by GitHub
parent 3091061460
commit a9bcad4b70
5 changed files with 105 additions and 4 deletions
+16 -3
View File
@@ -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
+4 -1
View File
@@ -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 :
+5
View File
@@ -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
+13
View File
@@ -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))