-
Notifications
You must be signed in to change notification settings - Fork 13
request canister install --argument silently corrupts Candid variant and integer types #623
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 infoWithout 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
}