dfinity / dfinity/orbit

`request canister install --argument` silently corrupts Candid variant and integer types

Open
#623 0 comments 0 reactions 0 assignees View on GitHub

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:

  1. Infers integer types incorrectly8 becomes int instead of nat8
  2. Creates incomplete variant type tablesvariant { ICRC1 } doesn't know ICRC2/ICRC3 exist

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 Nonedropping 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.