CVE-2026-46673: Russh Unchecked CryptoVec Allocation And Growth Vulnerability

HIGH
Advisory Identifier: GHSA-g9f8-wqj9-fjw5
CVSS 7.5
CWE-770
Published
Advisory Summary

CVE-2026-46673 is a HIGH (CVSS 7.5) unchecked cryptovec allocation and growth vulnerability (CWE-770: Allocation of Resources Without Limits or Throttling) affecting Russh libraries in the crates.io ecosystem. The issue involves unchecked cryptovec allocation and growth handling is reachable. Remediation is available by upgrading affected packages to version 0.60.3.

CVE-2026-46673 at a Glance

CVE Identifier CVE-2026-46673
GHSA Identifier GHSA-g9f8-wqj9-fjw5
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-770: Allocation of Resources Without Limits or Throttling
Affected Product Russh
Package Ecosystem crates.io
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability High (A:H)

How to Fix CVE-2026-46673

To remediate this vulnerability, upgrade all affected Russh dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

russh-cryptovec fixed: 0.60.3
Upgrade russh-cryptovec to 0.60.3
russh fixed: 0.60.3
Upgrade russh to 0.60.3
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for russh-cryptovec

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-46673, inspect your project dependencies for Russh:

inspect dependencies for russh-cryptovec

Analyze the terminal output to locate any version of Russh in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.60.3), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-770: Allocation of Resources Without Limits or Throttling, where the product allocates a reusable resource or group of resources on behalf of an actor without imposing any intended restrictions on the size or number of resources that can be allocated. In Russh, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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.rs` - `mlock` / `munlock` previously accepted zero-length calls and performed null-pointer validation inside the `unsafe` OS-call path There are two relevant reachability stories: 1. current local reachability in `russh` - `russh/src/keys/agent/client.rs` - `AgentClient::read_response()` read a peer-supplied `u32` length and then resized `self.buf` to that value before reading the payload - `russh/src/keys/agent/server.rs` - `Connection::run()` read a peer-supplied `u32` length and then resized `self.buf` to 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`. 2. historical remote reachability in older `russh` - before commit `712e32b` (first released in `v0.58.0`), non-secret transport and compression buffers in `russh` still used `CryptoVec` - I verified this in a detached pre-`712e32b` worktree by adding and running: - `cipher::tests::remote_packet_length_grows_transport_cryptovec_buffer` - `compression::tests::remote_compressed_payload_expands_cryptovec_output` - those tests show that remote SSH traffic could grow `CryptoVec` through: - 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: 1. hardens `CryptoVec` itself - checked capacity growth - checked length arithmetic - immediate allocation-failure handling - zero-length `mlock` / `munlock` no-ops - explicit null-pointer validation before entering the Unix `unsafe` locking calls 2. hardens the real untrusted-input path - caps agent frame lengths at `256 * 1024` on 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: ```rust #[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: ```rust #[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: ```rust #[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: ```rust #[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: ```rust #[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: ```text 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: ```bash 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-cryptovec ``` Historical pre-`0.58.0` checks were run from the detached `91d431d` worktree with: ```bash 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 -- --nocapture ``` ### Impact 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 `CryptoVec` through transport and compression buffers in `russh < 0.58.0` - under constrained memory, the historical remote compression path can be turned into a process abort in the old Unix `CryptoVec` code - 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

CVE-2026-46673 Affected Russh Versions

2 component(s)
Fixed Versions:
0.60.3
Product / Package Ecosystem Affected Versions Fixed Version
russh-cryptovec
crates.io
< 0.60.3
0.60.3
russh
crates.io
< 0.60.3
0.60.3

References

Frequently Asked Questions

