Skip to content

fix(cli): abort on corrupt config.json instead of falling through to mainnet defaults - #817

Closed
plind-junior wants to merge 2 commits into
entrius:testfrom
plind-junior:fix/load-config-silent-json-error
Closed

fix(cli): abort on corrupt config.json instead of falling through to mainnet defaults#817
plind-junior wants to merge 2 commits into
entrius:testfrom
plind-junior:fix/load-config-silent-json-error

Conversation

@plind-junior

@plind-junior plind-junior commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Problem

load_config() in gittensor/cli/issue_commands/helpers.py and the parallel _load_config_value() in gittensor/cli/miner_commands/helpers.py both catch json.JSONDecodeError and return {} / None with no log line, no warning, no exit code. Every command that reads ~/.gittensor/config.json then proceeds as if the operator had configured nothing.

This is a problem because the silent-empty-config path collapses to mainnet:

  • resolve_network() walks through CLI arg → config → env → default; on empty config the chain ends at NETWORK_MAP['finney'] (gittensor/cli/issue_commands/helpers.py).
  • get_contract_address() falls through to the hardcoded mainnet CONTRACT_ADDRESS in gittensor/constants.py.

So a config file that exists but parses badly silently re-targets every CLI hot path at finney + mainnet contract.

Location

gittensor/cli/issue_commands/helpers.py (the load_config() body):

if CONFIG_FILE.exists():
    try:
        with open(CONFIG_FILE, 'r') as f:
            return json.load(f)
    except (json.JSONDecodeError, IOError):
        pass
return {}

Same shape in gittensor/cli/miner_commands/helpers.py::_load_config_value(). There are at least 8 commands (vote, register, harvest, view, submissions, predict, miner post, miner check) that go through one of these two functions.

How the file becomes briefly invalid

The fix should defend against any of these — they're all real:

  1. Interrupted gitt config set. The current writer is non-atomic (Path.write_text truncates first), so a Ctrl-C / OOM / host shutdown mid-write leaves the file partial. (This is the write-side half — see C6.)
  2. Manual edit error. Operator opens ~/.gittensor/config.json in $EDITOR, mistypes a comma or a quote, saves.
  3. External tool corruption. scp, rsync --partial, sync-conflict tools, dotfile managers can produce a half-file at the destination.
  4. Host crash without fsync. Path.write_text doesn't fsync — power loss within the writeback window leaves zeros / truncation.
  5. Permission flipping. chmod 0 on the config doesn't reach this branch (raises IOError, see below) — but a file that exists and is read-protected by a UID change while the validator is running still hits the broken path.

The point is that the operator did configure the CLI; the fall-through pretends they didn't.

Why it's load-bearing

The CLI hot path includes operator-only commands: gitt vote solution <issue_id> <solver_hotkey> <solver_coldkey> <bounty> and gitt issues register --repo … --bounty …. These are typically run from automation (cron, scripts), where:

  • print_network_header(...) prints the resolved network and contract, but it's one line in a Rich panel; non-interactive runs and CI output capture often miss it.
  • confirm_or_abort(...) auto-skips on non-TTY, so the "are you sure?" guard isn't a backstop.
  • The vote / register transactions are signed and submitted — at best they fail on the chain layer (wrong contract address); at worst they succeed against the wrong target.

There's no recovery from a vote committed to the wrong network.

Distinction from related findings

  • C2 (typed config set whitelist) — write-side input validation, doesn't touch reads.
  • C6 (config set parse-then-wipe + non-atomic write) — write-side; closes the most common path into the corrupt state. Even after C6, paths 2–5 above still produce corrupt files, and C7's silent-read path still triggers.
  • Consolidate duplicated CLI endpoint/config resolution logic #623 (consolidate CLI endpoint/config resolution) — refactor / DRY-up of the two helper functions; doesn't claim the silent fallback is a bug.

So the read-side bug is independently exploitable and survives the fix to the write side.

Fix

On JSONDecodeError, abort the CLI with a clear message that points at the file and instructs the operator to inspect or remove it. Do not return {} / None.

IOError / OSError (file genuinely unavailable — missing, permission-denied) is fine to treat as "no config" — those are legitimate "operator hasn't configured this yet" states. The asymmetry is the point: a file that does not exist is "no config"; a file that exists and parses badly is operator data corruption and must not be silently ignored.

Suggested message: name the path, name the parse error, instruct the operator to inspect/remove, and exit non-zero. Roughly five lines added per call site.

btcli precedent

btcli upstream PR latent-to/btcli#800 ("fix: JSON output empty for btcli subnets list --json-out") was caused by the same shape — an exception was silently swallowed and produced empty output instead of an error. btcli's own Config.__init__ reads YAML and lets parse errors propagate with no try/except around the load. Gittensor's CLI is strictly more silent than btcli on the same surface.

Dup-check

Searched closed + open issues and PRs for: load_config, JSONDecodeError config, config silent, resolve_network finney fallback, mainnet fallback, config in:title. The closest neighbour is #623 (refactor; doesn't address behaviour). No issue currently claims the silent fallback is a correctness bug.

Fixes #816

before:
image

after:
image

@xiao-xiao-mao xiao-xiao-mao Bot added the bug Something isn't working label Apr 27, 2026
plind-junior added a commit to plind-junior/gittensor that referenced this pull request Apr 28, 2026
entrius#845)

When `~/.gittensor/config.json` failed to parse, `config_set` printed a
yellow warning, set `config = {}`, then wrote a fresh single-key file —
silently destroying every previously-configured key (network,
contract_address, ws_endpoint, hotkey, ...).

Mirror PR entrius#817's read-side fix: `JSONDecodeError` now aborts with a clear
message and `SystemExit(1)`, leaving the corrupt file untouched so the
operator can inspect or recover it.

New regression tests in `tests/cli/test_config_set.py` cover the
abort-with-nonzero-exit, byte-for-byte preservation of the corrupt file,
non-regression of the valid-file merge path, and the first-run case.

Closes entrius#845
…mainnet defaults

`load_config` and `_load_config_value` swallowed `json.JSONDecodeError` and
returned `{}` / `None`, silently retargeting commands at finney mainnet and
the hardcoded contract address. A briefly-corrupt config (interrupted
`gitt config set`, manual edit typo, host crash mid-write) would let the
next `gitt vote ...` or `gitt issues register ...` re-target mainnet
without warning.

Now: `JSONDecodeError` aborts with a clear message; `IOError` / `OSError`
(missing or permission-denied) still falls through to defaults, since
those are legitimate "no config yet" states.
entrius#845)

When `~/.gittensor/config.json` failed to parse, `config_set` printed a
yellow warning, set `config = {}`, then wrote a fresh single-key file —
silently destroying every previously-configured key (network,
contract_address, ws_endpoint, hotkey, ...).

Mirror PR entrius#817's read-side fix: `JSONDecodeError` now aborts with a clear
message and `SystemExit(1)`, leaving the corrupt file untouched so the
operator can inspect or recover it.

New regression tests in `tests/cli/test_config_set.py` cover the
abort-with-nonzero-exit, byte-for-byte preservation of the corrupt file,
non-regression of the valid-file merge path, and the first-run case.

Closes entrius#845
@plind-junior
plind-junior force-pushed the fix/load-config-silent-json-error branch from b795812 to e838b7f Compare April 29, 2026 20:34
@anderdc

anderdc commented Apr 30, 2026

Copy link
Copy Markdown
Collaborator

Closing.

@anderdc anderdc closed this Apr 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

load_config silently swallows JSONDecodeError, falling through to finney mainnet defaults

2 participants