Skip to content

Commit 9aa541d

Browse files
authored
block-buffer: fix exception safety (#1487)
Users may trigger UB by observing broken type invariant using panicking closures and `catch_unwind`. This PR introduces guards which reset the buffer types when dealing with user-provided closures.
1 parent 5c7e4f9 commit 9aa541d

5 files changed

Lines changed: 174 additions & 19 deletions

File tree

.github/workflows/block-buffer.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,19 @@ jobs:
6464
toolchain: ${{ matrix.rust }}
6565
- run: cargo test
6666
- run: cargo test --all-features
67+
68+
miri:
69+
runs-on: ubuntu-latest
70+
strategy:
71+
matrix:
72+
target:
73+
- x86_64-unknown-linux-gnu
74+
- s390x-unknown-linux-gnu
75+
steps:
76+
- uses: actions/checkout@v6
77+
- uses: RustCrypto/actions/cargo-cache@master
78+
- uses: dtolnay/rust-toolchain@master
79+
with:
80+
toolchain: nightly
81+
components: miri
82+
- run: cargo miri test --all-features --target ${{ matrix.target }}

block-buffer/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## 0.12.1 (UNRELEASED)
8+
### Fixed
9+
- Exception safety bug ([#1487])
10+
11+
[#1487]: https://github.com/RustCrypto/utils/pull/1487
12+
713
## 0.12.0 (2026-02-24)
814
### Added
915
- `BlockSizes` trait implemented for sizes bigger than `U0` and smaller than `U256` ([#1455])

block-buffer/src/lib.rs

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,18 @@ impl<BS: BlockSizes, K: BufferKind> BlockBuffer<BS, K> {
184184
if pos != 0 {
185185
let (left, right) = input.split_at(rem);
186186
input = right;
187+
188+
let g = ResetGuard(self);
189+
let buf = &mut g.0.buffer;
187190
// SAFETY: length of `left` is equal to number of remaining bytes in `buffer`,
188191
// so we can copy data into it and process `buffer` as fully initialized block.
192+
// Note that this code can temporarily break the eager buffer invariant,
193+
// but we reset the buffer immediately after `compress` using `Drop` impl of
194+
// `ResetGuard`, so this code is safe even if `compress` panics.
189195
let block = unsafe {
190-
let buf_ptr = self.buffer.as_mut_ptr().cast::<u8>().add(pos);
196+
let buf_ptr = buf.as_mut_ptr().cast::<u8>().add(pos);
191197
ptr::copy_nonoverlapping(left.as_ptr(), buf_ptr, left.len());
192-
self.buffer.assume_init_ref()
198+
buf.assume_init_ref()
193199
};
194200
compress(slice::from_ref(block));
195201
}
@@ -363,22 +369,34 @@ impl<BS: BlockSizes> BlockBuffer<BS, Eager> {
363369
suffix: &[u8],
364370
mut compress: impl FnMut(&Array<u8, BS>),
365371
) {
366-
assert!(suffix.len() <= BS::USIZE, "suffix is too long");
367372
let pos = self.get_pos();
368-
let mut buf = self.pad_with_zeros();
369-
buf[pos] = delim;
370-
371-
let n = self.size() - suffix.len();
372-
if self.size() - pos - 1 < suffix.len() {
373-
compress(&buf);
373+
let size = self.size();
374+
// Number of bytes remaining in the buffer after `delim` is written to it
375+
// This never underflows since for eager buffers `size` is always greater than `pos`.
376+
let pad_len = size - pos - 1;
377+
378+
let suffix_dst_pos = size
379+
.checked_sub(suffix.len())
380+
.expect("suffix must be smaller than buffer block size");
381+
382+
let g = ResetGuard(self);
383+
// SAFETY: we fully initialize the buffer. Note that we may temporarily break
384+
// the buffer invariant, but we restore it using `ResetGuard`,
385+
// which works even if `compress` panics.
386+
let buf = unsafe {
387+
let p: *mut u8 = g.0.buffer.as_mut_ptr().cast::<u8>().add(pos);
388+
ptr::write(p, delim);
389+
ptr::write_bytes(p.add(1), 0, pad_len);
390+
g.0.buffer.assume_init_mut()
391+
};
392+
393+
if pad_len < suffix.len() {
394+
compress(buf);
374395
buf.fill(0);
375-
buf[n..].copy_from_slice(suffix);
376-
compress(&buf);
377-
} else {
378-
buf[n..].copy_from_slice(suffix);
379-
compress(&buf);
380396
}
381-
self.reset();
397+
398+
buf[suffix_dst_pos..].copy_from_slice(suffix);
399+
compress(buf);
382400
}
383401

384402
/// Pad message with 0x80, zeros and 64-bit message length using
@@ -422,3 +440,12 @@ impl<BS: BlockSizes, K: BufferKind> Drop for BlockBuffer<BS, K> {
422440

423441
#[cfg(feature = "zeroize")]
424442
impl<BS: BlockSizes, K: BufferKind> ZeroizeOnDrop for BlockBuffer<BS, K> {}
443+
444+
/// Resets the referenced buffer on drop.
445+
struct ResetGuard<'a, BS: BlockSizes, K: BufferKind>(&'a mut BlockBuffer<BS, K>);
446+
447+
impl<BS: BlockSizes, K: BufferKind> Drop for ResetGuard<'_, BS, K> {
448+
fn drop(&mut self) {
449+
self.0.reset();
450+
}
451+
}

block-buffer/src/read.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,16 @@ impl<BS: BlockSizes> ReadBuffer<BS> {
110110
}
111111
assert!(read_len < BS::USIZE);
112112

113-
gen_block(&mut self.buffer);
114-
read_fn(&self.buffer[..read_len]);
113+
let g = ResetGuard(self);
114+
let buf = &mut g.0.buffer;
115+
116+
// Note that generated block is likely to break the `ReadBuffer` invariant.
117+
// We restore it using `set_pos_unchecked` below and in case if one of the closures
118+
// panic the buffer gets reset by the guard.
119+
gen_block(buf);
120+
read_fn(&buf[..read_len]);
121+
122+
core::mem::forget(g);
115123

116124
// We checked that `read_len` satisfies the `set_pos_unchecked` safety contract
117125
unsafe { self.set_pos_unchecked(read_len) };
@@ -180,3 +188,12 @@ impl<BS: BlockSizes> Drop for ReadBuffer<BS> {
180188

181189
#[cfg(feature = "zeroize")]
182190
impl<BS: BlockSizes> zeroize::ZeroizeOnDrop for ReadBuffer<BS> {}
191+
192+
/// Resets the referenced buffer on drop.
193+
struct ResetGuard<'a, BS: BlockSizes>(&'a mut ReadBuffer<BS>);
194+
195+
impl<BS: BlockSizes> Drop for ResetGuard<'_, BS> {
196+
fn drop(&mut self) {
197+
self.0.reset();
198+
}
199+
}

block-buffer/tests/mod.rs

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ use block_buffer::{
99
};
1010
use hex_literal::hex;
1111

12+
use core::{array, panic::AssertUnwindSafe};
13+
use std::panic::catch_unwind;
14+
1215
#[test]
13-
fn test_eager_digest_pad() {
16+
fn test_eager_digest() {
1417
let mut buf = EagerBuffer::<U4>::default();
1518
let inputs = [
1619
&b"01234567"[..],
@@ -45,7 +48,7 @@ fn test_eager_digest_pad() {
4548
}
4649

4750
#[test]
48-
fn test_lazy_digest_pad() {
51+
fn test_lazy_digest() {
4952
let mut buf = LazyBuffer::<U4>::default();
5053
let inputs = [
5154
&b"01234567"[..],
@@ -78,6 +81,38 @@ fn test_lazy_digest_pad() {
7881
assert_eq!(buf.get_pos(), 0);
7982
}
8083

84+
#[test]
85+
fn digest_pad_combinations() {
86+
let delim = 0x80;
87+
let data: [u8; 7] = array::from_fn(|i| u8::try_from(i).unwrap());
88+
let suffix: [u8; 7] = array::from_fn(|i| u8::try_from(i + 0x10).unwrap());
89+
90+
for data_len in 0..data.len() {
91+
for suffix_len in 0..suffix.len() {
92+
let data = &data[..data_len];
93+
let suffix = &suffix[..suffix_len];
94+
let mut buf = EagerBuffer::<U8>::default();
95+
96+
buf.digest_blocks(data, |_| panic!("should not be called"));
97+
98+
let mut accum = Vec::with_capacity(2 * buf.size());
99+
buf.digest_pad(delim, suffix, |block| accum.extend_from_slice(block));
100+
101+
assert!(accum.len() <= 2 * buf.size());
102+
assert_eq!(buf.get_pos(), 0);
103+
104+
let (res_data, rem) = accum.split_at(data_len);
105+
let (res_delim, rem) = rem.split_at(1);
106+
let (res_zeros, res_suffix) = rem.split_at(rem.len() - suffix_len);
107+
108+
assert_eq!(res_data, data);
109+
assert_eq!(res_delim, &[delim]);
110+
assert!(res_zeros.iter().all(|&b| b == 0));
111+
assert_eq!(res_suffix, suffix);
112+
}
113+
}
114+
}
115+
81116
#[test]
82117
fn test_read() {
83118
type Buf = ReadBuffer<U4>;
@@ -344,3 +379,57 @@ fn test_read_serialize() {
344379
let buf = Array([4, 0, 0, 1]);
345380
assert!(Buf::deserialize(&buf).is_err());
346381
}
382+
383+
#[test]
384+
fn eager_buffer_exception_safety() {
385+
let mut buf = EagerBuffer::<U4>::default();
386+
387+
let res = catch_unwind(AssertUnwindSafe(|| {
388+
buf.digest_blocks(b"ab", |_| {});
389+
buf.digest_blocks(b"cd", |_| panic!("compression panic"));
390+
}));
391+
392+
assert!(res.is_err());
393+
let _ = buf.get_pos();
394+
395+
let mut buf = EagerBuffer::<U4>::default();
396+
397+
let res = catch_unwind(AssertUnwindSafe(|| {
398+
buf.digest_pad(0x80, &[0xFF; 2], |_| panic!("compression panic"));
399+
}));
400+
401+
assert!(res.is_err());
402+
let _ = buf.get_pos();
403+
}
404+
405+
#[test]
406+
fn read_buffer_exception_safety() {
407+
let mut buf = ReadBuffer::<U4>::default();
408+
409+
let res = catch_unwind(AssertUnwindSafe(|| {
410+
buf.write_block(
411+
1,
412+
|block| {
413+
block[0] = 0xFF;
414+
panic!("block generation panic");
415+
},
416+
|_| {},
417+
);
418+
}));
419+
420+
assert!(res.is_err());
421+
let _ = buf.get_pos();
422+
423+
let mut buf = ReadBuffer::<U4>::default();
424+
425+
let res = catch_unwind(AssertUnwindSafe(|| {
426+
buf.write_block(
427+
1,
428+
|block| block.0 = [0xFF; 4],
429+
|_| panic!("data read panic"),
430+
);
431+
}));
432+
433+
assert!(res.is_err());
434+
let _ = buf.get_pos();
435+
}

0 commit comments

Comments
 (0)