Skip to content

[RWF] substrate-state-trie-migration-rpc: short child root panics migration_status #12610

Description

@xlc

Summary

In migration_status (lib.rs:98-106), values stored under child-storage-prefixed keys are treated as child trie root hashes and copied verbatim into an H::Out buffer using copy_from_slice. This call performs no length validation and panics when the stored value is not exactly the hash output length (e.g., 32 bytes for Blake2b256). Since the function is called through the StateMigrationApiServer::call RPC handler which fetches state via TrieCacheContext::Untrusted, processing adversarial or corrupted chain state that contains a wrong-length value under a child-storage-prefixed key will crash the node process rather than returning a graceful RPC error.

Severity / Sensitivity

Medium severity, non-sensitive public issue. This is reported from a confirmed Runtime Whitebox Fuzzer finding against public Polkadot SDK code.

Source Evidence

Full Relevant PoC Test

Paste the snippet below into an in-crate unit test module for the affected package. It is self-contained apart from helpers and types already available in that crate's existing test context.

use super::*;

use sp_core::storage::{well_known_keys, StateVersion};
use sp_core::Blake2Hasher;
use sp_state_machine::new_in_mem;

fn assert_migration_status_panic_safe<H, B>(
	backend: &B,
) -> std::result::Result<MigrationStatusResult, String>
where
	H: Hasher,
	H::Out: codec::Codec,
	B: AsTrieBackend<H>,
{
	let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
		migration_status::<H, B>(backend)
	}));
	match result {
		Ok(inner) => inner,
		Err(payload) => {
			let msg = payload
				.downcast_ref::<String>()
				.map(|s| s.clone())
				.or_else(|| payload.downcast_ref::<&'static str>().map(|s| s.to_string()))
				.unwrap_or_else(|| "unknown panic".to_string());
			panic!("migration_status panicked instead of returning Err: {}", msg);
		},
	}
}

fn assert_child_root_wrong_length_panic_safe(child_root_len: usize) {
	let mut child_key = well_known_keys::DEFAULT_CHILD_STORAGE_KEY_PREFIX.to_vec();
	child_key.extend_from_slice(b"bogus_child");
	let backend = new_in_mem::<Blake2Hasher>().update(
		vec![(None, vec![(child_key, Some(vec![0u8; child_root_len]))])],
		StateVersion::V1,
	);
	let _ = assert_migration_status_panic_safe::<Blake2Hasher, _>(&backend);
}

// RWF-RISKS: risk_264qyrx2
// RWF-INVARIANTS: inv_zdendbtr
#[test]
fn test_risk_264qyrx2_short_child_root_value_must_be_panic_safe() {
	assert_child_root_wrong_length_panic_safe(1);
}

Reproduction Command

cargo test -p substrate-state-trie-migration-rpc test_risk_264qyrx2_short_child_root_value_must_be_panic_safe -- --nocapture

Observed Result

The PoC fails on the current implementation with:

migration_status panicked instead of returning Err: source slice length (1) does not match destination slice length (32)

Expected Behavior

The target should preserve the invariant described above instead of producing the confirmed failure. In particular, the affected operation should reject malformed or inconsistent input, preserve durable state invariants, or return an error rather than silently corrupting state or panicking.

Validation Rationale

The public source shows migration_status reading child root values from trie iteration and copying them into H::Out with copy_from_slice without validating the source length. The RPC handler fetches state with TrieCacheContext::Untrusted, so malformed state should become an error, not a process panic. The PoC builds an in-memory trie with a child-storage-prefixed key whose value is 1 byte and asserts migration_status is panic-safe; it panics at the unchecked copy.

RWF Metadata

  • Found by Runtime Whitebox Fuzzer
  • Finding: find_6mg4iwewgs
  • Report: freport_nf7ib8bpng
  • Target: substrate-state-trie-migration-rpc
  • Run: 20260628-212807-a53980
  • Severity: medium
  • Category: Denial of Service
  • Invariants: inv_zdendbtr
  • Risks: risk_264qyrx2

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions