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}