From 3f46d299ec58d6278ff664825bb8c3b662aecca6 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Mon, 13 Jul 2026 00:45:54 +0000 Subject: [PATCH 1/2] age: Return error if LimitedReader is given an oversized file The previous behaviour exactly matched Go's `io.LimitReader`, which the Go age impl uses for its own bounds. However, Go age then manually transforms the max-read case into an error. We instead fix `LimitedReader` to explicitly error on oversized files. Closes str4d/rage#616. --- age/CHANGELOG.md | 3 ++ age/src/util.rs | 86 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/age/CHANGELOG.md b/age/CHANGELOG.md index e725ec1e..b567e653 100644 --- a/age/CHANGELOG.md +++ b/age/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to Rust's notion of to 1.0.0 are beta releases. ## [Unreleased] +### Fixed +- Parsing of recipient and identity files now returns an error if the input is + larger than the limit, instead of silently truncating. ## [0.11.3] - 2026-04-22 ### Changed diff --git a/age/src/util.rs b/age/src/util.rs index c4d70b47..aa94324f 100644 --- a/age/src/util.rs +++ b/age/src/util.rs @@ -20,17 +20,40 @@ pub(crate) fn parse_bech32(s: &str) -> Option<(String, Vec)> { pub(crate) struct LimitedReader { inner: R, n: usize, + limit_exceeded: bool, } impl LimitedReader { pub(crate) fn new(reader: R, n: usize) -> Self { - Self { inner: reader, n } + Self { + inner: reader, + n, + limit_exceeded: false, + } + } + + fn limit_exceeded() -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, "reader exceeded size limit") } } impl io::Read for LimitedReader { fn read(&mut self, mut buf: &mut [u8]) -> io::Result { + if buf.is_empty() { + return Ok(0); + } + + if self.limit_exceeded { + return Err(Self::limit_exceeded()); + } + if self.n == 0 { - Ok(0) + let mut probe = [0]; + if self.inner.read(&mut probe)? == 0 { + Ok(0) + } else { + self.limit_exceeded = true; + Err(Self::limit_exceeded()) + } } else { if buf.len() > self.n { buf = &mut buf[..self.n]; @@ -44,8 +67,17 @@ impl io::Read for LimitedReader { impl io::BufRead for LimitedReader { fn fill_buf(&mut self) -> io::Result<&[u8]> { + if self.limit_exceeded { + return Err(Self::limit_exceeded()); + } + if self.n == 0 { - Ok(&[]) + if self.inner.fill_buf()?.is_empty() { + Ok(&[]) + } else { + self.limit_exceeded = true; + Err(Self::limit_exceeded()) + } } else { let buf = self.inner.fill_buf()?; Ok(&buf[..buf.len().min(self.n)]) @@ -58,6 +90,54 @@ impl io::BufRead for LimitedReader { } } +#[cfg(test)] +mod tests { + use super::LimitedReader; + use std::io::{self, BufRead, Cursor, Read}; + + #[test] + fn limited_reader_read() { + for (input, limit) in [(&b"abc"[..], 4), (&b"abc"[..], 3)] { + let mut output = vec![]; + LimitedReader::new(input, limit) + .read_to_end(&mut output) + .unwrap(); + assert_eq!(output, input); + } + + let mut output = vec![]; + let mut reader = LimitedReader::new(&b"abcd"[..], 3); + let err = reader.read_to_end(&mut output).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + assert_eq!(output, b"abc"); + assert_eq!( + reader.read(&mut [0]).unwrap_err().kind(), + io::ErrorKind::InvalidData + ); + } + + #[test] + fn limited_reader_bufread() { + for (input, limit) in [(&b"abc"[..], 4), (&b"abc"[..], 3)] { + let mut output = vec![]; + LimitedReader::new(Cursor::new(input), limit) + .read_until(b'\n', &mut output) + .unwrap(); + assert_eq!(output, input); + } + + let mut output = vec![]; + let mut reader = LimitedReader::new(Cursor::new(&b"abcd"[..]), 3); + let err = reader.read_until(b'\n', &mut output).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + assert_eq!(output, b"abc"); + assert_eq!( + reader.fill_buf().unwrap_err().kind(), + io::ErrorKind::InvalidData + ); + } +} + pub(crate) mod read { use std::str::FromStr; From 2f5c9cdeb2d79873ca32645b1fb106b140f4e292 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Mon, 13 Jul 2026 00:49:40 +0000 Subject: [PATCH 2/2] age 0.11.4 --- Cargo.lock | 2 +- age/CHANGELOG.md | 2 ++ age/Cargo.toml | 2 +- fuzz-afl/Cargo.lock | 2 +- fuzz/Cargo.lock | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb081d7f..3a0d29c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,7 +60,7 @@ dependencies = [ [[package]] name = "age" -version = "0.11.3" +version = "0.11.4" dependencies = [ "aes", "aes-gcm", diff --git a/age/CHANGELOG.md b/age/CHANGELOG.md index b567e653..25bf4139 100644 --- a/age/CHANGELOG.md +++ b/age/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to Rust's notion of to 1.0.0 are beta releases. ## [Unreleased] + +## [0.11.4] - 2026-07-13 ### Fixed - Parsing of recipient and identity files now returns an error if the input is larger than the limit, instead of silently truncating. diff --git a/age/Cargo.toml b/age/Cargo.toml index a3f7312b..ac012a53 100644 --- a/age/Cargo.toml +++ b/age/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "age" description = "[BETA] A simple, secure, and modern encryption library." -version = "0.11.3" +version = "0.11.4" authors.workspace = true repository.workspace = true readme = "README.md" diff --git a/fuzz-afl/Cargo.lock b/fuzz-afl/Cargo.lock index 8622c914..1f10b8d2 100644 --- a/fuzz-afl/Cargo.lock +++ b/fuzz-afl/Cargo.lock @@ -26,7 +26,7 @@ dependencies = [ [[package]] name = "age" -version = "0.11.0" +version = "0.11.4" dependencies = [ "age-core", "base64", diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 225219d4..26b6f8f3 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -14,7 +14,7 @@ dependencies = [ [[package]] name = "age" -version = "0.11.0" +version = "0.11.4" dependencies = [ "age-core", "base64",