Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/artifacts
/Cargo.lock
/corpus
/coverage
/target
42 changes: 42 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[package]
name = "flac-codec-fuzz"
version = "0.0.0"
publish = false
edition = "2024"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"

[dependencies.flac-codec]
path = ".."

[[bin]]
name = "decode_raw"
path = "fuzz_targets/decode_raw.rs"
test = false
doc = false
bench = false

[[bin]]
name = "decode_structured"
path = "fuzz_targets/decode_structured.rs"
test = false
doc = false
bench = false

[[bin]]
name = "encode_roundtrip"
path = "fuzz_targets/encode_roundtrip.rs"
test = false
doc = false
bench = false

[[bin]]
name = "decode_seek"
path = "fuzz_targets/decode_seek.rs"
test = false
doc = false
bench = false
26 changes: 26 additions & 0 deletions fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Codec fuzzing

Run the raw decoder target with the crate fixtures and external decoder corpus as seeds:

```console
cargo +nightly fuzz run decode_raw tests/data flac-test-files/subset flac-test-files/uncommon flac-test-files/faulty -- -max_len=16777216
```

Run the structure-aware target to preserve valid checksums while mutating stream invariants:

```console
cargo +nightly fuzz run decode_structured
```

Exercise seek-table and post-seek decoding paths with the decoder corpus:

```console
cargo +nightly fuzz run decode_seek tests/data flac-test-files/subset flac-test-files/uncommon
```

Exercise encoder options, all bit depths, channel counts, and short-block boundaries while
asserting exact decoder round trips:

```console
cargo +nightly fuzz run encode_roundtrip
```
31 changes: 31 additions & 0 deletions fuzz/fuzz_targets/decode_raw.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#![no_main]

use flac_codec::{
decode::{FlacStreamReader, verify_reader},
stream::FrameIterator,
};
use libfuzzer_sys::fuzz_target;
use std::io::BufReader;

fuzz_target!(|data: &[u8]| {
if data.len() > 16 * 1024 * 1024 {
return;
}

let _ = verify_reader(data);

if let Ok(mut frames) = FrameIterator::new(data) {
for _ in 0..16 {
if frames.next().is_none() {
break;
}
}
}

let mut stream = FlacStreamReader::new(BufReader::new(data));
for _ in 0..4 {
if stream.read().is_err() {
break;
}
}
});
37 changes: 37 additions & 0 deletions fuzz/fuzz_targets/decode_seek.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#![no_main]

use flac_codec::decode::{FlacSampleReader, Metadata};
use libfuzzer_sys::fuzz_target;
use std::io::Cursor;

fuzz_target!(|data: &[u8]| {
if data.len() > 16 * 1024 * 1024 {
return;
}

let Ok(mut reader) = FlacSampleReader::new_seekable(Cursor::new(data)) else {
return;
};
let total_samples = reader.total_samples().unwrap_or_default();
let derived = data
.iter()
.take(8)
.enumerate()
.fold(0u64, |value, (shift, byte)| {
value | (u64::from(*byte) << (shift * 8))
});

for sample in [
0,
total_samples / 2,
total_samples,
total_samples.saturating_add(1),
derived,
u64::MAX,
] {
if reader.seek(sample).is_ok() {
let mut samples = [0; 64];
let _ = reader.read(&mut samples);
}
}
});
113 changes: 113 additions & 0 deletions fuzz/fuzz_targets/decode_structured.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#![no_main]

use flac_codec::{
byteorder::LittleEndian,
decode::{FlacByteReader, FlacSampleReader, verify_reader},
encode::{FlacSampleWriter, Options},
metadata::{BlockList, Streaminfo, write_blocks},
stream::{FrameIterator, FrameNumber},
};
use libfuzzer_sys::fuzz_target;
use std::io::{Cursor, sink};
use std::num::NonZero;
use std::sync::OnceLock;

fn base_stream() -> &'static [u8] {
static BASE: OnceLock<Vec<u8>> = OnceLock::new();

BASE.get_or_init(|| {
let mut output = Cursor::new(Vec::new());
let mut writer = FlacSampleWriter::new(
&mut output,
Options::fast().block_size(16).unwrap().no_padding(),
44100,
16,
1,
Some(33),
)
.unwrap();
writer.write(&(0..33).collect::<Vec<_>>()).unwrap();
writer.finalize().unwrap();
output.into_inner()
})
}

fn mutated_stream(selector: u8) -> Vec<u8> {
let source = base_stream();
let mut blocks = BlockList::read(source).unwrap();
let mut frames = FrameIterator::new(source)
.unwrap()
.map(|frame| frame.map(|(frame, _)| frame))
.collect::<Result<Vec<_>, _>>()
.unwrap();

match selector % 7 {
0 => blocks.streaminfo_mut().maximum_frame_size = NonZero::new(1),
1 => {
blocks.streaminfo_mut().minimum_frame_size =
Some(NonZero::new(Streaminfo::MAX_FRAME_SIZE).unwrap());
blocks.streaminfo_mut().maximum_frame_size =
Some(NonZero::new(Streaminfo::MAX_FRAME_SIZE).unwrap());
}
2 => {
blocks.streaminfo_mut().minimum_block_size = u16::MAX;
blocks.streaminfo_mut().maximum_block_size = u16::MAX;
}
3 => blocks.streaminfo_mut().total_samples = NonZero::new(1),
4 => {
blocks.streaminfo_mut().minimum_frame_size = None;
blocks.streaminfo_mut().maximum_frame_size = None;
frames[1].header.frame_number = frames[0].header.frame_number;
}
5 => {
blocks.streaminfo_mut().minimum_frame_size = None;
blocks.streaminfo_mut().maximum_frame_size = None;
let mut sample_offset = 0;
for frame in &mut frames {
frame.header.blocking_strategy = true;
frame.header.frame_number = FrameNumber(sample_offset);
sample_offset += u64::from(u16::from(frame.header.block_size));
}
frames[1].header.frame_number.0 += 1;
}
6 => {
blocks.streaminfo_mut().minimum_frame_size = None;
blocks.streaminfo_mut().maximum_frame_size = None;
frames[1].header.blocking_strategy = true;
frames[1].header.frame_number =
FrameNumber(u64::from(u16::from(frames[0].header.block_size)));
}
_ => unreachable!(),
}

let streaminfo = blocks.streaminfo().clone();
let mut output = Vec::new();
write_blocks(&mut output, blocks).unwrap();
for frame in frames {
frame.write(&streaminfo, &mut output).unwrap();
}
output
}

fn assert_rejected(flac: &[u8]) {
assert!(verify_reader(flac).is_err());

let byte_result = FlacByteReader::endian(flac, LittleEndian)
.and_then(|mut reader| std::io::copy(&mut reader, &mut sink()).map_err(Into::into));
assert!(byte_result.is_err());

let sample_result = FlacSampleReader::new(flac).and_then(|mut reader| {
let mut samples = Vec::new();
reader.read_to_end(&mut samples).map(|_| ())
});
assert!(sample_result.is_err());

let frame_result = FrameIterator::new(flac)
.and_then(|frames| frames.collect::<Result<Vec<_>, _>>().map(|_| ()));
assert!(frame_result.is_err());
}

fuzz_target!(|data: &[u8]| {
let selector = data.first().copied().unwrap_or_default();
assert_rejected(&mutated_stream(selector));
});
76 changes: 76 additions & 0 deletions fuzz/fuzz_targets/encode_roundtrip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#![no_main]

use flac_codec::{
decode::FlacSampleReader,
encode::{FlacSampleWriter, Options},
};
use libfuzzer_sys::fuzz_target;
use std::io::Cursor;

fn sample_for_bps(value: u32, bits_per_sample: u32) -> i32 {
let shift = 32 - bits_per_sample;
(value << shift) as i32 >> shift
}

fuzz_target!(|data: &[u8]| {
let Some((&bits, data)) = data.split_first() else {
return;
};
let Some((&channel_count, data)) = data.split_first() else {
return;
};
let Some((&option, data)) = data.split_first() else {
return;
};
let Some((&block_size, data)) = data.split_first() else {
return;
};

let bits_per_sample = 1 + u32::from(bits % 32);
let channels = 1 + channel_count % 8;
let block_size = 16 + u16::from(block_size);
let pcm_frames = (data.len() / 4 / usize::from(channels)).min(128);
if pcm_frames == 0 {
return;
}

let samples = data
.chunks_exact(4)
.take(pcm_frames * usize::from(channels))
.map(|bytes| {
sample_for_bps(
u32::from_le_bytes(bytes.try_into().unwrap()),
bits_per_sample,
)
})
.collect::<Vec<_>>();
let options = match option % 3 {
0 => Options::fast(),
1 => Options::default(),
_ => Options::best(),
}
.block_size(block_size)
.unwrap()
.no_padding();

let mut encoded = Cursor::new(Vec::new());
let mut writer = FlacSampleWriter::new(
&mut encoded,
options,
44_100,
bits_per_sample,
channels,
Some(samples.len() as u64),
)
.unwrap();
writer.write(&samples).unwrap();
writer.finalize().unwrap();

let flac = encoded.into_inner();
let mut decoded = Vec::new();
FlacSampleReader::new(flac.as_slice())
.unwrap()
.read_to_end(&mut decoded)
.unwrap();
assert_eq!(decoded, samples);
});
Loading