Skip to content

Commit 2534579

Browse files
committed
Fail closed on malformed stream decode
1 parent 5b7dddd commit 2534579

5 files changed

Lines changed: 134 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@
107107
short-write regressions for encoder and decoder adapters.
108108
- Changed reader-side stream output draining to copy queued data into caller
109109
buffers in bounded slices while clearing queue slots as bytes are consumed.
110+
- Hardened stream decoders to fail closed after malformed Base64 input, with
111+
`is_failed()` state inspection and regressions for writer and reader adapters.
110112

111113
## 0.8.0 - 2026-05-16
112114

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,9 @@ including recovery readiness, pending quantum state, and fixed output queue
576576
capacity. As with other `std::io::Write` implementations, direct `write()`
577577
calls may accept only part of the provided input while buffering encoded or
578578
decoded output; use `write_all()` when the whole input slice must be consumed.
579+
Decoder writer and reader adapters fail closed after malformed Base64 input;
580+
`is_failed()` exposes that state, while unchecked `into_inner()` remains
581+
available for explicit recovery of the wrapped object.
579582

580583
URL-safe, no-padding encoding:
581584

docs/RELEASE_EVIDENCE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ The release gate runs:
117117
is retained until the wrapped writer reports bytes accepted
118118
- stream reader output queues drain into caller buffers in bounded slices while
119119
consumed queue slots are cleared
120+
- stream decoder fail-closed tests proving malformed Base64 input poisons the
121+
adapter while preserving explicit unchecked inner recovery
120122
- stream fuzz coverage for chunked writers, fragmented reader sources, and
121123
adjacent framed payload boundaries
122124
- Kani proofs through `scripts/check_kani.sh` when Kani is installed and its

src/lib.rs

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,7 @@ pub mod stream {
959959
output: OutputQueue<1024>,
960960
finished: bool,
961961
finalized: bool,
962+
failed: bool,
962963
}
963964

964965
impl<W, A, const PAD: bool> Decoder<W, A, PAD>
@@ -976,6 +977,7 @@ pub mod stream {
976977
output: OutputQueue::new(),
977978
finished: false,
978979
finalized: false,
980+
failed: false,
979981
}
980982
}
981983

@@ -1075,11 +1077,21 @@ pub mod stream {
10751077
self.finalized
10761078
}
10771079

1080+
/// Returns whether this decoder has rejected malformed Base64 input.
1081+
///
1082+
/// Once this returns `true`, later writes, flushes, and finalization
1083+
/// attempts return an error. The unchecked [`Self::into_inner`] method
1084+
/// can still be used for explicit recovery of the wrapped writer.
1085+
#[must_use]
1086+
pub const fn is_failed(&self) -> bool {
1087+
self.failed
1088+
}
1089+
10781090
/// Returns whether [`Self::try_into_inner`] can recover the wrapped
10791091
/// writer without discarding pending encoded input.
10801092
#[must_use]
10811093
pub const fn can_into_inner(&self) -> bool {
1082-
!self.has_pending_input() && !self.has_buffered_output()
1094+
!self.is_failed() && !self.has_pending_input() && !self.has_buffered_output()
10831095
}
10841096