What is CVE-2026-46673?
CVE-2026-46673 is a security vulnerability in Russh involving Unchecked CryptoVec allocation and growth handling is reachable under CWE-770: Allocation of Resources Without Limits or Throttling.
Which Russh versions are affected?
Affected versions include releases prior to 0.60.3 across affected packages: russh-cryptovec, russh.
What version fixes CVE-2026-46673?
Upgrading to 0.60.3 (or the latest patched release for your respective branch) resolves the vulnerability.
Is russh-cryptovec affected?
Yes, russh-cryptovec is affected in releases prior to 0.60.3. Upgrade your dependency to 0.60.3 or later.
Is russh affected?
Yes, russh is affected in releases prior to 0.60.3. Upgrade your dependency to 0.60.3 or later.
What is the CVSS score of CVE-2026-46673?
CVE-2026-46673 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H.
What CWE is associated with CVE-2026-46673?
CVE-2026-46673 is classified under CWE-770: Allocation of Resources Without Limits or Throttling (The product allocates a reusable resource or group of resources on behalf of an actor without imposing any intended restrictions on the size or number of resources that can be allocated.).
How can I check crates.io dependencies for CVE-2026-46673?
Run the dependency tree command: 'inspect dependencies for russh-cryptovec'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-g9f8-wqj9-fjw5",
  "modified" : "2026-06-11T14:06:39Z",
  "published" : "2026-05-21T20:49:07Z",
  "aliases" : [ "CVE-2026-46673" ],
  "summary" : "Russh: Unchecked CryptoVec allocation and growth handling is reachable",
  "details" : "### Title\nUnchecked `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\n\n### Summary\n`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.\n\n### Details\nThe underlying unsafe paths were in `CryptoVec`:\n\n- `cryptovec/src/cryptovec.rs`\n  - unchecked capacity growth\n  - unchecked length arithmetic in growth callers\n  - raw allocation and reallocation paths coupled to those sizes\n- `cryptovec/src/platform/unix.rs`\n  - `mlock` / `munlock` previously accepted zero-length calls and performed null-pointer validation inside the `unsafe` OS-call path\n\nThere are two relevant reachability stories:\n\n1. current local reachability in `russh`\n\n- `russh/src/keys/agent/client.rs`\n  - `AgentClient::read_response()` read a peer-supplied `u32` length and then resized `self.buf` to that value before reading the payload\n- `russh/src/keys/agent/server.rs`\n  - `Connection::run()` read a peer-supplied `u32` length and then resized `self.buf` to that value before reading the payload\n\nThis is the path that still existed in current `0.60.x` releases before the fix, although by then those buffers were no longer `CryptoVec`.\n\n2. historical remote reachability in older `russh`\n\n- before commit `712e32b` (first released in `v0.58.0`), non-secret transport and compression buffers in `russh` still used `CryptoVec`\n- I verified this in a detached pre-`712e32b` worktree by adding and running:\n  - `cipher::tests::remote_packet_length_grows_transport_cryptovec_buffer`\n  - `compression::tests::remote_compressed_payload_expands_cryptovec_output`\n- those tests show that remote SSH traffic could grow `CryptoVec` through:\n  - transport packet reads\n  - zlib decompression output\n\nAlso added a constrained-memory reproduction in that historical worktree:\n\n- `compression::tests::remote_compressed_payload_can_crash_under_memory_limit`\n\nThat 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.\n\nThe prepared patch does two things:\n\n1. hardens `CryptoVec` itself\n   - checked capacity growth\n   - checked length arithmetic\n   - immediate allocation-failure handling\n   - zero-length `mlock` / `munlock` no-ops\n   - explicit null-pointer validation before entering the Unix `unsafe` locking calls\n\n2. hardens the real untrusted-input path\n   - caps agent frame lengths at `256 * 1024` on both client and server before resizing buffers\n\nThis cap matches OpenSSH’s agent framing guardrail.\n\n### PoC\nThe 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.\n\nClient-side agent reply path:\n\n```rust\n#[test]\nfn oversized_agent_response_is_rejected_before_allocation() -> std::io::Result<()> {\n    let runtime = tokio::runtime::Builder::new_current_thread()\n        .enable_all()\n        .build()?;\n\n    runtime.block_on(async {\n        let (mut writer, reader) = tokio::io::duplex(64);\n        let server = tokio::spawn(async move {\n            let mut frame = [0u8; 4];\n            writer.read_exact(&mut frame).await?;\n            let len = BigEndian::read_u32(&frame) as usize;\n            let mut body = vec![0; len];\n            writer.read_exact(&mut body).await?;\n\n            BigEndian::write_u32(&mut frame, (MAX_AGENT_FRAME_LEN + 1) as u32);\n            writer.write_all(&frame).await?;\n            Ok::<(), std::io::Error>(())\n        });\n\n        let mut client = AgentClient::connect(reader);\n        let err = client.request_identities().await.unwrap_err();\n        assert!(matches!(err, Error::AgentProtocolError));\n        server.await.expect(\"server task\")?;\n        Ok::<(), std::io::Error>(())\n    })?;\n\n    Ok(())\n}\n```\n\nServer-side agent request path:\n\n```rust\n#[test]\nfn oversized_agent_request_is_rejected_before_allocation() -> std::io::Result<()> {\n    let runtime = tokio::runtime::Builder::new_current_thread()\n        .enable_all()\n        .build()?;\n\n    runtime.block_on(async {\n        let (server, mut client) = tokio::io::duplex(64);\n        let connection = Connection {\n            lock: Lock(std::sync::Arc::new(std::sync::RwLock::new(crate::CryptoVec::new()))),\n            keys: KeyStore(std::sync::Arc::new(std::sync::RwLock::new(\n                std::collections::HashMap::new(),\n            ))),\n            agent: Some(()),\n            s: server,\n            buf: Vec::new(),\n        };\n        let server = tokio::spawn(async move { connection.run().await });\n\n        let mut frame = [0u8; 4];\n        BigEndian::write_u32(&mut frame, (MAX_AGENT_FRAME_LEN + 1) as u32);\n        client.write_all(&frame).await?;\n        drop(client);\n\n        let err = server.await.expect(\"server task\").unwrap_err();\n        assert!(matches!(err, Error::AgentProtocolError));\n        Ok::<(), std::io::Error>(())\n    })?;\n\n    Ok(())\n}\n```\n\nThese 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.\n\nFor historical `russh < 0.58.0`, I also verified remote reachability into `CryptoVec` in a detached pre-`712e32b` worktree (`91d431d`, package version `0.57.1`).\n\nTransport packet read path:\n\n```rust\n#[test]\nfn remote_packet_length_grows_transport_cryptovec_buffer() -> std::io::Result<()> {\n    let runtime = tokio::runtime::Builder::new_current_thread()\n        .enable_all()\n        .build()?;\n\n    runtime.block_on(async {\n        let packet_len = MAXIMUM_PACKET_LEN;\n        let (mut writer, mut reader) = tokio::io::duplex(packet_len + 4);\n        let writer_task = tokio::spawn(async move {\n            let mut packet = vec![0u8; packet_len + 4];\n            packet[..4].copy_from_slice(&(packet_len as u32).to_be_bytes());\n            writer.write_all(&packet).await?;\n            Ok::<(), std::io::Error>(())\n        });\n\n        let mut buffer = SSHBuffer::new();\n        let mut cipher = clear::Key;\n        let n = read(&mut reader, &mut buffer, &mut cipher).await.unwrap();\n\n        assert_eq!(n, packet_len + 4);\n        assert_eq!(buffer.buffer.len(), packet_len + 4);\n        assert_eq!(&buffer.buffer[..4], &(packet_len as u32).to_be_bytes());\n\n        writer_task.await.expect(\"writer task\")?;\n        Ok::<(), std::io::Error>(())\n    })?;\n\n    Ok(())\n}\n```\n\nCompression growth path:\n\n```rust\n#[test]\nfn remote_compressed_payload_expands_cryptovec_output() {\n    let payload = vec![b'A'; 64 * 1024];\n\n    let compression = Compression::new(&ZLIB);\n    let mut compressor = Compress::None;\n    let mut decompressor = Decompress::None;\n    compression.init_compress(&mut compressor);\n    compression.init_decompress(&mut decompressor);\n\n    let mut compressed = CryptoVec::new();\n    let encoded = compressor\n        .compress(&payload, &mut compressed)\n        .expect(\"compress\")\n        .to_vec();\n\n    let mut output = CryptoVec::new();\n    let decoded = decompressor\n        .decompress(&encoded, &mut output)\n        .expect(\"decompress\");\n\n    assert_eq!(decoded.len(), payload.len());\n    assert_eq!(decoded, payload.as_slice());\n    assert!(encoded.len() < output.len());\n}\n```\n\nConstrained-memory crash reproduction for the historical remote compression path:\n\n```rust\n#[test]\nfn remote_compressed_payload_can_crash_under_memory_limit() {\n    const CHILD_ENV: &str = \"RUSSH_REMOTE_COMPRESS_CRASH_CHILD\";\n\n    if std::env::var_os(CHILD_ENV).is_some() {\n        let payload = vec![b'A'; 96 * 1024 * 1024];\n\n        let compression = Compression::new(&ZLIB);\n        let mut compressor = Compress::None;\n        let mut decompressor = Decompress::None;\n        compression.init_compress(&mut compressor);\n        compression.init_decompress(&mut decompressor);\n\n        let mut compressed = CryptoVec::new();\n        let encoded = compressor\n            .compress(&payload, &mut compressed)\n            .expect(\"compress\")\n            .to_vec();\n\n        let mut output = CryptoVec::new();\n        let decoded = decompressor\n            .decompress(&encoded, &mut output)\n            .expect(\"decompress\");\n        assert_eq!(decoded.len(), payload.len());\n        return;\n    }\n\n    let exe = std::env::current_exe().expect(\"current exe\");\n    let status = Command::new(\"prlimit\")\n        .args([\n            \"--as=134217728\",\n            \"--\",\n            exe.to_str().expect(\"utf8 exe path\"),\n            \"--exact\",\n            \"compression::tests::remote_compressed_payload_can_crash_under_memory_limit\",\n            \"--nocapture\",\n        ])\n        .env(CHILD_ENV, \"1\")\n        .status()\n        .expect(\"spawn child\");\n\n    assert!(\n        !status.success(),\n        \"expected child to fail under constrained address space\"\n    );\n}\n```\n\nOn that historical worktree, the constrained-memory child aborts in the old Unix `CryptoVec` path with:\n\n```text\nunsafe precondition(s) violated: NonNull::new_unchecked requires that the pointer is non-null\nthread caused non-unwinding panic. aborting.\n```\n\nTo run the reproduced checks:\n\n```bash\ncargo test -p russh oversized_agent_response_is_rejected_before_allocation -- --nocapture\ncargo test -p russh oversized_agent_request_is_rejected_before_allocation -- --nocapture\ncargo test -p russh-cryptovec\n```\n\nHistorical pre-`0.58.0` checks were run from the detached `91d431d` worktree with:\n\n```bash\ncargo test --offline -p russh remote_packet_length_grows_transport_cryptovec_buffer -- --nocapture\ncargo test --offline -p russh remote_compressed_payload_expands_cryptovec_output -- --nocapture\ncargo test --offline -p russh remote_compressed_payload_can_crash_under_memory_limit -- --nocapture\n```\n\n### Impact\nThis is a memory-safety hardening issue with demonstrated untrusted-input reachability.\n\nWhat is demonstrated:\n\n- current local agent peers could previously reach allocation growth directly from attacker-controlled frame lengths\n- historical remote SSH traffic could previously reach `CryptoVec` through transport and compression buffers in `russh < 0.58.0`\n- under constrained memory, the historical remote compression path can be turned into a process abort in the old Unix `CryptoVec` code\n- the fixed code now rejects oversized agent frames early and hardens the underlying allocation paths\n\nWhat is not demonstrated:\n\n- practical code execution\n- a demonstrated integrity or confidentiality break",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "crates.io",
      "name" : "russh-cryptovec"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.60.3"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 0.60.2"
    }
  }, {
    "package" : {
      "ecosystem" : "crates.io",
      "name" : "russh"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.60.3"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 0.60.2"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/Eugeny/russh/security/advisories/GHSA-g9f8-wqj9-fjw5"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-46673"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/Eugeny/russh"
  }, {
    "type" : "WEB",
    "url" : "https://rustsec.org/advisories/RUSTSEC-2026-0153.html"
  }, {
    "type" : "WEB",
    "url" : "https://rustsec.org/advisories/RUSTSEC-2026-0154.html"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-770" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-21T20:49:07Z",
    "nvd_published_at" : "2026-06-10T22:17:00Z"
  }
}