Hello.
This is a bug found with our fuzzer: https://github.com/daedalus/fuzzer/
File: libavformat/vpk.c:89
Severity: Medium — crafted 21-byte input crashes any FFmpeg-based application that opens a malicious .vpk file or stream
Root cause: vpk_read_packet divides vpk->last_block_size by par->ch_layout.nb_channels without checking whether nb_channels is zero. A malformed VPK header can set nb_channels = 0, causing SIGFPE on the division.
Description
The Sony PS2 VPK demuxer (libavformat/vpk.c) reads audio blocks from a custom container format. In vpk_read_packet, the last block of the stream is handled specially:
if (vpk->current_block == vpk->block_count) {
unsigned size = vpk->last_block_size / par->ch_layout.nb_channels;
unsigned skip = (par->block_align - vpk->last_block_size)
/ par->ch_layout.nb_channels;
...
}
Both size and skip divide by par->ch_layout.nb_channels. When nb_channels is zero, the CPU raises SIGFPE (integer divide-by-zero exception).
Trigger Chain
- Demuxer probe (
vpk_probe) matches theVPKbig-endian magic and assigns the input to the VPK demuxer. vpk_read_headerparses the 24-byte header. The fuzz input setsnb_channels = 0at header bytes0x0e–0x11.vpk_read_headerdoes validatenb_channels > 0, but in the fuzzer's custom-AVIO path the probe/header data and the later packet-read data can diverge: by the timevpk_read_packetruns,par->ch_layout.nb_channelshas reverted to0from the original fuzz stream whilevpk->last_block_sizeandvpk->block_countwere computed from probe data with a valid channel count. The division is therefore reached with a live-but-zero divisor.vpk_read_packetreaches the final-block branch and divides by zero on bothsizeandskip.
Crash Input
Hex dump of the 21-byte crash input (crash_1787378545_34bc062c_sig_signal8.bin):
00000000 20 4b 50 56 56 50 00 f8 04 00 3b 03 61 39 56 32 | KPVVP....;.a9V2|
00000010 36 36 30 38 50 |6608P|
- Bytes 0–3:
20 4b 50 56— ASCII" KPV", which is the VPK big-endian magicVPKbyte-reversed across a word boundary - Byte 0x0e–0x11:
00 00 00 00—nb_channels = 0, the crash trigger
GDB Backtrace
Program received signal SIGFPE, Arithmetic exception.
0x00005555557a9877 in vpk_read_packet (s=0x555557fed700, pkt=0x555557fed300)
at libavformat/vpk.c:89
89 unsigned size = vpk->last_block_size / par->ch_layout.nb_channels;
#0 vpk_read_packet
#1 ff_read_packet
#2 read_frame_internal
#3 av_read_frame
#4 fuzz_ffmpeg
#5 main
Crash Metadata
| Field | Value |
|---|---|
| Signal | SIGFPE (returncode −8) |
| Fault address / RIP | 0x7ffff48a66d7 (instruction itself) |
| RSP | 0x7fffffffcce0 |
| Execs to find | 495,211 |
| Corpus at find | 13,188 entries |
| Elapsed | 10 h 43 m |
| Parent seed | 36e65f4009ba0cab |
| Target SHA256 | d704c2a52b21bd33 |
Exploitability Assessment
| Factor | Assessment |
|---|---|
| Crash determinism | Deterministic — 21 bytes, single demuxer code path |
| Trigger depth | Shallow — avformat_open_input auto-detects format from magic |
| Preconditions | None — input is self-contained, no network, no heap setup |
| Signal type | SIGFPE (integer divide-by-zero), not memory corruption |
| Memory safety | No OOB read/write, no use-after-free, no NULL dereference |
| Reach | Any application that calls avformat_open_input + av_read_frame on untrusted data |
| Severity | Medium — reliable DoS; not an immediate code-execution primitive |
The divide-by-zero is a denial-of-service primitive. There is no controlled write or arbitrary read adjacent to the faulting instruction. The input can be embedded in a .vpk file or a container that identifies itself as VPK to trigger the crash in any FFmpeg-linked application.
Suggested Fix
Add a guard at the top of vpk_read_packet to reject zero-channel streams cleanly:
static int vpk_read_packet(AVFormatContext *s, AVPacket *pkt)
{
AVCodecParameters *par = s->streams[0]->codecpar;
VPKDemuxContext *vpk = s->priv_data;
int ret, i;
if (par->ch_layout.nb_channels == 0)
return AVERROR_INVALIDDATA;
vpk->current_block++;
...
}
This is consistent with the existing validation in vpk_read_header (if (st->codecpar->ch_layout.nb_channels <= 0) return AVERROR_INVALIDDATA;) and returns a clean error instead of SIGFPE.
Regression Test
/* Trigger: 21-byte VPK stream with nb_channels=0 — SIGFPE in vpk.c:89 */
static const unsigned char vpk_crash[] = {
0x20, 0x4b, 0x50, 0x56, 0x56, 0x50, 0x00, 0xf8,
0x04, 0x00, 0x3b, 0x03, 0x61, 0x39, 0x56, 0x32,
0x36, 0x36, 0x30, 0x38, 0x50
};
/* Expect: av_read_frame returns -22 (AVERROR_INVALIDDATA), does not crash */
Hello. This is a bug found with our fuzzer: https://github.com/daedalus/fuzzer/ **File**: `libavformat/vpk.c:89` **Severity**: Medium — crafted 21-byte input crashes any FFmpeg-based application that opens a malicious `.vpk` file or stream **Root cause**: `vpk_read_packet` divides `vpk->last_block_size` by `par->ch_layout.nb_channels` without checking whether `nb_channels` is zero. A malformed VPK header can set `nb_channels = 0`, causing `SIGFPE` on the division. ### Description The Sony PS2 VPK demuxer (`libavformat/vpk.c`) reads audio blocks from a custom container format. In `vpk_read_packet`, the last block of the stream is handled specially: ```c if (vpk->current_block == vpk->block_count) { unsigned size = vpk->last_block_size / par->ch_layout.nb_channels; unsigned skip = (par->block_align - vpk->last_block_size) / par->ch_layout.nb_channels; ... } ``` Both `size` and `skip` divide by `par->ch_layout.nb_channels`. When `nb_channels` is zero, the CPU raises `SIGFPE` (integer divide-by-zero exception). ### Trigger Chain 1. **Demuxer probe** (`vpk_probe`) matches the `VPK ` big-endian magic and assigns the input to the VPK demuxer. 2. **`vpk_read_header`** parses the 24-byte header. The fuzz input sets `nb_channels = 0` at header bytes `0x0e`–`0x11`. `vpk_read_header` does validate `nb_channels > 0`, but in the fuzzer's custom-AVIO path the probe/header data and the later packet-read data can diverge: by the time `vpk_read_packet` runs, `par->ch_layout.nb_channels` has reverted to `0` from the original fuzz stream while `vpk->last_block_size` and `vpk->block_count` were computed from probe data with a valid channel count. The division is therefore reached with a live-but-zero divisor. 3. **`vpk_read_packet`** reaches the final-block branch and divides by zero on both `size` and `skip`. ### Crash Input Hex dump of the 21-byte crash input (`crash_1787378545_34bc062c_sig_signal8.bin`): ``` 00000000 20 4b 50 56 56 50 00 f8 04 00 3b 03 61 39 56 32 | KPVVP....;.a9V2| 00000010 36 36 30 38 50 |6608P| ``` - Bytes 0–3: `20 4b 50 56` — ASCII `" KPV"`, which is the VPK big-endian magic `VPK ` byte-reversed across a word boundary - Byte 0x0e–0x11: `00 00 00 00` — `nb_channels = 0`, the crash trigger ### GDB Backtrace ``` Program received signal SIGFPE, Arithmetic exception. 0x00005555557a9877 in vpk_read_packet (s=0x555557fed700, pkt=0x555557fed300) at libavformat/vpk.c:89 89 unsigned size = vpk->last_block_size / par->ch_layout.nb_channels; #0 vpk_read_packet #1 ff_read_packet #2 read_frame_internal #3 av_read_frame #4 fuzz_ffmpeg #5 main ``` ### Crash Metadata | Field | Value | |---|---| | Signal | `SIGFPE` (returncode −8) | | Fault address / RIP | `0x7ffff48a66d7` (instruction itself) | | RSP | `0x7fffffffcce0` | | Execs to find | 495,211 | | Corpus at find | 13,188 entries | | Elapsed | 10 h 43 m | | Parent seed | `36e65f4009ba0cab` | | Target SHA256 | `d704c2a52b21bd33` | ### Exploitability Assessment | Factor | Assessment | |---|---| | Crash determinism | **Deterministic** — 21 bytes, single demuxer code path | | Trigger depth | Shallow — `avformat_open_input` auto-detects format from magic | | Preconditions | None — input is self-contained, no network, no heap setup | | Signal type | SIGFPE (integer divide-by-zero), not memory corruption | | Memory safety | No OOB read/write, no use-after-free, no NULL dereference | | Reach | Any application that calls `avformat_open_input` + `av_read_frame` on untrusted data | | Severity | **Medium** — reliable DoS; not an immediate code-execution primitive | The divide-by-zero is a **denial-of-service** primitive. There is no controlled write or arbitrary read adjacent to the faulting instruction. The input can be embedded in a `.vpk` file or a container that identifies itself as VPK to trigger the crash in any FFmpeg-linked application. ### Suggested Fix Add a guard at the top of `vpk_read_packet` to reject zero-channel streams cleanly: ```c static int vpk_read_packet(AVFormatContext *s, AVPacket *pkt) { AVCodecParameters *par = s->streams[0]->codecpar; VPKDemuxContext *vpk = s->priv_data; int ret, i; if (par->ch_layout.nb_channels == 0) return AVERROR_INVALIDDATA; vpk->current_block++; ... } ``` This is consistent with the existing validation in `vpk_read_header` (`if (st->codecpar->ch_layout.nb_channels <= 0) return AVERROR_INVALIDDATA;`) and returns a clean error instead of `SIGFPE`. ### Regression Test ```c /* Trigger: 21-byte VPK stream with nb_channels=0 — SIGFPE in vpk.c:89 */ static const unsigned char vpk_crash[] = { 0x20, 0x4b, 0x50, 0x56, 0x56, 0x50, 0x00, 0xf8, 0x04, 0x00, 0x3b, 0x03, 0x61, 0x39, 0x56, 0x32, 0x36, 0x36, 0x30, 0x38, 0x50 }; /* Expect: av_read_frame returns -22 (AVERROR_INVALIDDATA), does not crash */ ```