GHSA-35g8-35p8-c8fw CVE-2026-102821
Russh: Unbounded memory exhaustion via CHANNEL_OPEN flood during a client-stalled rekey
- Severity
- 6.5 / 10
- Medium · CVSS 3.1
- Exploit chance (EPSS)
- 0.36%
- Chance of exploitation in the next 30 days. Higher than 29.59% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-400 Uncontrolled Resource Consumption
- CWE-770 Allocation of Resources Without Limits or Throttling
- Fix
0.63.2- Upgrade russh to this version or later
Description
Summary
A russh server can be driven to unbounded heap growth (process OOM / kill) by a peer that speaks only standard SSH messages, in the default configuration.
The peer starts a key re-exchange (sends SSH_MSG_KEXINIT) but never sends the
follow-up SSH_MSG_KEX_ECDH_INIT, leaving the server's kex state machine in
SessionKexState::InProgress indefinitely. While a rekey is in progress the
server's three message-drain paths are all gated off (if !self.kex.active()),
but the network-read path stays active, so every SSH_MSG_CHANNEL_OPEN the peer
sends is processed inline and appends one reply to an unbounded internal
queue (priority_receiver, an UnboundedReceiver) that is not dequeued until
the rekey completes. Because the peer decides whether the rekey ever completes,
the queue — and the server's memory — grows without bound.
This is reproducible end-to-end against a real russh server over a real encrypted transport; see "Proof of concept". A one-line negative control (same flood, no rekey) keeps memory flat, isolating the rekey window as the sole trigger.
Impact
- Availability / DoS. Server RSS climbs at roughly 3.3 KB per
CHANNEL_OPEN, driven entirely by the peer, until the process is OOM-killed. In the reproduction one connection pushed the server from ~4 MB to 2.57 GB (and past 4.8 GB against the stockechoserverexample) and it was still climbing when the flood was stopped. - Reached with standard messages, no special configuration. Any handler is affected, including one that rejects every channel (the reply enqueued on rejection is exactly what accumulates). There is no per-connection cap on in-flight channel opens or on the queue, and the queue's sender has no backpressure.
- The peer keeps the connection alive simply by continuing to send, so the inactivity timer never fires.
Affected component
russh/src/server/session.rs—Session::runtokio::select!loop. The network-read arm is ungated; the three drain paths are gated on!self.kex.active().russh/src/server/mod.rs—reply()routes a non-kex message received during a rekey straight toserver_read_encrypted(inline processing).russh/src/lib_inner.rs—ChannelOpenHandleInner'saccept/reject/Dropallsenda reply on anUnboundedSender.
Verified on v0.63.1 (commit d3ae702), which is the latest release. The
gating logic predates it.
Details
Session::run (russh/src/server/session.rs:631) drives a tokio::select!
(:713). Three of its message-drain sites are gated on !self.kex.active():
- the pre-
select!batch drain ofpriority_receiver/receiver(session.rs:680), - the
priority_receiver.recv()arm (session.rs:762), - the
receiver.recv()arm (session.rs:770, which also holds the only otherpriority_receiverdrain at:777).
The fourth arm, r = &mut reading (session.rs:714), is ungated: it reads
and processes one incoming packet every loop iteration regardless of rekey
state, calling reply() (server/mod.rs:1128).
During a rekey, session.common.encrypted.is_some(), so the strict-kex
message-ordering guard (server/mod.rs:1143, which is additionally gated on
encrypted.is_none()) does not apply. A non-kex message therefore falls
through reply() to session.server_read_encrypted(handler, pkt)
(server/mod.rs:1232) and is handled inline. For SSH_MSG_CHANNEL_OPEN this
reaches the channel-open handling, which hands the application a
ChannelOpenHandle.
Whether the handler accepts or (the trait default) rejects, the handle's
accept / reject / Drop all send a Msg::ChannelOpenReply on an
UnboundedSender (russh/src/lib_inner.rs:560-603; Drop sends
AdministrativelyProhibited at :594-603). That sender feeds
priority_receiver, declared UnboundedReceiver<Msg> (session.rs:23) and
created with tokio::sync::mpsc::unbounded_channel() (session.rs:1522). Its
only drain sites are the three arms gated off during the rekey. So each
CHANNEL_OPEN processed during the rekey window appends one reply (carrying a
PendingChannelOpen = channel params + mpsc ChannelRef + ids, a few KB
retained in practice) to a queue that is never dequeued.
Two facts make this unbounded and remote:
- The server enters
InProgressthe moment it receives the peer'sKEXINIT(server/mod.rs:1153-1158,begin_rekey) and only leaves it upon receiving the peer'sKEX_ECDH_INIT. The peer decides whether to ever send that, so the window is attacker-held. - There is no per-connection cap on channels or on the priority queue, and the sender is unbounded (no backpressure).
Root cause
The intended design was to buffer packets received during a rekey and replay
them afterwards: the fields pending_reads: Vec<Vec<u8>> and pending_len: u32
(session.rs:26-27) exist and are drained at kex completion
(server/mod.rs:1194-1198). But nothing ever pushes to pending_reads or
increments pending_len (they are dead — confirmed by grep across
russh/src/). Instead of being buffered, channel messages received during a
rekey are processed inline, and their replies pile up in the unbounded
priority_receiver. The missing piece is a bound on — or bounded deferral of —
channel processing while kex.active().
Proof of concept
Everything runs inside a container; nothing touches the host.
Lab. poc/Dockerfile builds russh-lab:head from Eugeny/russh @ d3ae702
(v0.63.1), default features (rust 1.91). A raw-SSH-client PoC
(poc/poc_rekey_dos.rs) implements curve25519-sha256 / ssh-ed25519 /
aes256-ctr / hmac-sha2-256 by hand, completes the handshake and a publickey
auth, then:
- sends
SSH_MSG_KEXINIT(server enterskex.active()), - never sends
KEX_ECDH_INIT(rekey stalls, attacker-held), - floods
SSH_MSG_CHANNEL_OPEN.
A minimal server (poc/poc_server.rs) uses the trait-default
channel_open_session (which rejects by dropping the handle), so the measured
growth is purely the undrained priority queue, not accepted-channel state.
poc/run.sh runs the attack leg and an identical negative control with no
rekey. Fresh run inside the lab, N = 800,000 opens
(results/rerun-2026-08-29.log):
=== ATTACK (rekey stall) === (poc_server baseline_RSS=4208KB, N=800000)
t=2s server_RSS=667760KB
t=4s server_RSS=1599600KB
t=6s server_RSS=2556016KB
t=8s server_RSS=2566256KB (flood done; memory retained)
=== CONTROL (no rekey) === (poc_server baseline_RSS=4208KB, N=800000)
t=2s..t=12s server_RSS=4208KB (flat throughout)
- ATTACK: RSS
4,208 KB → 2,566,256 KB(~2.57 GB) and retained after the flood ends — ~3.3 KB perCHANNEL_OPEN, attacker-driven. (An earlier canonical run reached 2.7 GB at 800k opens, and past 4.8 GB against the stockechoserverexample at 1.5 M opens — seeresults/canonical-run.log.) - CONTROL: RSS flat at
4,208 KB. Without the rekey window the replies are drained normally; TCP backpressure (the client never reads the failure replies) even throttles the flood.
The control isolates the rekey window as the sole trigger. Both legs exercise
the real server entry point (Session::run → reply → server_read_encrypted)
over a real encrypted transport.
Reproduce: C=russh-lab N=800000 ./poc/run.sh (see poc/POC-README.md).
Remediation
The priority queue carries locally generated channel-open replies, which are non-kex messages the server must not send during a rekey anyway (RFC 4253 §7.1). So the fix is to bound how much channel work is done during a rekey, not to drain the queue mid-rekey. Any of:
- (recommended, minimal) cap the number of non-kex messages processed while a
rekey is in progress and disconnect a peer that exceeds it. A stalled/abusive
rekey is then torn down after a small constant instead of growing memory
without bound. See
patch/rekey-message-cap.patch(a ~10-line change local toSession::run, validated in the lab — the attack leg is disconnected after the cap and RSS stays flat; a normal rekey, which completes in one round trip, is unaffected). - bound / deferred-buffer channel messages during a rekey using the existing
(currently dead)
pending_reads/pending_lenmachinery, with a hard cap. - bound the number of in-flight channel opens per connection and reject beyond it.
OpenSSH does not service new channels mid-kex; matching that intent closes the whole family.
Affected versions
crates.io
| Package | Vulnerable | Fixed in |
|---|---|---|
| russh | < 0.63.2 |
0.63.2 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
russh crates.io → 0.63.2
- cargo
cargo update -p russh --precise 0.63.2
Severity breakdown
CVSS 3.1 6.5
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- Low
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- None
- Integrity
- None
- Availability
- High
Weakness
CWE-400: Uncontrolled Resource Consumption
The product does not properly control the allocation and maintenance of a limited resource.
How to prevent it
- Architecture and Design: Design throttling mechanisms into the system architecture. The best protection is to limit the amount of resources that an unauthorized user can cause to be expended. A strong authentication and access control model will help prevent such attacks from occurring in the first place. The login application should be protected against DoS attacks as much as possible. Limiting the database access, perhaps by caching result sets, can help minimize the resources expended. To further limit the potential for a DoS attack, consider tracking the rate of requests received from users and blocking requests that exceed a defined rate threshold.
- Architecture and Design: Mitigation of resource exhaustion attacks requires that the target system either: recognizes the attack and denies that user further access for a given amount of time, or uniformly throttles all requests in order to make it more difficult to consume resources more quickly than they can again be freed. The first of these solutions is an issue in itself though, since it may allow attackers to prevent the use of the system by a particular valid user. If the attacker impersonates the valid user, they may be able to prevent the user from accessing the server in question. The second solution is simply difficult to effectively institute -- and even when properly done, it does not provide a full solution. It simply makes the attack require more resources on the part of the attacker.
- Architecture and Design: Ensure that protocols have specific limits of scale placed on them.
- Implementation: Ensure that all failures in resource allocation place the system into a safe posture.
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 to prevent it
- Requirements: Clearly specify the minimum and maximum expectations for capabilities, and dictate which behaviors are acceptable when resource allocation reaches limits.
- Architecture and Design: Limit the amount of resources that are accessible to unprivileged users. Set per-user limits for resources. Allow the system administrator to define these limits. Be careful to avoid CWE-410.
- Architecture and Design: Design throttling mechanisms into the system architecture. The best protection is to limit the amount of resources that an unauthorized user can cause to be expended. A strong authentication and access control model will help prevent such attacks from occurring in the first place, and it will help the administrator to identify who is committing the abuse. The login application should be protected against DoS attacks as much as possible. Limiting the database access, perhaps by caching result sets, can help minimize the resources expended. To further limit the potential for a DoS attack, consider tracking the rate of requests received from users and blocking requests that exceed a defined rate threshold.
- Implementation: Assume all input is malicious. Use an "accept known good" input validation strategy, i.e., use a list of acceptable inputs that strictly conform to specifications. Reject any input that does not strictly conform to specifications, or transform it into something that does. When performing input validation, consider all potentially relevant properties, including length, type of input, the full range of acceptable values, missing or extra inputs, syntax, consistency across related fields, and conformance to business rules. As an example of business rule logic, "boat" may be syntactically valid because it only contains alphanumeric characters, but it is not valid if the input is only expected to contain colors such as "red" or "blue." Do not rely exclusively on looking for malicious or malformed inputs. This is likely to miss at least one undesirable input, especially if the code's environment changes. This can give attackers enough room to bypass the intended validation. However, denylists can be useful for detecting potential attacks or determining which inputs are so malformed that they should be rejected outright.
- Architecture and Design: For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server.
- Architecture and Design: Mitigation of resource exhaustion attacks requires that the target system either: recognizes the attack and denies that user further access for a given amount of time, typically by using increasing time delays uniformly throttles all requests in order to make it more difficult to consume resources more quickly than they can again be freed. The first of these solutions is an issue in itself though, since it may allow attackers to prevent the use of the system by a particular valid user. If the attacker impersonates the valid user, they may be able to prevent the user from accessing the server in question. The second solution can be difficult to effectively institute -- and even when properly done, it does not provide a full solution. It simply requires more resources on the part of the attacker.
- Architecture and Design: Ensure that protocols have specific limits of scale placed on them.
- Architecture and Design, Implementation: If the program must fail, ensure that it fails gracefully (fails closed). There may be a temptation to simply let the program fail poorly in cases such as low memory conditions, but an attacker may be able to assert control before the software has fully exited. Alternately, an uncontrolled failure could cause cascading problems with other downstream components; for example, the program could send a signal to a downstream process so the process immediately knows that a problem has occurred and has a better chance of recovery. Ensure that all failures in resource allocation place the system into a safe posture.
- Operation, Architecture and Design: Use quotas or other resource-limiting settings provided by the operating system or environment. For example, when managing system resources in POSIX, setrlimit() can be used to set limits for certain types of resources, and getrlimit() can determine how many resources are available. However, these functions are not available on all operating systems. When the current levels get close to the maximum that is defined for the application (see CWE-770), then limit the allocation of further resources to privileged users; alternately, begin releasing resources for less-privileged users. While this mitigation may protect the system from attack, it will not necessarily stop attackers from adversely impacting other users. Ensure that the application performs the appropriate error checks and error handling in case resources become unavailable (CWE-703).
References
Advisories
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-vp5q-ff62-jwqv russh: Agent server never calls `Agent::confirm`, so keys added with the confirm constraint (`ssh-add -c`) sign without confirmation | High | Also affects russh |
| GHSA-fw5c-8223-6h42 russh: peer-declared zero maximum packet size permanently stalls channel writes (silent AsyncWrite hang) | Medium | Also affects russh |
| GHSA-9wfm-jcj8-jfv9 russh's SSH agent server is susceptible to unbound allocations via repeated SSH_AGENT_CONSTRAIN_CONFIRM messages | Low | Also affects russh |
| GHSA-4p9f-7p9m-5vfg russh's SSH client is susceptible to unbounded allocations via EXT_INFO packets | Medium | Also affects russh |
| GHSA-4wc5-f2rc-q74m russh: Unbounded memory allocation in agent protocol server via crafted constraint records | Medium | Also affects russh |
| GHSA-j7h2-66j4-ghjf A russh SSH server accepts a publickey authentication whose signature uses a different algorithm than the one named in the request | Low | Also affects russh |
| GHSA-g6xm-f9xp-qq35 Russh: Configured server auth-attempt cap is not enforced in the USERAUTH_REQUEST runtime path | Low | Also affects russh |
| GHSA-w3jg-pjxf-73p4 Russh: Missing X25519 zero-point validation in hybrid ML-KEM key exchange | Medium | Also affects russh |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor