← Back to Research
Research · July 13, 2026

How unbounded HTTP/2 concurrent streams let one TCP connection OOM-kill an IOTA node's public gRPC server

Simon Morley·7 min read

An IOTA node's public gRPC server accepts an unbounded number of concurrent HTTP/2 streams on a single TCP connection. One client that opens a single connection and multiplexes around 200 concurrent GetTransactions calls, each requesting a large batch, drives the server's resident memory from a few hundred MB to multiple GB in around 10 seconds — until the kernel OOM-killer terminates the node process. No large bandwidth is required, and there is no idle eviction: one source IP, one burst, node down, restart required.

This is an availability issue only — no memory corruption, no funds or consensus impact.

Why this matters

Anyone running an IOTA node with the public gRPC API enabled is exposed — including the IOTA Foundation's own grpc.mainnet.iota.cafe endpoint and third-party RPC providers. The consequence is not degraded service but the whole node process dying: gRPC and JSON-RPC both go down together, and if the node is a validator, consensus participation stops until an operator restarts it. Because the underlying iota-localnet/iota-node binary is what gets OOM-killed, this is process-level, not method-level, unavailability.

A naïve rate limit does not catch this. The attack is a single burst of roughly 200 requests on one connection, then the attacker just holds the streams open — there is no request-rate spike to detect, and the server keeps buffering per-stream response state regardless of how slowly (or never) the client reads it back. An HTTP/2 concurrent-stream cap at the edge (for example, a CDN's own post-CVE-2023-44487 default) only forces the attacker onto two or three TCP connections instead of one; it does not bound the uncapped backend, so the kill still lands.

How the attack works

  • Entry point. The IOTA node's public gRPC LedgerService, specifically the unary GetTransactions call (/iota.grpc.v1.ledger_service.LedgerService/GetTransactions). Remote, unauthenticated, reachable over a single TCP connection.
  • Preconditions. Any IOTA node exposing the public gRPC API whose tonic Server builder does not set .max_concurrent_streams(N). In that state the server's HTTP/2 SETTINGS frame advertises max_concurrent_streams = u32::MAX — effectively unlimited — to any connecting client.
  • Processing path. The tonic server is constructed at iota-grpc-server/src/server.rs:185 via tonic::transport::Server::builder() with no .max_concurrent_streams(N) call added, so the HTTP/2 layer inherits an unbounded per-connection concurrent-stream count. The node does have a concurrency cap — default_grpc_api_max_concurrent_stream_subscribers = 1024 (iota-config/src/node.rs:341-347) — but it is scoped only to the StreamCheckpoints server-streaming subscription; it does not apply to GetTransactions, GetObjects, or GetCheckpoint, the unary calls this attack multiplexes.
  • Resource boundary crossed. Server-side memory. Each in-flight GetTransactions call accumulates its response state (serialized batch data) in memory independent of the client's receive rate — there is no receive-rate backpressure bounding retained memory, so concurrency alone, not bandwidth, is the multiplier.
  • Resulting behaviour. Memory grows roughly linearly with the number of concurrent open streams — measured at approximately 100 MB resident memory per concurrent stream — with no ceiling but the host's available RAM, ending in a kernel OOM-kill of the node process. The node recovers only via operator restart; if the burst continues, it is killed again.

Affected systems

  • Chain: IOTA.
  • Implementation: IOTA node (iota-node; upstream repository iotaledger/iota).
  • Component / method: public gRPC LedgerService, unary GetTransactions (and the other unary ledger reads — GetObjects, GetCheckpoint — which share the same uncapped concurrency).
  • Version tested. The reproduction was run against a fresh-genesis IOTA node at version v1.23.2, on a 4 vCPU / 8 GB RAM Ubuntu 24.04 lab host. The underlying gap — no .max_concurrent_streams(N) set on the tonic server builder — is a configuration default in the server code rather than a version-pinned regression; this brief makes no claim about which other IOTA node releases do or do not carry the same default, only about the version measured.
  • Vendor response. IOTA's security contact is [email protected]. IOTA does not run a public paid bug-bounty program (the legacy program is defunct), and node-availability/DoS is not a paid-impact category in any case, so this finding is out of scope for any bounty. There is no embargo: the finding rests on NullRabbit's own measurement against a self-owned lab node, and it is published as an operator advisory rather than a coordinated disclosure.

Evidence

  • Publicly documented. None. There is no external CVE or GHSA reference for this finding — it is original NullRabbit research.
  • Independently reproduced by NullRabbit. A code-level reproducer accompanies NR-2026-003 (reproducers/): it opens one TCP connection to a local gRPC endpoint, multiplexes N concurrent GetTransactions calls, and samples the node process's RSS once per second. It targets only a self-owned, locally-run node — it is not a turnkey tool and does not reference or target any public or mainnet endpoint.
  • Measured under controlled conditions. On an 8 GB lab node: RSS climbed from 472 MB to 7.6 GB in approximately 10 seconds, then the kernel OOM-killer terminated the process (dmesg: oom-kill ... anon-rss:7798928kB). The trigger was roughly 200 concurrent GetTransactions streams on one TCP connection, each stream requesting a batch of 500 digests. The measured slope was approximately 100 MB RSS per concurrent stream.
  • Inferred but not separately reproduced. The advisory extrapolates the measured ~100 MB/stream slope to other host sizes: roughly 70–80 concurrent streams would exhaust an 8 GB host, roughly 150 a 16 GB host, and roughly 300 a 32 GB host. Only the 8 GB case was directly measured to a kill; the 16 GB and 32 GB figures are linear extrapolations from the measured slope, not independently reproduced OOM events on hosts of those sizes.

Operator actions

  • Server-side fix (the one that closes it). Bound the tonic gRPC server with Server::builder().max_concurrent_streams(N), for example N = 64. This caps per-connection concurrent streams at the transport layer, so RSS plateaus instead of climbing without bound.
  • Edge/CDN controls reduce but do not remove exposure. An HTTP/2 concurrent-stream cap at the edge forces the attacker onto two or three TCP connections instead of one; it does not save an uncapped backend, so it is not a substitute for the server-side fix.
  • Exposure check. An operator can check their own node's HTTP/2 SETTINGS frame on the public gRPC port: if max_concurrent_streams is advertised as u32::MAX (effectively unlimited) rather than a bounded value, the tonic server builder has not been configured with .max_concurrent_streams(N) and the node is exposed as described above.

Canonical references

Related attacks

  • NRDAX-T0064 — endpoint-concurrency-cap-exhaustion on IOTA — advisory NR-2026-009, primitive iota_grpc_stream_cap_dos. The same public gRPC surface, but a different mechanism and resource: a single IP fills the StreamCheckpoints subscriber cap (1024, process-global) and denies all other legitimate subscribers, rather than exhausting host memory.
  • NRDAX-T0329 — unbounded-rpc-response-amplification on IOTA — advisory NR-2026-031, primitive iota_f10_grpc_batch_amp. Response-size amplification on the same GetObjects/GetTransactions unary reads, on a single stream rather than via concurrency; the two mechanisms compound when combined on one connection.

Track attacks affecting IOTA

New IOTA node findings are catalogued as they are reproduced. Track attacks affecting IOTA on NRDAX and in NullRabbit research.


Analysis plus reproducer. No weaponised proof-of-concept code.

researchadvisoryiotaiota-nodegrpcmemory_ampinfrastructure-security
Publication policy:Why we publish these findings →

Related Posts