89353 bc medium unbounded pre auth pq noise setup saturates full node cpu before established peer limits apply
Submitted on Aug 18th 2026 at 18:58:28 UTC by @Porcupine17126 for Audit Comp | Quantus
Report ID: #89353
Report Type: Blockchain/DLT
Report severity: Medium
Target: https://github.com/immunefi-team/audit-comp-quantus-chain
Impacts:
Increasing network processing node resource consumption by at least 30% without brute force actions, compared to the preceding 24 hours
Description
Target
https://github.com/immunefi-team/audit-comp-quantus-chain
Tested node version: quantus-node 0.8.0-exodus-3b243b870a5
Tested competition revision: 3b243b870a5442a6e5f443056f67bbba87d472f5
Severity and exact in-scope impact
Suggested severity: Medium
Exact listed impact:
Increasing network processing node resource consumption by at least 30% without brute force actions, compared to the preceding 24 hours
Summary
Quantus configures litep2p's inbound limit as 10,000 established incoming connections. Pending inbound sockets are checked against the established set but are not reserved in that set. Therefore, while the node has zero authenticated peers, an arbitrary number of pending sockets can pass the check and enter transport negotiation concurrently.
This becomes a qualifying resource vulnerability because Quantus replaced upstream Noise with a post-quantum pqXX handshake. Immediately after an unauthenticated client negotiates the public /noise multistream protocol, the listener synchronously:
generates a fresh ML-KEM-768 keypair;
creates a Clatter
pqXXsession; andsigns the KEM public key with the node's ML-DSA-87 identity key.
Only after completing this work does the listener await pqXX message 1 from the remote socket. The attacker never needs to send that message, authenticate a PeerId, own an account, sign a transaction, pay a fee, or guess a value.
The loopback PoC reproduced in the Proof of Concept section sends only the two ordinary multistream frames—28 application bytes per connection—then stops before pqXX message 1. On the unmodified scoped full-node binary:
idle CPU: 0.0–0.2%;
256 plain TCP sockets: 1.1% peak CPU;
256 sockets negotiating
/noise: 78.8%, 95.4%, 98.4%, 99.0%, and 54.5% CPU in consecutive one-second samples;attack setup: 256/256 succeeded, zero errors, 4.625 seconds;
RPC
system_health.peers: 0 throughout; andrecovery after closure: 0.2% CPU, zero peers.
The attack consumed approximately 4.61 node CPU-seconds during the 4.625-second setup interval, while the matched plain-TCP control consumed about 0.06 CPU-seconds over its entire 18-second measurement. The clean attack therefore saturated one full network-processing core at roughly 99% and exceeded the listed 30% threshold by a wide margin. A separate 1,024-connection run kept the node at approximately 95–100% CPU for about 17 seconds; the attacker can repeat bounded bursts indefinitely.
Production reachability
The vulnerable code is the network backend used by quantus-node. Validator nodes default to raw TCP listeners, and the PoC uses the same TCP transport and /noise negotiation path on an explicit loopback listener. The test uses role FULL only to avoid authority mining contaminating the CPU baseline; the transport, connection manager, identity, and PQ Noise implementation are unchanged.
No request was sent to a Quantus-operated host. All impact measurements used an unmodified local node bound to 127.0.0.1.
Root cause
1. The configured inbound cap tracks established connections only
client/network/src/lib.rs:309-310 names the limit accordingly:
client/network/src/litep2p/mod.rs:515-522 installs that value as the only incoming connection limit:
client/litep2p/src/transport/manager/limits.rs:104-112 checks a pending socket only against incoming_connections.len():
The connection ID is inserted only later, after authentication and establishment, at lines 144-152:
client/litep2p/src/transport/manager/mod.rs:1374-1386 consequently accepts every pending inbound socket while the established set remains below 10,000:
There is no separate pending-handshake reservation, semaphore, or per-source admission bound.
2. Quantus performs expensive PQ work before reading attacker-controlled handshake data
At the beginning of every Noise handshake, client/litep2p/src/crypto/noise/mod.rs:769-772 calls NoiseContext::new:
NoiseContext::new creates a fresh KEM keypair, a Clatter session, and the signed identity payload:
assemble performs the ML-DSA identity signature:
client/litep2p/src/crypto/noise/protocol.rs:163-171 shows that the keypair is generated per context:
Only after all of that does the listener execute read_handshake_message for pqXX message 1 at noise/mod.rs:809-813. The PoC deliberately stops at this boundary.
Why this is not an upstream-only report
The generic established-connection bookkeeping exists in upstream litep2p 0.13.3. This report does not claim that bookkeeping alone as the qualifying impact.
The resource amplification comes from Quantus's in-repository rewrite of upstream Noise. Upstream v0.13.3 NoiseContext::new uses Snow's ordinary DH generate_keypair() and an Ed25519 identity. The scoped Quantus source instead creates a fresh ML-KEM-768 keypair, a four-message Clatter pqXX session, and an ML-DSA identity signature before reading pqXX message 1. Those Quantus-specific operations are precisely what the matched PoC triggers and measures.
The 256-socket plain control peaks at only 1.1% CPU; negotiating Quantus's custom /noise path with the same connection count reaches 99.0%. The eligible defect is therefore the Quantus PQ integration performing expensive custom work before any bounded admission, not a best-practice complaint about generic TCP or unmodified Substrate.
Why existing limits and timeout do not mitigate the impact
10,000 incoming limit: counts only authenticated, established connections. The PoC keeps the count at zero.
10-second transport timeout: limits how long one socket survives, but cannot recover the ML-KEM key generation and ML-DSA signing CPU already spent. Replenishment is faster than expiration.
OS descriptor/backlog limits: 256 sockets are sufficient; the test does not approach the local 1,048,575 descriptor limit.
Peer policy or identity checks: occur only after the expensive pre-authentication work.
Traffic/rate argument: each connection sends 28 application bytes and no PQ payload. The clean run uses only 7,168 total application bytes to saturate processing for about five seconds. Repeating the same deterministic burst sustains the load.
Recommended remediation
Add a small, separate bound for pending inbound handshakes and reserve a slot before accept_pending(connection_id). Release it on every success, rejection, error, close, and timeout path. The pending cap must not reuse the established-connection set.
Also consider per-source admission/rate limits and avoiding fresh ML-KEM key generation plus ML-DSA signing before a bounded admission or stateless anti-abuse step. The established 10,000-peer limit should remain independent.
Add regressions which:
configure a small pending-handshake cap;
open more sockets than the cap without authenticating;
assert excess sockets are rejected before
NoiseContext::new;verify reservations are released on timeout and failed negotiation; and
confirm an attacker cannot starve a legitimate incoming handshake.
Public-known-issue check
The listed Quantus Substrate audit does not cover litep2p, PQ Noise, or connection admission. Searches of the scoped repository, upstream litep2p issues, and upstream changelog found adjacent public work for established-connection limits, rejected-connection accounting, and handshake timeouts, but no disclosure of unbounded pending PQ handshakes or the demonstrated pre-auth ML-KEM/ML-DSA CPU amplification.
Private duplicate risk remains possible, as in any live audit competition.
Proof of Concept
Save the following as poc.py and run it only against a local node.
Environment
Was this helpful?