How an unauthenticated TLS half-open flood pins rippled's memory and crashes it under low file-descriptor limits
rippled's inbound TLS acceptors — the peer-protocol listener on port 51235 and the JSON-RPC HTTPS listener on port 5006 — install no handshake deadline and no per-IP cap on concurrent half-open connections, so a single unauthenticated source IP can hold thousands of stalled TLS handshakes open, pin server memory that does not free on client disconnect, and, at a default Linux file-descriptor limit, crash the process outright.
Why this matters
Both listeners are public-by-default on a rippled node: the peer port is how the XRP Ledger's peer-to-peer network operates, and the RPC-HTTPS port is a normal way to expose the JSON-RPC API. Any host that can reach either port can trigger this — no authentication, and no completed handshake, is required.
A naïve requests-per-second or connections-per-second rate limit does not close it. The only defence rippled's inbound path documents is a per-IP Resource::Consumer admission gate (OverlayImpl.cpp:235), and it throttles the rate of new connections, not the count of concurrent half-open ones. Measurement confirms this gap directly: 10,000 half-open connections from a single source IP complete admission in about a second, with no throttling observed. A rate limit sized for normal peer-discovery or RPC traffic will not stop an attacker from accumulating a large number of simultaneously held half-open handshakes.
How the attack works
- Entry point. The inbound TLS accept path on the peer-protocol listener (
51235,OverlayImpl::onHandoff→boost::asio::ssl::stream::async_handshake) and the JSON-RPC HTTPS listener (5006,protocol = https). - Preconditions. Network reachability to either port. No authentication and no completed TLS handshake are required — the attack lives entirely in the pre-handshake phase.
- Processing path. The attacker opens a TCP connection, sends a 5-byte partial TLS record header (
16 03 01 02 00— a Handshake record, TLS 1.0, with a declared body length of 512 bytes) and then stalls, never sending the 512-byte body the header promises. rippled allocates SSL-stream state for the connection and waits. - Resource/trust boundary crossed. Two, from one root cause. First, the inbound-vs-outbound handshake-deadline asymmetry: the outbound peer handshake is protected by a 15 s
boost::asio::steady_timerdeadline atConnectAttempt.cpp:153, but the inbound path installs no analogous deadline on either port. Second, the per-IPResource::Consumeradmission gate (OverlayImpl.cpp:235) bounds connection rate, not the number of half-opens a single IP can hold concurrently. - Resulting behaviour. Two distinct, measured impacts from the same root cause: (Bug A) the SSL-stream state allocated per half-open connection persists — it does not release on client TCP close, server-side TLS cleanup, or socket release — so server RSS grows roughly linearly with the number of concurrently held half-opens and does not recover; (Bug B) once concurrent half-opens exhaust the process's file-descriptor limit,
accept()returnsEMFILEand rippled'sServer::reopenAcceptorretry path tries to re-open the listening acceptor without releasing the old one, the re-bind fails (address already in use), the resulting exception is uncaught, and the process aborts.
Affected systems
- Chain: XRP Ledger.
- Implementation:
rippled(XRPLF/rippled). - Components: the inbound peer-protocol TLS listener (port
51235) and the JSON-RPC HTTPS listener (port5006). - Version measured:
rippledv3.1.3, via thexrpllabsofficial/xrpld:latestcommunity Docker image — a downstream packaging of upstreamXRPLF/rippled. The source-trace citations (OverlayImpl::onHandoff,ConnectAttempt.cpp:153,OverlayImpl.cpp:235,Server::reopenAcceptor) are read against upstreamXRPLF/rippled; a build compiled directly fromXRPLF/rippledHEAD was not separately measured, so confirmation against a from-source HEAD build remains outstanding. This is a finding on therippledimplementation and should not be generalised to every XRP Ledger client implementation. - Scope. Availability only — memory pin leading toward OOM, and file-descriptor exhaustion leading to a process crash. No consensus-safety break, no funds impact, no authentication bypass. A node whose inbound handshake is deadline-bounded and half-open-capped per source, running at a normal file-descriptor limit, is not affected by this specific gap.
Evidence
-
Publicly documented. None. This is NullRabbit's own research; no CVE or GitHub Security Advisory has been assigned, and the advisory characterises it as a rate-limit/handshake-hardening class rather than a novel implementation flaw.
-
Independently reproduced by NullRabbit. The source-trace (the inbound/outbound handshake-deadline asymmetry and the rate-not-count admission gate) was verified by reading the cited
XRPLF/rippledcode paths directly. A corpus reproducer for the primitiverippled_tls_slow_handshake(shipped inNullRabbit/nr-bundles-public) captures the attack's wire signature — many short TCP flows, each sending the 5-byte partial record header and then holding half-open, across several postures (single-IP, multi-source, and a paced RPC-HTTPS variant). The reproducer captures traffic against a mock endpoint that deliberately never completes the handshake; it does not itself stand up a liverippledinstance. The RSS-pin and crash impacts below were measured separately, directly against runningrippled. -
Measured under controlled conditions.
Loopback (rippled v3.1.3,
xrpllabsofficial/xrpld:latest, Docker host network, file-descriptor limit 65536, idle baseline 117 MB):Port Half-opens Δ persistent RSS Δ / connection 51235 (peer) 1,000 +97 MB 97 KB 51235 (peer) 5,000 +444 MB 89 KB 51235 (peer) 10,000 +745 MB 75 KB 5006 (RPC-HTTPS) 10,000 +342 MB 34 KB A 6-burst series across both ports pinned +1.85 GB of committed RSS from a single source IP, holding 32,000 half-open connections in total; post-close samples stayed within ±60 KB of the held samples, confirming the pin does not recover.
DigitalOcean production-shape confirmation (lon1 target, sfo3 attacker, ~140 ms RTT, stock Ubuntu 24.04, same
xrpllabsofficial/xrpld:latestimage, file-descriptor limit 65536): the peer-port pin reproduced within 5–15% of the loopback per-connection cost, at +712 MB persistent RSS for 10,000 half-opens.File-descriptor exhaustion (Bug B), default Linux file-descriptor limit of 1024: 1,000 half-opens from a single source IP crashed rippled (
SIGSEGV/ exit code 139) in about 14 seconds on loopback, and in the same wall-second as the attack burst on the DigitalOcean run. -
Inferred but not separately measured. The advisory models a sustained-attack path to OOM (roughly 75–88 MB of attack-attributable RSS growth per second at a sustained rate of about 1,000 new half-opens per second, which would exhaust a 12–16 GB validator's headroom in a few minutes) from the per-connection RSS-growth rate above. This sustained-rate projection was not run to an actual OOM event; the bursts that were measured (up to 10,000 concurrent half-opens per burst) are the directly observed data point.
Operator actions
Documented mitigations from the advisory, applicable ahead of any upstream fix:
- Install an inbound TLS-handshake deadline symmetric with the existing outbound
ConnectAttempt15 s timer, on both the peer (51235) and RPC-HTTPS (5006) acceptors, so a stalled handshake is torn down instead of held indefinitely. - Cap concurrent half-open handshakes per source IP — the existing
Resource::Consumerbudget bounds admission rate, not the number of connections one IP can hold open at once; a separate count-based cap is needed. - Run with a raised, normalised file-descriptor limit, and fix the
Server::reopenAcceptorretry path so that anEMFILEaccept failure closes the old acceptor fully before re-binding, and catches the bind exception, instead of aborting the process. - Do not expose the RPC-HTTPS port unauthenticated on a routable interface. Front it with a gateway that rate-limits and bounds handshake duration.
- Detection. Watch for sustained RSS growth uncorrelated with legitimate connection counts, a rising count of established-but-unhandshaked TLS sessions from a small number of source IPs, and — as a leading indicator of an imminent crash — repeated
accept: Too many open files/re-opening acceptorlog lines.
Canonical references
- NRDAX technique: NRDAX-T0099 — half-open-handshake-slowloris
- NullRabbit advisory: NR-2026-033 — XRP (rippled): inbound TLS listeners have no handshake timeout or per-IP half-open cap
- Evidence bundle (Hugging Face): NullRabbit/nr-bundles-public — primitive
rippled_tls_slow_handshake - Upstream:
XRPLF/rippled— inbound peer TLS listenerOverlayImpl::onHandoff(boost::asio::ssl::stream::async_handshake, peer port51235) and the JSON-RPC HTTPS listener (port5006); no independent CVE or GitHub Security Advisory has been assigned to this finding.
Related attacks
- NRDAX-T0099 is a cross-chain technique record; besides the
rippled/XRP Ledger instance covered here, it also has recorded instances on the Internet Computer, IOTA, and Solana codebases. This brief covers only therippledinstance measured in NR-2026-033 — it does not describe, and NullRabbit has not represented, those other instances as reproduced by this work. See the technique record for the full instance list.
Track attacks affecting rippled
New XRP Ledger and rippled infrastructure findings are catalogued as they are reproduced. Track attacks affecting rippled on NRDAX and in NullRabbit research.
Analysis plus reproducer. No weaponised proof-of-concept code.
Related Posts
How unauthenticated simulateTransaction requests saturate an Agave RPC node's executor pool
Agave runs the simulateTransaction handler synchronously on its shared Tokio executor threads, so unauthenticated, gas-free simulate requests pin those workers and add queue latency across the validator's entire JSON-RPC tier — not just the simulate path.
How CometBFT's SecretConnection handshake spends CPU before authenticating the peer
CometBFT completes the full SecretConnection STS handshake — an X25519 Diffie–Hellman exchange and an Ed25519 signature verification — for any connecting peer before checking whether that peer's node-ID is allowlisted, so an unauthenticated remote source can spend the node's asymmetric-crypto budget at will.
NR-2026-001 - Three Agave RPC architectural findings
Three architectural findings in the Agave JSON-RPC layer at v3.1.9: response amplification on getMultipleAccounts, Tokio executor saturation via simulateTransaction, and spawn_blocking pool saturation via getProgramAccounts. Architectural patterns, not rate-limit DoS - operator rate limits don't close them.
