[2025-05-23] Polkadot-SDK: Transaction Depth Limit

On May 23, 2025, we were notified of a security vulnerability in Polkadot-SDK. The report described an incident that occurred two days earlier on Bittensor. It stated that sending a batch transaction containing 256 nested calls would cause the chain to stall. After an initial investigation, we reproduced the issue and determined it was a critical vulnerability affecting all Polkadot-SDK users since 2020. We quickly fixed the underlying issue and began notifying all known Polkadot-SDK users.

What was problem?

As mentioned above, an “attacker” only needed to send a batch transaction containing 256 nested calls. The chain accepted the transaction and included it in a block. The problem arose when any node attempted to import the block containing this transaction. Each node failed to execute the check_inherents call for the block. Because this call failed, the node assumed the block was invalid and began banning the peer that sent this block. Since the transaction had already propagated through the network, every block producer included it in its block. This ultimately led all block producers to ban one another. Although the bans were lifted after a short time, the nodes would reconnect, attempt to import the block again, and the cycle would restart. A potential temporary solution would have been to blacklist the transaction and remove any blocks containing it. However, an adversary could have sent a similar transaction with a different hash, causing the issue to recur. Let’s examine why the block producer accepted the transaction while importing nodes did not.

The check_inherents function accepts the block as an argument and performs checks on its inherents. This function is provided by the runtime, meaning it executes in WASM. To invoke a WASM function, we SCALE-encode all parameters on the host and SCALE-decode them in the runtime. This decoding failed because the decoding depth exceeded 256. The decoding depth is set to 256 to constrain transaction stack-size usage. The depth is tracked during type decoding by incrementing and decrementing a counter. When the counter reaches its maximum, the decoding returns an error, as it did in this case. In our scenario, the transaction carries 256 nested batch calls, meaning it utilizes the maximum allowed decoding depth. When we call check_inherents, the block containing these transactions is passed as a parameter. The block stores the transactions in a vector, and decoding the vector increments the depth counter by one. Thus, attempting to decode another 256 nested batch calls causes the counter to reach its limit, and decoding fails. The underlying issue is that the decoding depth should apply only to transactions, but the current implementation applies it to every parameter passed to a runtime API function.

How to fix it?

Resolving the root cause was straightforward. As demonstrated above, the issue stems from decoding all parameters under the depth limit. The first step was to remove the decoding depth limit from runtime API parameter decoding. Next, we ensured that only transactions maintain a decoding depth limit of 256. The final patch is available here.

You can verify whether your chain is patched by running the following script in polkadot-js:

const ALICE = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';  

let depth = 256; 
let call = api.tx.system.remark("foo"); 

for (let i = 0; i < depth; i++) {
	call = api.tx.utility.batch([call]); 
}

await call.signAndSend(ALICE);

If the chain does not stall, it is secure :slight_smile:

Fixed Versions

We have backported the fix to the following branches and releases:

  • stable2503
  • stable2412
  • stable2409
  • stable2407
  • v1.11.0
  • v1.10.0
  • v1.7.2
  • v1.7.0
  • v1.6.0
  • v1.1.0
  • v1.0.0
  • v0.9.42

This list is not exhaustive. If you do not see your version here and require the fix for a different release or branch, please reach out to us.

For anyone who discovers other security issues and would like to earn a bounty, please submit details via: https://www.parity.io/bug-bounty.

7 Likes