Skip to main content

oxideav_core/
limits.rs

1//! Decoder DoS-protection limits.
2//!
3//! [`DecoderLimits`] is a small `Copy + Default` configuration struct
4//! threaded through [`CodecParameters`](crate::CodecParameters) so every
5//! decoder constructed from a stream sees the same caps. Each cap is a
6//! conservative default chosen to be generous enough that no real-world
7//! file trips it but tight enough that a malicious input (huge declared
8//! dimensions in a tiny container, decompression bombs, etc.) returns
9//! [`Error::ResourceExhausted`](crate::Error::ResourceExhausted) instead
10//! of OOM-ing the process.
11//!
12//! Two layers consume these caps:
13//!
14//! 1. **Header-parse layer.** Every decoder, immediately after parsing
15//!    a stream/sequence header that declares dimensions, channel/group
16//!    counts, or sample-rate × duration products, must check those
17//!    declared values against [`DecoderLimits::max_pixels_per_frame`] /
18//!    [`DecoderLimits::max_decoded_audio_seconds_per_packet`] *before*
19//!    any allocation. A 1 GiB declared frame in a 4 KiB file should
20//!    error here without ever calling `Vec::with_capacity`.
21//!
22//! 2. **Arena layer.** [`ArenaPool`](crate::arena::ArenaPool) honours
23//!    [`DecoderLimits::max_arenas_in_flight`] (pool size) and
24//!    [`DecoderLimits::max_alloc_bytes_per_frame`] (arena capacity).
25//!    [`DecoderLimits::max_alloc_count_per_frame`] catches small-alloc
26//!    DoS where each individual allocation is tiny but the count grows
27//!    unbounded (e.g. one alloc per macroblock × millions of macroblocks).
28//!
29//! The struct is `Copy` so threading it through call chains never
30//! involves clones or refcounts. It is also `#[non_exhaustive]` so
31//! additional caps can be added without a semver break — construct
32//! defaults with [`DecoderLimits::default`] and use the builder methods
33//! to tighten individual fields.
34
35/// Caps that bound a single decoder's peak resource use.
36///
37/// Defaults are intentionally **generous** (32 k × 32 k pixels, 1 GiB
38/// per arena, 60 s of decoded audio per packet, …) so existing
39/// real-world media decodes unchanged. Callers wanting tighter bounds
40/// (e.g. a server processing untrusted uploads) should construct
41/// `DecoderLimits` explicitly with the builder methods.
42///
43/// `Copy` and `Default` so the struct travels through hot paths
44/// without indirection. `#[non_exhaustive]` so future caps can be
45/// added without breaking semver — use [`DecoderLimits::default`] and
46/// the `with_*` builder methods rather than struct-literal syntax.
47#[non_exhaustive]
48#[derive(Copy, Clone, Debug, PartialEq, Eq)]
49pub struct DecoderLimits {
50    /// Hard cap on `width × height` for a single decoded video frame.
51    /// Header-parse code computes this product (using `u64` to avoid
52    /// `u32::MAX × u32::MAX` overflow) and compares against this cap
53    /// before allocating any plane. Default: `32_768 × 32_768` =
54    /// `1_073_741_824` pixels (4 GiB at 32-bpp / 1 GiB at 8-bpp).
55    pub max_pixels_per_frame: u64,
56
57    /// Hard cap on the total bytes any single decoded frame may
58    /// consume across all of its plane allocations. Also defines the
59    /// per-arena capacity — see
60    /// [`crate::arena::ArenaPool::new`]. Default: `1 GiB`. Tighter
61    /// than `max_pixels_per_frame × bytes_per_pixel` for catching
62    /// pathological pixel formats (e.g. a 16-bit-per-channel RGBA
63    /// surface at near-cap dimensions).
64    pub max_alloc_bytes_per_frame: u64,
65
66    /// Hard cap on the *count* of allocations performed inside a
67    /// single arena, regardless of total bytes. Catches small-alloc
68    /// DoS (e.g. one alloc per macroblock × millions of macroblocks
69    /// where the bytes-per-frame check would be too loose to fire).
70    /// Default: `1_000_000` allocations.
71    pub max_alloc_count_per_frame: u32,
72
73    /// Hard cap on how many arenas a single decoder may have in
74    /// flight at once — i.e. the size of the per-decoder
75    /// [`ArenaPool`](crate::arena::ArenaPool). When all arenas are
76    /// checked out the next `lease()` returns
77    /// [`Error::ResourceExhausted`](crate::Error::ResourceExhausted),
78    /// providing automatic backpressure: a slow downstream consumer
79    /// stalls the decoder rather than letting it grow memory
80    /// unboundedly. Default: `8` arenas.
81    pub max_arenas_in_flight: u8,
82
83    /// Audio-only cap on the wall-clock duration (in seconds) of
84    /// decoded samples a single packet may produce. Header-parse
85    /// code computes `(samples_per_frame × frames_per_packet) /
86    /// sample_rate` and rejects packets whose declared output
87    /// exceeds this. Default: `60` seconds — far more than any
88    /// real-world AAC/Opus/etc. packet would ever produce, but
89    /// finite enough to refuse a malformed packet that claims
90    /// hours of output.
91    pub max_decoded_audio_seconds_per_packet: u32,
92}
93
94impl Default for DecoderLimits {
95    fn default() -> Self {
96        Self {
97            max_pixels_per_frame: 32_768u64 * 32_768u64,
98            max_alloc_bytes_per_frame: 1u64 << 30, // 1 GiB
99            max_alloc_count_per_frame: 1_000_000,
100            max_arenas_in_flight: 8,
101            max_decoded_audio_seconds_per_packet: 60,
102        }
103    }
104}
105
106impl DecoderLimits {
107    /// Tighten the per-frame pixel cap. See
108    /// [`DecoderLimits::max_pixels_per_frame`].
109    pub fn with_max_pixels_per_frame(mut self, n: u64) -> Self {
110        self.max_pixels_per_frame = n;
111        self
112    }
113
114    /// Tighten the per-frame allocation byte cap (also defines arena
115    /// capacity). See [`DecoderLimits::max_alloc_bytes_per_frame`].
116    pub fn with_max_alloc_bytes_per_frame(mut self, n: u64) -> Self {
117        self.max_alloc_bytes_per_frame = n;
118        self
119    }
120
121    /// Tighten the per-frame allocation count cap. See
122    /// [`DecoderLimits::max_alloc_count_per_frame`].
123    pub fn with_max_alloc_count_per_frame(mut self, n: u32) -> Self {
124        self.max_alloc_count_per_frame = n;
125        self
126    }
127
128    /// Tighten the per-decoder pool size. See
129    /// [`DecoderLimits::max_arenas_in_flight`].
130    pub fn with_max_arenas_in_flight(mut self, n: u8) -> Self {
131        self.max_arenas_in_flight = n;
132        self
133    }
134
135    /// Tighten the per-packet decoded-audio duration cap. See
136    /// [`DecoderLimits::max_decoded_audio_seconds_per_packet`].
137    pub fn with_max_decoded_audio_seconds_per_packet(mut self, n: u32) -> Self {
138        self.max_decoded_audio_seconds_per_packet = n;
139        self
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn defaults_are_conservative_but_finite() {
149        let l = DecoderLimits::default();
150        // 32k x 32k pixels.
151        assert_eq!(l.max_pixels_per_frame, 1_073_741_824);
152        // 1 GiB per arena.
153        assert_eq!(l.max_alloc_bytes_per_frame, 1u64 << 30);
154        // 1M allocations per frame.
155        assert_eq!(l.max_alloc_count_per_frame, 1_000_000);
156        // 8 arenas in flight.
157        assert_eq!(l.max_arenas_in_flight, 8);
158        // 60 s of decoded audio per packet.
159        assert_eq!(l.max_decoded_audio_seconds_per_packet, 60);
160    }
161
162    #[test]
163    fn builder_methods_compose() {
164        let l = DecoderLimits::default()
165            .with_max_pixels_per_frame(1024 * 1024)
166            .with_max_alloc_bytes_per_frame(8 * 1024 * 1024)
167            .with_max_alloc_count_per_frame(1024)
168            .with_max_arenas_in_flight(2)
169            .with_max_decoded_audio_seconds_per_packet(1);
170        assert_eq!(l.max_pixels_per_frame, 1024 * 1024);
171        assert_eq!(l.max_alloc_bytes_per_frame, 8 * 1024 * 1024);
172        assert_eq!(l.max_alloc_count_per_frame, 1024);
173        assert_eq!(l.max_arenas_in_flight, 2);
174        assert_eq!(l.max_decoded_audio_seconds_per_packet, 1);
175    }
176
177    #[test]
178    fn copy_semantics() {
179        let a = DecoderLimits::default();
180        let b = a; // would not compile if not Copy
181        assert_eq!(a, b);
182    }
183}