`request canister install --argument` silently corrupts Candid variant and integer types
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 43
- Forks
- 18
- Avg merge
- 2d 19h
- Merged PRs (30d)
- 6
Description
Summary
dfx-orbit request canister install --argument encodes Candid text without type information (parse_idl_args().to_bytes()), causing silent data corruption with no error. Data passes encoding, passes checksum verification, but decodes incorrectly on the canister.
Root Cause
In tools/dfx-orbit/src/canister/util.rs, parse_arguments() encodes Candid text without a type environment:
candid_parser::parse_idl_args(&arg_string)
.with_context(|| "Invalid Candid values".to_string())?
.to_bytes() // ← encodes without type info
Without type information, the Candid encoder:
- Infers integer types incorrectly —
8becomesintinstead ofnat8 - Creates incomplete variant type tables —
variant { ICRC1 }doesn't knowICRC2/ICRC3exist
This is a fundamental Candid text format limitation — the text cannot express full type definitions.
Bug 1: opt fields silently become None
When untyped encoding encodes 8 as int but the canister expects nat8 (e.g. decimals field), the int→nat8 subtyping check fails at decode time. Because the field is wrapped in opt, Candid's upgradeability rule silently coerces the failure to None — dropping all data without any error.
Reproduction
#[test]
fn test_untyped_encoding_causes_silent_data_loss() {
#[derive(candid::CandidType, candid::Deserialize, Debug)]
struct InitData {
owner: Principal,
tokens: Option<Vec<Token>>,
}
#[derive(candid::CandidType, candid::Deserialize, Debug)]
struct Token {
decimals: u8, // expects nat8
name: String,
}
// Untyped encoding: 8 encoded as int (not nat8)
let args = r#"(record {
owner = principal "aaaaa-aa";
tokens = opt vec { record { decimals = 8; name = "ICP" } }
})"#;
let bytes = candid_parser::parse_idl_args(args).unwrap().to_bytes().unwrap();
let result: (InitData,) = candid::decode_args(&bytes).unwrap();
// No error thrown, but tokens silently became None!
assert!(result.0.tokens.is_none()); // BUG: data silently dropped
}
Bug 2: Variant type collapse
Candid text variant { ICRC2 } doesn't tell the encoder that ICRC1 and ICRC3 also exist. The encoder creates a type table with only one variant. On decode, all variants map to index 0.
Reproduction (CLI)
#[test]
fn test_untyped_encoding_causes_variant_collapse() {
#[derive(candid::CandidType, candid::Deserialize, Debug, PartialEq)]
enum IcrcStandard {
ICRC1,
ICRC2,
ICRC3,
}
// Encode three distinct variants WITHOUT type info
let untyped = r#"(vec {
variant { ICRC1 };
variant { ICRC2 };
variant { ICRC3 }
})"#;
let bytes = candid_parser::parse_idl_args(untyped)
.unwrap()
.to_bytes()
.unwrap();
let result: (Vec<IcrcStandard>,) = candid::decode_args(&bytes).unwrap();
// BUG: all three decode as ICRC1!
assert_eq!(result.0.len(), 3);
assert_eq!(result.0[0], IcrcStandard::ICRC1);
assert_eq!(result.0[1], IcrcStandard::ICRC1); // should be ICRC2
assert_eq!(result.0[2], IcrcStandard::ICRC1); // should be ICRC3
}
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Read tools/dfx-orbit/src/canister/util.rs and trace parse_arguments from the request canister install --argument entry point. Use the two reproductions in the issue to verify behavior, with completion meaning integer values retain their expected types and distinct Candid variants no longer collapse during decoding.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100