Skip to content

fix(sdk/ble): bound BLE reassembly pre-allocation to chunk count#428

Open
dsmfa10 wants to merge 1 commit into
mainfrom
fix/sdk-ble-reassembly-alloc-bound
Open

fix(sdk/ble): bound BLE reassembly pre-allocation to chunk count#428
dsmfa10 wants to merge 1 commit into
mainfrom
fix/sdk-ble-reassembly-alloc-bound

Conversation

@dsmfa10
Copy link
Copy Markdown
Collaborator

@dsmfa10 dsmfa10 commented Jun 3, 2026

Problem

ReassemblyBuffer::reassemble pre-allocates the output buffer from
expected_size, which is the peer-supplied payload_len (u32, read from the
chunk header). A single chunk with total_chunks = 1 and a declared
payload_len of ~4 GiB makes is_complete() true and triggers a multi-gigabyte
Vec::with_capacity before the integrity and size checks run — a memory
exhaustion DoS from one crafted frame.

Evidence

// src/bluetooth/ble_frame_coordinator.rs (before)
impl ReassemblyBuffer {
    fn new(header: &BleFrameHeader, ...) -> Self {
        Self { ..., total_chunks: header.total_chunks as u16, expected_size: header.payload_len, ... }
    }
    pub fn reassemble(self) -> Result<Vec<u8>, DsmError> {
        let mut result = Vec::with_capacity(self.expected_size as usize);   // <-- peer-controlled, unbounded
        for i in 0..self.total_chunks { /* extend from received chunks */ }
        if result.len() != self.expected_size as usize { return Err(...); }  // size only checked AFTER alloc
        ...
    }
}

MAX_BLE_CHUNK_SIZE = 400, so a real payload can be at most
total_chunks * 400 bytes — far smaller than the u32 payload_len an attacker
can claim.

Fix

Cap the pre-allocation hint to what the chunks could actually hold. The exact
length is still validated against expected_size afterward, so correctness is
unchanged; this only prevents the oversized speculative allocation.

let capacity_cap = (self.total_chunks as usize).saturating_mul(MAX_BLE_CHUNK_SIZE);
let mut result = Vec::with_capacity((self.expected_size as usize).min(capacity_cap));

Verification

cargo check -p dsm_sdk clean. The post-reassembly result.len() != expected_size
check and the BLAKE3 frame-commitment integrity check are untouched, so a frame
that lied about payload_len still fails — it just can no longer force a giant
allocation first.

Notes

A secondary observation from the audit (not fixed here): total_chunks/
chunk_index are truncated u32 -> u16; out-of-range values are accepted then
fail integrity rather than being rejected up front. Low impact; left as-is.

CI gates & coverage

Full verification for this PR runs in CI — Rust (cargo fmt --check,
clippy -D warnings, workspace tests), Frontend, Android Unit Tests,
Coverage, SPDX headers, CodeQL (see the PR's Checks tab). The local
check noted above is a subset; the broader mandated gates (full workspace test
suite, codegen/scan, Android, Frontend) run in CI, not locally — none is
silently skipped.

`ReassemblyBuffer::reassemble` pre-allocated `Vec::with_capacity(expected_size)`
where `expected_size` is the peer-supplied `payload_len` (u32). A single
small chunk (`total_chunks = 1`) declaring a ~4 GiB `payload_len` made the
buffer "complete" and triggered a multi-gigabyte allocation before the
integrity and size checks ran — a memory-exhaustion DoS from one frame.

Cap the capacity hint at `total_chunks * MAX_BLE_CHUNK_SIZE` (the maximum
the chunks could actually hold). The exact reassembled length is still
validated against `expected_size` afterward, so correctness is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants