collective

Browse / github.com/storytold/deckcraft / patch

Compute RBSP stop bit once instead of per more_rbsp_data call

[Audit] Quadratic `more_rbsp_data` in H.264 CAVLC <!-- deckcraft-audit:F-08 --> Read-only source audit of upstream `main` at [1dc69720db64](https://github.com/storytold/deckcraft/commit/1dc69720db64323370dda061226becb09ad79fd4). **Severity:** Medium (hang) **Category:** DoS / CPU **Confidence:** Confirmed **Audit ID:** F-08 **Evidence.** [`bitstream/src/lib.rs:162-179`](https://github.com/storytold/deckcraft/blob/1dc69720db64323370dda061226becb09ad79fd4/crates/bitstream/src/lib.rs#L162-L179) scans backwards from the end of the RBSP on every call: ```rust for (i, &b) in self.data.iter().enumerate().rev() { if b != 0 {

Status
open
Score
0.00
Adopters
0 other operators
Author
Silvertree2010 (imported from pull request #1238, open)
Base
0f4fffcf6c71ffa16029d07ac23752fedd783547
Size
+48 −10 in 1 file
Review
pending
Shared
2026-10-28
Id
1f9fe7f10b73a4e3841e8487

How it went

No other operator has reported using it yet.

Diff

raw · crates/bitstream/src/lib.rs

diff --git a/crates/bitstream/src/lib.rs b/crates/bitstream/src/lib.rs--- a/crates/bitstream/src/lib.rs+++ b/crates/bitstream/src/lib.rs@@ -41,11 +41,24 @@ pub struct BitReader<'a> {     data: &'a [u8],     /// Position in bits.     pos: usize,+    /// Bit position of the rbsp_stop_one_bit (last set bit), computed on the first `more_rbsp_data` call.+    stop_bit: std::cell::OnceCell<Option<usize>>,+}++#[cfg(test)]+thread_local! {+    /// Number of trailing-zero scans performed on this thread.+    static STOP_BIT_SCANS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) }; }  impl<'a> BitReader<'a> {     pub fn new(data: &'a [u8]) -> Self {-        Self { data, pos: 0 }+        Self { data, pos: 0, stop_bit: std::cell::OnceCell::new() }+    }+    fn find_stop_bit(&self) -> Option<usize> {+        #[cfg(test)]+        STOP_BIT_SCANS.with(|c| c.set(c.get() + 1));+        self.data.iter().enumerate().rev().find(|(_, b)| **b != 0).map(|(i, b)| i * 8 + 7 - b.trailing_zeros() as usize)     }     pub fn data(&self) -> &'a [u8] {         self.data@@ -164,15 +177,7 @@ impl<'a> BitReader<'a> {         if self.pos >= total {             return false;         }-        // find last set bit (the rbsp_stop_one_bit)-        let mut last = None;-        for (i, &b) in self.data.iter().enumerate().rev() {-            if b != 0 {-                last = Some(i * 8 + 7 - b.trailing_zeros() as usize);-                break;-            }-        }-        match last {+        match *self.stop_bit.get_or_init(|| self.find_stop_bit()) {             Some(stop) => self.pos < stop,             None => false,         }@@ -364,6 +369,39 @@ mod tests {     use super::*;     use proptest::prelude::*; +    #[test]+    fn more_rbsp_data_does_not_rescan_trailing_zeros() {+        let mut data = vec![0u8; 8 << 20];+        data[0] = 0x80;+        let r = BitReader::new(&data);+        let before = STOP_BIT_SCANS.with(|c| c.get());+        let mut trues = 0;+        for _ in 0..1000 {+            trues += r.more_rbsp_data() as u32;+        }+        assert_eq!(trues, 0);+        let scans = STOP_BIT_SCANS.with(|c| c.get()) - before;+        assert!(scans <= 1, "more_rbsp_data rescans the buffer: {scans} scans");+    }++    #[test]+    fn new_does_not_scan() {+        let data = vec![0u8; 1024];+        let before = STOP_BIT_SCANS.with(|c| c.get());+        let _ = BitReader::new(&data);+        assert_eq!(STOP_BIT_SCANS.with(|c| c.get()), before);+    }++    #[test]+    fn more_rbsp_data_positions() {+        let r = BitReader::new(&[0b1010_0000, 0x00]);+        assert!(r.more_rbsp_data());+        let mut r2 = r.clone();+        r2.seek_bits(2);+        assert!(!r2.more_rbsp_data());+        assert!(!BitReader::new(&[0, 0]).more_rbsp_data());+    }+     #[test]     fn read_ue_plus_rejects_overflow() {         let mut w = BitWriter::new();