10851097
/// Consumes the decoder without flushing pending input.
@@ -1165,6 +1177,7 @@ pub mod stream {
11651177
.field("can_into_inner", &self.can_into_inner())
11661178
.field("terminal_padding", &self.finished)
11671179
.field("finalized", &self.finalized)
1180+
.field("failed", &self.failed)
11681181
.finish()
11691182
}
11701183
}
@@ -1184,6 +1197,9 @@ pub mod stream {
11841197
/// the caller still owns the decoder for diagnostics or explicit
11851198
/// recovery.
11861199
pub fn try_finish(&mut self) -> io::Result<()> {
1200+
if self.failed {
1201+
return Err(stream_decoder_failed_error());
1202+
}
11871203
if !self.finalized {
11881204
self.queue_pending_final()?;
11891205
self.finalized = true;
@@ -1208,7 +1224,10 @@ pub mod stream {
12081224
let mut decoded = [0u8; 3];
12091225
let result = self.queue_decoded_temp(&pending[..pending_len], &mut decoded);
12101226
crate::wipe_bytes(&mut pending);
1211-
result?;
1227+
if let Err(err) = result {
1228+
self.clear_pending();
1229+
return Err(err);
1230+
}
12121231
self.clear_pending();
12131232
Ok(())
12141233
}
@@ -1229,6 +1248,7 @@ pub mod stream {
12291248
Ok(written) => written,
12301249
Err(err) => {
12311250
crate::wipe_bytes(decoded);
1251+
self.failed = true;
12321252
return Err(decode_error_to_io(err));
12331253
}
12341254
};
@@ -1275,6 +1295,9 @@ pub mod stream {
12751295
A: Alphabet,
12761296
{
12771297
fn write(&mut self, input: &[u8]) -> io::Result<usize> {
1298+
if self.failed {
1299+
return Err(stream_decoder_failed_error());
1300+
}
12781301
if input.is_empty() {
12791302
self.drain_output()?;
12801303
return Ok(0);
@@ -1287,6 +1310,7 @@ pub mod stream {
12871310
));
12881311
}
12891312
if self.finished {
1313+
self.failed = true;
12901314
return Err(trailing_input_after_padding_error());
12911315
}
12921316

@@ -1305,7 +1329,10 @@ pub mod stream {
13051329
quad[self.pending_len..].copy_from_slice(&input[..needed]);
13061330
let result = self.queue_full_quad(quad);
13071331
crate::wipe_bytes(&mut quad);
1308-
result?;
1332+
if let Err(err) = result {
1333+
self.clear_pending();
1334+
return Err(err);
1335+
}
13091336
self.clear_pending();
13101337
consumed += needed;
13111338
return Ok(consumed);
@@ -1330,6 +1357,9 @@ pub mod stream {
13301357
}
13311358

13321359
fn flush(&mut self) -> io::Result<()> {
1360+
if self.failed {
1361+
return Err(stream_decoder_failed_error());
1362+
}
13331363
self.drain_output()?;
13341364
self.inner_mut().flush()
13351365
}
@@ -1346,6 +1376,13 @@ pub mod stream {
13461376
)
13471377
}
13481378

1379+
fn stream_decoder_failed_error() -> io::Error {
1380+
io::Error::new(
1381+
io::ErrorKind::InvalidInput,
1382+
"base64 stream decoder is failed after malformed input",
1383+
)
1384+
}
1385+
13491386
/// A streaming Base64 decoder for `std::io::Read`.
13501387
///
13511388
/// For padded engines, this reader stops at the terminal padded Base64
@@ -1363,6 +1400,7 @@ pub mod stream {
13631400
output: OutputQueue<3>,
13641401
finished: bool,
13651402
terminal_seen: bool,
1403+
failed: bool,
13661404
}
13671405

13681406
impl<R, A, const PAD: bool> DecoderReader<R, A, PAD>
@@ -1380,6 +1418,7 @@ pub mod stream {
13801418
output: OutputQueue::new(),
13811419
finished: false,
13821420
terminal_seen: false,
1421+
failed: false,
13831422
}
13841423
}
13851424

@@ -1489,11 +1528,22 @@ pub mod stream {
14891528
self.finished && self.output.is_empty()
14901529
}
14911530

1531+
/// Returns whether this decoder reader has rejected malformed Base64
1532+
/// input.
1533+
///
1534+
/// Once this returns `true`, later reads return an error. The unchecked
1535+
/// [`Self::into_inner`] method can still be used for explicit recovery
1536+
/// of the wrapped reader.
1537+
#[must_use]
1538+
pub const fn is_failed(&self) -> bool {
1539+
self.failed
1540+
}
1541+
14921542
/// Returns whether [`Self::try_into_inner`] can recover the wrapped
14931543
/// reader without discarding buffered decoded output.
14941544
#[must_use]
14951545
pub const fn can_into_inner(&self) -> bool {
1496-
self.is_finished()
1546+
!self.is_failed() && self.is_finished()
14971547
}
14981548

14991549
/// Consumes the decoder reader and returns the wrapped reader.
@@ -1575,6 +1625,7 @@ pub mod stream {
15751625
.field("can_into_inner", &self.can_into_inner())
15761626
.field("finished", &self.finished)
15771627
.field("terminal_padding", &self.terminal_seen)
1628+
.field("failed", &self.failed)
15781629
.finish()
15791630
}
15801631
}
@@ -1588,6 +1639,9 @@ pub mod stream {
15881639
if output.is_empty() {
15891640
return Ok(0);
15901641
}
1642+
if self.failed {
1643+
return Err(stream_decoder_failed_error());
1644+
}
15911645

15921646
while self.output.is_empty() && !self.finished {
15931647
self.fill_output()?;
@@ -1603,6 +1657,9 @@ pub mod stream {
16031657
A: Alphabet,
16041658
{
16051659
fn fill_output(&mut self) -> io::Result<()> {
1660+
if self.failed {
1661+
return Err(stream_decoder_failed_error());
1662+
}
16061663
if self.terminal_seen {
16071664
self.finished = true;
16081665
return Ok(());
@@ -1656,6 +1713,7 @@ pub mod stream {
16561713
Ok(written) => written,
16571714
Err(err) => {
16581715
crate::wipe_bytes(&mut decoded);
1716+
self.failed = true;
16591717
return Err(decode_error_to_io(err));
16601718
}
16611719
};

tests/rfc4648.rs

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3341,14 +3341,20 @@ fn stream_decoder_try_finish_reports_bad_final_pending_input() {
33413341
let err = decoder.try_finish().unwrap_err();
33423342
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
33433343
assert!(!decoder.is_finalized());
3344-
assert_eq!(decoder.pending_len(), 1);
3345-
assert!(decoder.has_pending_input());
3344+
assert!(decoder.is_failed());
3345+
assert_eq!(decoder.pending_len(), 0);
3346+
assert!(!decoder.has_pending_input());
3347+
assert!(!decoder.can_into_inner());
33463348
assert!(decoder.get_ref().is_empty());
33473349

3348-
decoder.write_all(b"Gk=").unwrap();
3349-
decoder.try_finish().unwrap();
3350-
assert!(decoder.is_finalized());
3351-
assert_eq!(decoder.get_ref(), b"hi");
3350+
assert_eq!(
3351+
decoder.write_all(b"Gk=").unwrap_err().kind(),
3352+
std::io::ErrorKind::InvalidInput
3353+
);
3354+
assert_eq!(
3355+
decoder.try_finish().unwrap_err().kind(),
3356+
std::io::ErrorKind::InvalidInput
3357+
);
33523358
}
33533359

33543360
#[cfg(feature = "stream")]
@@ -3357,6 +3363,8 @@ fn stream_decoder_rejects_trailing_input_after_padding() {
33573363
let mut decoder = Decoder::new(Vec::new(), STANDARD);
33583364
let err = decoder.write_all(b"aGk=AA").unwrap_err();
33593365
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
3366+
assert!(decoder.is_failed());
3367+
assert!(!decoder.can_into_inner());
33603368
}
33613369

33623370
#[cfg(feature = "stream")]
@@ -3366,6 +3374,31 @@ fn stream_decoder_rejects_short_trailing_input_after_pending_padding() {
33663374
decoder.write_all(b"aG").unwrap();
33673375
let err = decoder.write_all(b"k=A").unwrap_err();
33683376
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
3377+
assert!(decoder.is_failed());
3378+
assert_eq!(decoder.pending_len(), 0);
3379+
assert!(!decoder.can_into_inner());
3380+
}
3381+
3382+
#[cfg(feature = "stream")]
3383+
#[test]
3384+
fn stream_decoder_fails_closed_after_malformed_input() {
3385+
let mut decoder = Decoder::new(Vec::new(), STANDARD);
3386+
3387+
let err = decoder.write(b"!!!!").unwrap_err();
3388+
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
3389+
assert!(decoder.is_failed());
3390+
assert!(!decoder.is_finalized());
3391+
assert_eq!(decoder.pending_len(), 0);
3392+
assert!(!decoder.can_into_inner());
3393+
3394+
assert_eq!(
3395+
decoder.write(b"aGk=").unwrap_err().kind(),
3396+
std::io::ErrorKind::InvalidInput
3397+
);
3398+
assert_eq!(
3399+
decoder.flush().unwrap_err().kind(),
3400+
std::io::ErrorKind::InvalidInput
3401+
);
33693402
}
33703403

33713404
#[cfg(feature = "stream")]
@@ -3542,6 +3575,32 @@ fn stream_decoder_reader_rejects_bad_final_pending_input() {
35423575
let mut reader = DecoderReader::new(&b"a"[..], STANDARD);
35433576
let mut decoded = Vec::new();
35443577
assert!(reader.read_to_end(&mut decoded).is_err());
3578+
assert!(reader.is_failed());
3579+
assert_eq!(reader.pending_len(), 0);
3580+
assert!(!reader.can_into_inner());
3581+
3582+
let mut output = [0u8; 1];
3583+
assert_eq!(
3584+
reader.read(&mut output).unwrap_err().kind(),
3585+
std::io::ErrorKind::InvalidInput
3586+
);
3587+
}
3588+
3589+
#[cfg(feature = "stream")]
3590+
#[test]
3591+
fn stream_decoder_reader_fails_closed_after_malformed_input() {
3592+
let mut reader = DecoderReader::new(Cursor::new(&b"!!!!aGk="[..]), STANDARD);
3593+
let mut output = [0u8; 1];
3594+
3595+
let err = reader.read(&mut output).unwrap_err();
3596+
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
3597+
assert!(reader.is_failed());
3598+
assert!(!reader.can_into_inner());
3599+
3600+
assert_eq!(
3601+
reader.read(&mut output).unwrap_err().kind(),
3602+
std::io::ErrorKind::InvalidInput
3603+
);
35453604
}
35463605

35473606
#[cfg(feature = "stream")]

0 commit comments

Comments
 (0)