Severity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Primary rating from Vendor (https://github.com/Eugeny/russh) · only source for this CVE.
CVSS VectorVendor: https://github.com/Eugeny/russh
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Lifecycle Timeline
2DescriptionCVE.org
Title
Unchecked CryptoVec allocation and growth handling was reachable from local agent inputs in current russh releases and from remote SSH traffic in historical pre-0.58.0 releases
Summary
CryptoVec used unchecked capacity growth, unchecked length arithmetic, and unsafe allocation/locking paths. In current russh releases, local SSH agent peers could still feed attacker-controlled frame lengths into buffer growth before validation. In older russh releases before 0.58.0, remote SSH traffic also reached CryptoVec through transport and compression buffers.
Details
The underlying unsafe paths were in CryptoVec:
cryptovec/src/cryptovec.rs- unchecked capacity growth
- unchecked length arithmetic in growth callers
- raw allocation and reallocation paths coupled to those sizes
cryptovec/src/platform/unix.rsmlock/munlockpreviously accepted zero-length calls and performed null-pointer validation inside theunsafeOS-call path
There are two relevant reachability stories:
- current local reachability in
russh
russh/src/keys/agent/client.rsAgentClient::read_response()read a peer-suppliedu32length and then resizedself.bufto that value before reading the payloadrussh/src/keys/agent/server.rsConnection::run()read a peer-suppliedu32length and then resizedself.bufto that value before reading the payload
This is the path that still existed in current 0.60.x releases before the fix, although by then those buffers were no longer CryptoVec.
- historical remote reachability in older
russh
- before commit
712e32b(first released inv0.58.0), non-secret transport and compression buffers inrusshstill usedCryptoVec - I verified this in a detached pre-
712e32bworktree by adding and running: cipher::tests::remote_packet_length_grows_transport_cryptovec_buffercompression::tests::remote_compressed_payload_expands_cryptovec_output- those tests show that remote SSH traffic could grow
CryptoVecthrough: - transport packet reads
- zlib decompression output
Also added a constrained-memory reproduction in that historical worktree:
compression::tests::remote_compressed_payload_can_crash_under_memory_limit
That test re-execs the test binary under prlimit --as=134217728, decompresses a highly compressible payload that expands to 96 MiB, and reliably aborts in the old Unix CryptoVec path when NonNull::new_unchecked() receives a null pointer after allocation failure.
The prepared patch does two things:
- hardens
CryptoVecitself
- checked capacity growth
- checked length arithmetic
- immediate allocation-failure handling
- zero-length
mlock/munlockno-ops - explicit null-pointer validation before entering the Unix
unsafelocking calls
- hardens the real untrusted-input path
- caps agent frame lengths at
256 * 1024on both client and server before resizing buffers
This cap matches OpenSSH’s agent framing guardrail.
PoC
The following end-to-end tests demonstrate the real untrusted-input path by feeding oversized peer-controlled agent frame lengths into the public client and server flows and asserting that they are rejected before buffer growth.
Client-side agent reply path:
#[test]
fn oversized_agent_response_is_rejected_before_allocation() -> std::io::Result<()> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
runtime.block_on(async {
let (mut writer, reader) = tokio::io::duplex(64);
let server = tokio::spawn(async move {
let mut frame = [0u8; 4];
writer.read_exact(&mut frame).await?;
let len = BigEndian::read_u32(&frame) as usize;
let mut body = vec![0; len];
writer.read_exact(&mut body).await?;
BigEndian::write_u32(&mut frame, (MAX_AGENT_FRAME_LEN + 1) as u32);
writer.write_all(&frame).await?;
Ok::<(), std::io::Error>(())
});
let mut client = AgentClient::connect(reader);
let err = client.request_identities().await.unwrap_err();
assert!(matches!(err, Error::AgentProtocolError));
server.await.expect("server task")?;
Ok::<(), std::io::Error>(())
})?;
Ok(())
}Server-side agent request path:
#[test]
fn oversized_agent_request_is_rejected_before_allocation() -> std::io::Result<()> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
runtime.block_on(async {
let (server, mut client) = tokio::io::duplex(64);
let connection = Connection {
lock: Lock(std::sync::Arc::new(std::sync::RwLock::new(crate::CryptoVec::new()))),
keys: KeyStore(std::sync::Arc::new(std::sync::RwLock::new(
std::collections::HashMap::new(),
))),
agent: Some(()),
s: server,
buf: Vec::new(),
};
let server = tokio::spawn(async move { connection.run().await });
let mut frame = [0u8; 4];
BigEndian::write_u32(&mut frame, (MAX_AGENT_FRAME_LEN + 1) as u32);
client.write_all(&frame).await?;
drop(client);
let err = server.await.expect("server task").unwrap_err();
assert!(matches!(err, Error::AgentProtocolError));
Ok::<(), std::io::Error>(())
})?;
Ok(())
}These tests pass on the fixed branch and fail on unfixed v0.60.2, where oversized agent frame lengths are not rejected at the framing boundary.
For historical russh < 0.58.0, I also verified remote reachability into CryptoVec in a detached pre-712e32b worktree (91d431d, package version 0.57.1).
Transport packet read path:
#[test]
fn remote_packet_length_grows_transport_cryptovec_buffer() -> std::io::Result<()> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
runtime.block_on(async {
let packet_len = MAXIMUM_PACKET_LEN;
let (mut writer, mut reader) = tokio::io::duplex(packet_len + 4);
let writer_task = tokio::spawn(async move {
let mut packet = vec![0u8; packet_len + 4];
packet[..4].copy_from_slice(&(packet_len as u32).to_be_bytes());
writer.write_all(&packet).await?;
Ok::<(), std::io::Error>(())
});
let mut buffer = SSHBuffer::new();
let mut cipher = clear::Key;
let n = read(&mut reader, &mut buffer, &mut cipher).await.unwrap();
assert_eq!(n, packet_len + 4);
assert_eq!(buffer.buffer.len(), packet_len + 4);
assert_eq!(&buffer.buffer[..4], &(packet_len as u32).to_be_bytes());
writer_task.await.expect("writer task")?;
Ok::<(), std::io::Error>(())
})?;
Ok(())
}Compression growth path:
#[test]
fn remote_compressed_payload_expands_cryptovec_output() {
let payload = vec![b'A'; 64 * 1024];
let compression = Compression::new(&ZLIB);
let mut compressor = Compress::None;
let mut decompressor = Decompress::None;
compression.init_compress(&mut compressor);
compression.init_decompress(&mut decompressor);
let mut compressed = CryptoVec::new();
let encoded = compressor
.compress(&payload, &mut compressed)
.expect("compress")
.to_vec();
let mut output = CryptoVec::new();
let decoded = decompressor
.decompress(&encoded, &mut output)
.expect("decompress");
assert_eq!(decoded.len(), payload.len());
assert_eq!(decoded, payload.as_slice());
assert!(encoded.len() < output.len());
}Constrained-memory crash reproduction for the historical remote compression path:
#[test]
fn remote_compressed_payload_can_crash_under_memory_limit() {
const CHILD_ENV: &str = "RUSSH_REMOTE_COMPRESS_CRASH_CHILD";
if std::env::var_os(CHILD_ENV).is_some() {
let payload = vec![b'A'; 96 * 1024 * 1024];
let compression = Compression::new(&ZLIB);
let mut compressor = Compress::None;
let mut decompressor = Decompress::None;
compression.init_compress(&mut compressor);
compression.init_decompress(&mut decompressor);
let mut compressed = CryptoVec::new();
let encoded = compressor
.compress(&payload, &mut compressed)
.expect("compress")
.to_vec();
let mut output = CryptoVec::new();
let decoded = decompressor
.decompress(&encoded, &mut output)
.expect("decompress");
assert_eq!(decoded.len(), payload.len());
return;
}
let exe = std::env::current_exe().expect("current exe");
let status = Command::new("prlimit")
.args([
"--as=134217728",
"--",
exe.to_str().expect("utf8 exe path"),
"--exact",
"compression::tests::remote_compressed_payload_can_crash_under_memory_limit",
"--nocapture",
])
.env(CHILD_ENV, "1")
.status()
.expect("spawn child");
assert!(
!status.success(),
"expected child to fail under constrained address space"
);
}On that historical worktree, the constrained-memory child aborts in the old Unix CryptoVec path with:
unsafe precondition(s) violated: NonNull::new_unchecked requires that the pointer is non-null
thread caused non-unwinding panic. aborting.To run the reproduced checks:
cargo test -p russh oversized_agent_response_is_rejected_before_allocation -- --nocapture
cargo test -p russh oversized_agent_request_is_rejected_before_allocation -- --nocapture
cargo test -p russh-cryptovecHistorical pre-0.58.0 checks were run from the detached 91d431d worktree with:
cargo test --offline -p russh remote_packet_length_grows_transport_cryptovec_buffer -- --nocapture
cargo test --offline -p russh remote_compressed_payload_expands_cryptovec_output -- --nocapture
cargo test --offline -p russh remote_compressed_payload_can_crash_under_memory_limit -- --nocaptureImpact
This is a memory-safety hardening issue with demonstrated untrusted-input reachability.
What is demonstrated:
- current local agent peers could previously reach allocation growth directly from attacker-controlled frame lengths
- historical remote SSH traffic could previously reach
CryptoVecthrough transport and compression buffers inrussh < 0.58.0 - under constrained memory, the historical remote compression path can be turned into a process abort in the old Unix
CryptoVeccode - the fixed code now rejects oversized agent frames early and hardens the underlying allocation paths
What is not demonstrated:
- practical code execution
- a demonstrated integrity or confidentiality break
AnalysisAI
Denial-of-service via unchecked memory allocation in russh (Rust SSH library) versions <= 0.60.2 allows local SSH agent peers to trigger uncontrolled buffer growth by sending oversized frame length values, and in pre-0.58.0 releases the same CryptoVec allocation path was reachable from remote SSH transport and zlib decompression buffers. The flaw stems from CryptoVec performing unchecked capacity growth, unchecked length arithmetic, and unsafe allocation/locking calls including NonNull::new_unchecked on potentially failed allocations, which can abort the process under memory pressure. Publicly available exploit code exists in the form of researcher-supplied PoC tests demonstrating both rejection on patched code and crash behavior on historical versions; no public exploit identified at time of analysis for active campaigns and the issue is not listed in CISA KEV.
Technical ContextAI
russh is a pure-Rust SSH client and server library; russh-cryptovec is its companion crate providing CryptoVec, a heap buffer intended to hold sensitive data and to be mlock'd into RAM. The root cause is CWE-770 (Allocation of Resources Without Limits or Throttling) compounded by misuse of Rust unsafe primitives: cryptovec/src/cryptovec.rs grew capacity and computed lengths without overflow or allocation-failure checks, while cryptovec/src/platform/unix.rs invoked mlock/munlock and NonNull::new_unchecked without validating zero-length or null-pointer preconditions, violating Rust safety invariants. In russh itself, AgentClient::read_response() and Connection::run() (in russh/src/keys/agent/{client,server}.rs) read a peer-controlled u32 frame length and resized the receive buffer to that value before validating it. Historically, before commit 712e32b shipped in v0.58.0, the same CryptoVec buffer was also used for SSH transport packet reads and zlib decompression output, exposing it to remote SSH traffic. Affected CPEs are pkg:rust/russh-cryptovec and pkg:rust/russh.
RemediationAI
Vendor-released patch: upgrade russh and russh-cryptovec to 0.60.3 or later via cargo update, which both hardens CryptoVec (checked capacity growth, checked length arithmetic, immediate allocation-failure handling, zero-length mlock/munlock no-ops, explicit null-pointer validation before unsafe Unix locking calls) and caps SSH agent frame lengths at 256 KiB on client and server before any buffer resize, matching OpenSSH's agent framing guardrail. Consumers should review their Cargo.lock and rebuild dependent binaries. If immediate upgrade is not possible, restrict SSH agent socket access to trusted local users only via filesystem permissions on the agent socket (already standard practice, but explicitly enforce 0600 ownership), and for any deployment still on russh < 0.58.0 do not expose the SSH server to untrusted networks and consider disabling zlib compression negotiation to remove the historical decompression growth path (trade-off: loses bandwidth savings on slow links). Advisory and patch details at https://github.com/Eugeny/russh/security/advisories/GHSA-g9f8-wqj9-fjw5 and https://github.com/advisories/GHSA-g9f8-wqj9-fjw5.
GNU Bash through 4.3 processes trailing strings after function definitions in the values of environment variables, which
GNU Bash through 4.3 bash43-026 does not properly parse function definitions in the values of environment variables, whi
GNU Bash through 4.3 bash43-025 processes trailing strings after certain malformed function definitions in the values of
freeSSHd.exe in freeSSHd through 1.2.6 allows remote attackers to bypass authentication via a crafted session, as demons
GNU Bash through 4.3 bash43-026 does not properly parse function definitions in the values of environment variables, whi
OpenSSH server (sshd) 9.1 introduced a double-free vulnerability during options.kex_algorithms handling. Rated medium se
sshd in OpenSSH before 7.3, when SHA256 or SHA512 are used for user password hashing, uses BLOWFISH hashing on a static
The kbdint_next_device function in auth2-chall.c in sshd in OpenSSH through 6.9 does not properly restrict the processin
The auth_password function in auth-passwd.c in sshd in OpenSSH before 7.3 does not limit password lengths for password a
Remote code execution in OpenSSH's sshd server (regression of CVE-2006-5051) allows unauthenticated remote attackers to
The SSH USERAUTH CHANGE REQUEST feature in SSH Tectia Server 6.0.4 through 6.0.20, 6.1.0 through 6.1.12, 6.2.0 through 6
The SSH transport protocol with certain OpenSSH extensions, found in OpenSSH before 9.6 and other products, allows remot
Same technique Denial Of Service
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-36124
GHSA-g9f8-wqj9-fjw5