DictionaryRef::get and its iterator convert FFmpeg dictionary bytes with from_utf8_unchecked. FFmpeg does not guarantee that container metadata is UTF-8; for example, RIFF INFO tags commonly contain legacy encodings.
Consequently, an ordinary media file can make a safe API return a &str that violates Rust's UTF-8 validity invariant. This is the one report in this set whose trigger is untrusted input. I confirmed language-level UB but did not demonstrate memory corruption or exploitability.
Minimal reproduction
First generate a small valid WAV containing a non-UTF-8 title:
import struct
def chunk(name, data):
size = len(data)
if size % 2:
data += b'\0'
return name + struct.pack('<I', size) + data
title = b'x\xff\xbf\xbf\xbfy\0'
info = chunk(b'LIST', b'INFO' + chunk(b'INAM', title))
fmt = chunk(b'fmt ', struct.pack('<HHIIHH', 1, 1, 8000, 8000, 1, 8))
data = chunk(b'data', b'\x80' * 16)
body = b'WAVE' + info + fmt + data
open('badchar.wav', 'wb').write(b'RIFF' + struct.pack('<I', len(body)) + body)
Then, with ffmpeg-next = "=9.0.0":
use ffmpeg_next as ffmpeg;
use ffmpeg::format;
fn main() {
ffmpeg::init().unwrap();
let input = format::input("badchar.wav").unwrap();
let metadata = input.metadata();
let title = metadata.get("title").unwrap(); // safe API returns `&str`
println!("bytes: {:x?}", title.as_bytes());
println!("checked: {:?}", std::str::from_utf8(title.as_bytes()));
let chars: Vec<char> = title.chars().collect();
println!("{chars:?}");
}
$ python3 make_wav.py
$ cargo run
bytes: [78, ff, bf, bf, bf, 79]
checked: Err(Utf8Error { ... })
thread 'main' panicked ... unsafe precondition(s) violated: invalid value for `char`
thread caused non-unwinding panic. aborting.
The crafted title above is chosen to make the decoder produce a specific invalid scalar, but a crafted file is not required. An ordinary Latin-1 tag — caf\xe9 na\xefve \xff\xfe latin-1, i.e. what a real-world legacy RIFF file looks like — produces the same outcome: from_utf8 returns Err, chars() yields characters that are not in the file ('\u{982e}', '\u{fda5}'), and the iteration then trips the same check.
thread 'main' panicked at core/src/char/methods.rs:243:18:
unsafe precondition(s) violated: invalid value for `char`
thread caused non-unwinding panic. aborting.
The debug-only precondition check is what turns this into an abort. In release it is absent and the invalid scalar simply propagates, which is the worse outcome:
$ cargo run --release
bytes = [78, ff, bf, bf, bf, 79]
is valid UTF-8? = false
char scalar = 0x78 valid = true
char scalar = 0x1fffff valid = false <- not a Unicode scalar value
char scalar = 0x79 valid = true
0x1fffff is outside the range char is permitted to hold, so every downstream consumer of that &str is operating on a value the language says cannot exist.
Environment
Reproduced with ffmpeg-next/ffmpeg-sys-next 9.0.0, x86_64-unknown-linux-gnu, default features, on two independent stacks:
- Debian 12 — FFmpeg 5.1.9 (
libavformat.so.59.27.100)
- Debian 13 — FFmpeg 7.1.5 (
libavformat.so.61.7.103), rustc 1.98.0 stable
The generator, both reproductions, and the debug/release transcripts are available on request.
Proposed direction
Possible APIs include checked conversion plus a raw get_bytes accessor, lossy conversion, or returning None/an error for invalid UTF-8. The iterator needs the same decision for both keys and values.
I have not opened a PR because this changes API or behavior. Which policy would you prefer, and would you welcome a PR implementing it?
Found by Crustify, an experimental UB/soundness auditing agent developed at UC Berkeley and running on Claude Opus 5, then manually reviewed and independently reproduced.
DictionaryRef::getand its iterator convert FFmpeg dictionary bytes withfrom_utf8_unchecked. FFmpeg does not guarantee that container metadata is UTF-8; for example, RIFFINFOtags commonly contain legacy encodings.Consequently, an ordinary media file can make a safe API return a
&strthat violates Rust's UTF-8 validity invariant. This is the one report in this set whose trigger is untrusted input. I confirmed language-level UB but did not demonstrate memory corruption or exploitability.Minimal reproduction
First generate a small valid WAV containing a non-UTF-8 title:
Then, with
ffmpeg-next = "=9.0.0":The crafted title above is chosen to make the decoder produce a specific invalid scalar, but a crafted file is not required. An ordinary Latin-1 tag —
caf\xe9 na\xefve \xff\xfe latin-1, i.e. what a real-world legacy RIFF file looks like — produces the same outcome:from_utf8returnsErr,chars()yields characters that are not in the file ('\u{982e}','\u{fda5}'), and the iteration then trips the same check.The debug-only precondition check is what turns this into an abort. In release it is absent and the invalid scalar simply propagates, which is the worse outcome:
0x1fffffis outside the rangecharis permitted to hold, so every downstream consumer of that&stris operating on a value the language says cannot exist.Environment
Reproduced with
ffmpeg-next/ffmpeg-sys-next9.0.0,x86_64-unknown-linux-gnu, default features, on two independent stacks:libavformat.so.59.27.100)libavformat.so.61.7.103), rustc 1.98.0 stableThe generator, both reproductions, and the debug/release transcripts are available on request.
Proposed direction
Possible APIs include checked conversion plus a raw
get_bytesaccessor, lossy conversion, or returningNone/an error for invalid UTF-8. The iterator needs the same decision for both keys and values.I have not opened a PR because this changes API or behavior. Which policy would you prefer, and would you welcome a PR implementing it?
Found by Crustify, an experimental UB/soundness auditing agent developed at UC Berkeley and running on Claude Opus 5, then manually reviewed and independently reproduced.