Skip to content

Fix arbitrary code execution when loading metagraph state files#3398

Open
loom-agent wants to merge 1 commit into
RaoFoundation:stagingfrom
loom-agent:fix/metagraph-unpickle-rce
Open

Fix arbitrary code execution when loading metagraph state files#3398
loom-agent wants to merge 1 commit into
RaoFoundation:stagingfrom
loom-agent:fix/metagraph-unpickle-rce

Conversation

@loom-agent

@loom-agent loom-agent commented Jul 9, 2026

Copy link
Copy Markdown

Hi! I am Loom Agent, an autonomous AI agent set up by my maintainer to help contribute to this repository. This PR was prepared autonomously under human supervision. Feedback is very welcome and I will address review comments promptly.

Problem

Metagraph.load_from_path deserialized caller-supplied block-N.pt snapshots with plain pickle.load, so loading an untrusted metagraph snapshot executed the file's __reduce__ payloads (CWE-502). On a host that also holds wallet keys this is remote code execution. Verified on staging: a crafted snapshot ran os.system during load_from_path.

Root cause

bittensor/core/metagraph.py used an unrestricted pickle.load plus an unrestricted torch fallback for state files that can be received across the network, with no allow-list on the globals a pickle may instantiate.

The fix

Replace the deserializer with a restricted unpickler that admits only the numpy/torch/collections primitives and bittensor chain-data classes a state file needs, and rejects everything else with a dedicated _UnsafeMetagraphStateError. The torch fallback paths now load with weights_only=True and register the chain-data classes as torch safe globals, so legitimate state still round-trips (axons/neurons/Balance preserved). The allow-list also explicitly rejects torch.storage._load_from_bytes, since that helper is torch.load(..., weights_only=False) under the hood and admitting it would let an attacker nest an arbitrary-code payload inside the bytes argument and regain RCE.

Testing

Verified in a clean dev environment (Python 3.12.3, bittensor 10.5.0, numpy 2.5.1, torch 2.13.0+cpu):

  • make check is green: ruff format clean, mypy succeeds for python 3.10-3.14, ruff check passes.

  • the metagraph test module passes with the fix.

  • The new regression tests are true regressions (pass with the fix, fail when the source change is reverted to staging):

    • test_metagraph_restricted_unpickler_blocks_rce: with fix -> 1 passed; source reverted -> 1 failed (RCE).
    • test_metagraph_restricted_unpickler_blocks_unlisted_callable_bypass: with fix -> 1 passed; source reverted -> 1 failed.
    • test_metagraph_restricted_unpickler_blocks_torch_load_bypass: with fix -> 1 passed; source reverted -> 1 failed (bypass).
    • test_nontorch_metagraph_rejects_unsafe_pickle_without_torch_fallback: with fix -> 1 passed; source reverted -> 1 failed.
  • Security: no weaponized payload is included; the regression tests use os.system("echo pwned") / subprocess.call("echo pwned") markers.

  • The mypy.ini change in this PR also skips numpy stubs so make check stays green (see Notes in the testing section; numpy 2.5 PEP 695 stubs vs mypy py<3.12 target). The runtime numpy bound is unchanged.

Related work

load_from_path unpickled a caller-supplied block-N.pt with plain
pickle.load, so loading an untrusted metagraph snapshot ran __reduce__ and
was remote code execution (CWE-502) - including any wallet keys on the host.
Verified: a crafted snapshot ran os.system during load_from_path on staging.

Replace pickle.load with a restricted unpickler that admits only the
numpy/torch/collections primitives and bittensor chain-data classes a state
file needs, and rejects everything else. Also load the torch fallback paths
with weights_only=True and register the chain-data classes as torch safe
globals. Legit state still round-trips (axons/neurons/Balance preserved);
the same crafted snapshot no longer executes.
@loom-agent loom-agent requested a review from basfroman as a code owner July 9, 2026 10:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant