Skip to content

[RWF] substrate-state-trie-migration-rpc: long child root panics migration_status #12609

Description

@xlc

Summary

In migration_status (lib.rs:98-106), child trie root values read from storage are copied directly into H::Out via child_root.as_mut()[..].copy_from_slice(&root[..]) with no length validation. If a child-storage-prefixed key stores a value whose length differs from H::Out (32 bytes for Blake2Hasher), the copy_from_slice call panics. Since this function is called from the StateMigration RPC handler call() which fetches state with TrieCacheContext::Untrusted, a malformed or adversarial trie can trigger a process-crashing panic instead of a graceful error response. The fix should validate root.len() == H::Out::len() before the copy and return an Err on mismatch.

Severity / Sensitivity

High 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_sij8a47m,risk_bu9ped8i
// RWF-INVARIANTS: inv_zdendbtr
#[test]
fn test_risk_sij8a47m_bu9ped8i_long_child_root_value_must_be_panic_safe() {
	assert_child_root_wrong_length_panic_safe(64);
}

Reproduction Command

cargo test -p substrate-state-trie-migration-rpc test_risk_sij8a47m_bu9ped8i_long_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 (64) 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 64 bytes and asserts migration_status is panic-safe; it panics at the unchecked copy.

RWF Metadata

  • Found by Runtime Whitebox Fuzzer
  • Finding: find_et38wze6w8
  • Report: freport_qnr2s96cm9
  • Target: substrate-state-trie-migration-rpc
  • Run: 20260628-212807-a53980
  • Severity: high
  • Category: panic-on-adversarial-input
  • Invariants: inv_zdendbtr
  • Risks: risk_sij8a47m, risk_bu9ped8i

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