Skip to content

Commit 9d9c71e

Browse files
committed
fix(decoder): heap-allocate OggVorbis_File to prevent UAF on decoder move
Issue #35 reported a use-after-free bug after decoding a sample block with the high-level `VorbisDecoder` struct, and then moving it, due to a faulty assumption on our bindings that the internal `OggVorbis_File` struct was safe to move alongside the `VorbisDecoder`. I've independently verified that the issue is real and that the root cause analysis outlined in #35 by the issue reporter is correct. Given such root cause, `Box`ing the underlying `OggVorbis_File` is the only proper fix for the problem, short of rewriting `libvorbis` internals. As a part of the fix and investigation for this issue, I came up with a regression test for it. Fixes #35.
1 parent bddcf74 commit 9d9c71e

3 files changed

Lines changed: 71 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ and this project adheres to
1717

1818
- Documentation on how to use `vorbis_rs` with WebAssembly targets was added.
1919

20+
### Fixed
21+
22+
- Resolved a use-after-free memory safety issue caused by moving a
23+
`VorbisDecoder` after decoding a block of samples. (#35, thanks @EriKWDev)
24+
2025
## [0.5.5] - 2024-12-13
2126

2227
### Changed

packages/vorbis_rs/src/decoder/decoder_impl.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::{common::VorbisError, decoder::VorbisAudioSamples};
1919
/// A decoder that turns a perceptually-encoded, non-chained Ogg Vorbis stream into
2020
/// blocks of planar, single-precision float audio samples.
2121
pub struct VorbisDecoder<R: Read> {
22-
ogg_vorbis_file: OggVorbis_File,
22+
ogg_vorbis_file: Box<OggVorbis_File>,
2323
source: PhantomData<R>,
2424
last_audio_block: Option<VorbisAudioSamples>
2525
}
@@ -33,14 +33,17 @@ impl<R: Read> VorbisDecoder<R> {
3333
/// I/O errors that might happen during that operation will be returned to the
3434
/// caller.
3535
pub fn new<S: Into<Box<R>>>(source: S) -> Result<Self, VorbisError> {
36-
let mut ogg_vorbis_file = MaybeUninit::uninit();
37-
3836
// The source read needs to be allocated in the heap (i.e., boxed) to have a
3937
// constant memory address. Then leak it to a raw pointer to hand its ownership
4038
// over to C code. Related, interesting read about trait objects and FFI:
4139
// https://adventures.michaelfbryan.com/posts/ffi-safe-polymorphism-in-rust/
4240
let source = Box::into_raw(source.into());
4341

42+
// The underlying `OggVorbis_File` struct also needs to have a constant memory address because
43+
// it stores the vorbis_dsp_state and vorbis_block states by value, and the latter stores a
44+
// pointer to the former that would be invalidated on move
45+
let mut ogg_vorbis_file = Box::new_uninit();
46+
4447
// SAFETY: we assume ov_open_callbacks follows its documented contract
4548
unsafe {
4649
match vorbisfile_return_value_to_result!(ov_open_callbacks(
@@ -120,7 +123,7 @@ impl<R: Read> VorbisDecoder<R> {
120123
// VorbisAudioSamples implementation for more safety information
121124
unsafe {
122125
let samples_read = vorbisfile_return_value_to_result!(ov_read_float(
123-
&mut self.ogg_vorbis_file,
126+
&mut *self.ogg_vorbis_file,
124127
sample_buf.as_mut_ptr(),
125128
2048, // Most stereo Ogg Vorbis files in the wild use a maximum block size of 2048 samples
126129
current_bitstream.as_mut_ptr()
@@ -164,7 +167,7 @@ impl<R: Read> VorbisDecoder<R> {
164167

165168
impl<R: Read> Drop for VorbisDecoder<R> {
166169
fn drop(&mut self) {
167-
unsafe { ov_clear(&mut self.ogg_vorbis_file) };
170+
unsafe { ov_clear(&mut *self.ogg_vorbis_file) };
168171
}
169172
}
170173

packages/vorbis_rs/tests/regression_tests.rs

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
//! Tests for ensuring past issues are kept fixed.
2-
3-
use std::num::{NonZeroU8, NonZeroU32};
2+
use std::{
3+
mem::{self, MaybeUninit},
4+
num::{NonZeroU8, NonZeroU32},
5+
slice
6+
};
47

58
use vorbis_rs::{VorbisDecoder, VorbisEncoderBuilder};
69

@@ -56,3 +59,56 @@ fn issue_17() {
5659
"The number of encoded samples must match the number of decoded samples"
5760
);
5861
}
62+
63+
#[doc = concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/35")]
64+
#[test]
65+
fn issue_35() {
66+
// To reliably showcase this issue about use-after-free due to a move, we need to store the
67+
// decoder at a memory address where we can loudly invalidate any reads made to the address it
68+
// was at before the move. A stack-allocated variable is inconvenient for this because the stack
69+
// frames of any called function stay valid while they are part of the call stack, and the
70+
// specifics of how their storage gets reused for new stack frames or variables is
71+
// platform-specific. Therefore, we use a heap allocation through a `Vec` (see comments below
72+
// for why this instead of, e.g., a `Box`)
73+
let mut heap_decoder_buf = vec![
74+
VorbisDecoder::<&[u8]>::new(
75+
&include_bytes!(
76+
"../../aotuv_lancer_vorbis_sys/src/8khz_500ms_mono_400hz_sine_wave.ogg"
77+
)[..]
78+
)
79+
.unwrap(),
80+
];
81+
82+
// Ensure all decoder state is initialized, which includes pointers in C structs
83+
heap_decoder_buf[0].decode_audio_block().unwrap();
84+
85+
// Now that we have a fully initialized decoder on a heap-backed `Vec`, move it out of such `Vec`.
86+
// Per Rust's move semantics, this effectively `memcpy`s the decoder to a memory address
87+
// somewhere on the stack, rendering the previous heap address it was located at free for use by
88+
// any other `Vec` elements, or released back to the OS if the backing buffer is freed or
89+
// reallocated. Because we don't do the later by this point, the `Vec` buffer has capacity
90+
// allocated and likely storing a copy of the single element we had in it (this would not
91+
// necessarily be the case with a `Box`), so we can alter the memory representation of that copy
92+
// through spare `Vec` capacity accessors (the `unsafe` block that follows is a backport to
93+
// stable Rust of nightly Rust's MaybeUninit::as_bytes_mut`) to garbage bytes. In turn, the
94+
// garbage bytes chosen practically guarantee that libvorbis attempts to reference invalid
95+
// memory when any of its internal structs still refer to the freed, stale copy of the struct.
96+
// TODO: replace the `unsafe` below with `MaybeUninit::as_bytes_mut` once it gets stabilized
97+
let mut stack_decoder = heap_decoder_buf.remove(0);
98+
for byte in unsafe {
99+
slice::from_raw_parts_mut(
100+
heap_decoder_buf.spare_capacity_mut()[0]
101+
.as_mut_ptr()
102+
.cast::<MaybeUninit<u8>>(),
103+
mem::size_of::<VorbisDecoder<&[u8]>>()
104+
)
105+
} {
106+
byte.write(0xCA); // Neither 0 nor 0xFF to avoid interpretation as sentinel values
107+
}
108+
109+
// After the setup above, when affected by the use-after-free due to a move bug this will reliably
110+
// cause an invalid memory address to be accessed, aborting the process with SIGSEGV on
111+
// Unix-like systems or he STATUS_ACCESS_VIOLATION structured exception on Windows, and fail the
112+
// test
113+
stack_decoder.decode_audio_block().ok();
114+
}

0 commit comments

Comments
 (0)