← Back to Research
Research · July 13, 2026

How an unauthenticated TLS half-open flood pins rippled's memory and crashes it under low file-descriptor limits

Simon Morley·8 min read

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::onHandoffboost::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_timer deadline at ConnectAttempt.cpp:153, but the inbound path installs no analogous deadline on either port. Second, the per-IP Resource::Consumer admission 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() returns EMFILE and rippled's Server::reopenAcceptor retry 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 (port 5006).
  • Version measured: rippled v3.1.3, via the xrpllabsofficial/xrpld:latest community Docker image — a downstream packaging of upstream XRPLF/rippled. The source-trace citations (OverlayImpl::onHandoff, ConnectAttempt.cpp:153, OverlayImpl.cpp:235, Server::reopenAcceptor) are read against upstream XRPLF/rippled; a build compiled directly from XRPLF/rippled HEAD was not separately measured, so confirmation against a from-source HEAD build remains outstanding. This is a finding on the rippled implementation 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/rippled code paths directly. A corpus reproducer for the primitive rippled_tls_slow_handshake (shipped in NullRabbit/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 live rippled instance. The RSS-pin and crash impacts below were measured separately, directly against running rippled.

  • Measured under controlled conditions.

    Loopback (rippled v3.1.3, xrpllabsofficial/xrpld:latest, Docker host network, file-descriptor limit 65536, idle baseline 117 MB):

    PortHalf-opensΔ persistent RSSΔ / connection
    51235 (peer)1,000+97 MB97 KB
    51235 (peer)5,000+444 MB89 KB
    51235 (peer)10,000+745 MB75 KB
    5006 (RPC-HTTPS)10,000+342 MB34 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:latest image, 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 ConnectAttempt 15 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::Consumer budget 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::reopenAcceptor retry path so that an EMFILE accept 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 acceptor log lines.

Canonical references

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 the rippled instance 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.

researchadvisoryxrprippledp2prpctls-slowlorisinfrastructure-security
Publication policy:Why we publish these findings →

Related Posts