Skip to content

Release candid 0.10.28 and candid_parser 0.3.2#731

Merged
lwshang merged 3 commits into
masterfrom
fix/leb128-decode-truncation
May 20, 2026
Merged

Release candid 0.10.28 and candid_parser 0.3.2#731
lwshang merged 3 commits into
masterfrom
fix/leb128-decode-truncation

Conversation

@lwshang
Copy link
Copy Markdown
Contributor

@lwshang lwshang commented May 20, 2026

Summary

  • Fix LEB128/SLEB128 fast path silently truncating Nat/Int values near the u64/i64 boundary during decoding (ecc0c45)
  • Fix Int::decode truncating large magnitudes due to fast-path leakage (d08b8da)
  • Bump candid 0.10.27 → 0.10.28 and candid_derive to match
  • Cut candid_parser 0.3.2, which moves the previously unreleased Motoko Float32 binding fix into a dated release
  • Update CHANGELOG.md with a 2026-05-20 entry covering both crates

Test plan

  • cargo check -p candid -p candid_derive -p candid_parser
  • Reviewer to confirm changelog wording / release scope
  • Publish to crates.io after merge

🤖 Generated with Claude Code

lwshang and others added 3 commits May 20, 2026 14:50
…ndary

The fast paths added in #721 for deserialize_nat/deserialize_int call
hand-rolled LEB128 readers that bail to the bignum decoder on overflow.
The overflow check ran after `shift += 7` and only fired at shift >= 70,
but the in-loop write `result |= low << shift` already truncated high
bits at shift = 63 — silently returning a wrong value instead of
signalling overflow. e.g. `Nat::from(1u128 << 64)` round-tripped to 0.

Lower the bailout threshold so the fast path only handles values
encodable in 9 LEB128 bytes (fits in 63 bits). Anything that would
require a 10th byte falls through to the existing bignum decoder, which
handles arbitrary precision correctly. This also lets us drop the
`if shift < 64` guard in the hot loop body and the analogous guard in
the sign-extension step.

Tradeoff: u64 values in [2^63, u64::MAX] and i64 values like i64::MIN
now take the bignum path even though they fit. The fast path stays
branch-light, and the affected range is well outside the common case
(small counters, token amounts up to ~9.2 * 10^18).

Includes a regression test reproducing the original bug report.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…kage

The small-value fast path in `Int::decode` (#717) keeps the running
value in an `i64` until it can prove a chunk doesn't fit in i64, at
which point it lifts to a `BigInt`. At shift in [57, 64), `fits_i64`
checks whether the chunk's non-data bits match the sign-extension
pattern — but it did so without requiring the byte to be terminal.

For an input like `i128::MAX` (LEB128: 18 × 0xff, then 0x01), iteration
10 has shift=63, byte=0xff (continuation set). The check looks at
`low_bits | !0x7f == -1` and concludes "fits", then runs
`small |= low_bits << 63`, which silently truncates bits 1..6 of
`low_bits`. The corrupted `i64` (now -1) is then carried into the
BigInt path as the seed, and the final decoded value is -1.

Require `byte & 0x80 == 0` in the boundary branch so the check only
declares "fits" on a terminal byte; otherwise the BigInt path takes
over with the correct seed before any truncating shift happens.

Includes a regression test covering i128::MAX/MIN, i128 mid-range, and
beyond-i128 values for both signs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Name Max Mem (Kb) Encode Decode
blob 4_224 4_207_460 2_121_399
btreemap 75_456 529_878_557 ($\textcolor{red}{0.00\%}$) 10_161_391_774
double_option 128 1_322_108 ($\textcolor{green}{-0.08\%}$) 26_966_185 ($\textcolor{red}{0.18\%}$)
large_variant 320 1_039_871 ($\textcolor{green}{-0.02\%}$) 20_310_031 ($\textcolor{red}{0.02\%}$)
multi_arg 64 552_261 ($\textcolor{red}{0.13\%}$) 6_282_713
nns 192 2_006_575 5_514_809 ($\textcolor{green}{-0.18\%}$)
nns_list_neurons 1_152 6_596_503 ($\textcolor{green}{-0.03\%}$) 209_435_552 ($\textcolor{red}{0.12\%}$)
nns_list_proposal 1_216 6_972_264 ($\textcolor{red}{0.05\%}$) 55_006_346 ($\textcolor{green}{-0.00\%}$)
option_list 128 727_567 ($\textcolor{green}{-0.00\%}$) 16_212_689 ($\textcolor{red}{0.09\%}$)
result_variant 192 1_378_553 ($\textcolor{red}{0.01\%}$) 16_140_563
subtype_decode 512 2_664_248 ($\textcolor{green}{-0.06\%}$) 49_306_687 ($\textcolor{red}{0.02\%}$)
text 6_336 4_204_312 7_877_260
variant_list 128 721_955 ($\textcolor{green}{-0.07\%}$) 15_625_445 ($\textcolor{green}{-0.07\%}$)
vec_int16 12_480 8_404_598 249_585_711
vec_nat 11_008 66_046_873 276_028_299
vec_nat32 24_768 16_793_206 243_294_536
vec_nat64 49_344 33_570_406 251_683_398
vec_service 64 689_583 94_833_660
wide_record 1_152 3_264_617 ($\textcolor{green}{-0.13\%}$) 44_558_381 ($\textcolor{green}{-0.00\%}$)
  • Parser cost: 15_587_601 ($\textcolor{red}{0.00\%}$)
  • Extra args: 2_861_930
Click to see raw report
---------------------------------------------------

Benchmark: blob
  total:
    instructions: 6.33 M (no change)
    heap_increase: 66 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 4.21 M (no change)
    heap_increase: 66 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 2.12 M (no change)
    heap_increase: 0 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: btreemap
  total:
    instructions: 10.69 B (0.00%) (change within noise threshold)
    heap_increase: 1179 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 529.88 M (0.00%) (change within noise threshold)
    heap_increase: 159 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 10.16 B (no change)
    heap_increase: 1020 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: double_option
  total:
    instructions: 28.29 M (0.17%) (change within noise threshold)
    heap_increase: 2 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 1.32 M (-0.08%) (change within noise threshold)
    heap_increase: 0 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 26.97 M (0.18%) (change within noise threshold)
    heap_increase: 2 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: extra_args
  total:
    instructions: 2.86 M (no change)
    heap_increase: 0 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: large_variant
  total:
    instructions: 21.35 M (0.02%) (change within noise threshold)
    heap_increase: 5 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 1.04 M (-0.02%) (change within noise threshold)
    heap_increase: 2 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 20.31 M (0.02%) (change within noise threshold)
    heap_increase: 3 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: multi_arg
  total:
    instructions: 6.84 M (0.01%) (change within noise threshold)
    heap_increase: 1 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 552.26 K (0.13%) (change within noise threshold)
    heap_increase: 1 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 6.28 M (no change)
    heap_increase: 0 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: nns
  total:
    instructions: 23.94 M (-0.04%) (change within noise threshold)
    heap_increase: 3 pages (no change)
    stable_memory_increase: 0 pages (no change)

  0. Parsing (scope):
    calls: 1 (no change)
    instructions: 15.59 M (0.00%) (change within noise threshold)
    heap_increase: 3 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 2.01 M (no change)
    heap_increase: 0 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 5.51 M (-0.18%) (change within noise threshold)
    heap_increase: 0 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: nns_list_neurons
  total:
    instructions: 216.03 M (0.12%) (change within noise threshold)
    heap_increase: 18 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 6.60 M (-0.03%) (change within noise threshold)
    heap_increase: 18 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 209.44 M (0.12%) (change within noise threshold)
    heap_increase: 0 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: nns_list_proposal
  total:
    instructions: 61.98 M (0.00%) (change within noise threshold)
    heap_increase: 19 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 6.97 M (0.05%) (change within noise threshold)
    heap_increase: 5 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 55.01 M (-0.00%) (change within noise threshold)
    heap_increase: 14 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: option_list
  total:
    instructions: 16.94 M (0.08%) (change within noise threshold)
    heap_increase: 2 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 727.57 K (-0.00%) (change within noise threshold)
    heap_increase: 0 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 16.21 M (0.09%) (change within noise threshold)
    heap_increase: 2 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: result_variant
  total:
    instructions: 17.52 M (0.00%) (change within noise threshold)
    heap_increase: 3 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 1.38 M (0.01%) (change within noise threshold)
    heap_increase: 1 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 16.14 M (no change)
    heap_increase: 2 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: subtype_decode
  total:
    instructions: 51.97 M (0.02%) (change within noise threshold)
    heap_increase: 8 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 2.66 M (-0.06%) (change within noise threshold)
    heap_increase: 8 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 49.31 M (0.02%) (change within noise threshold)
    heap_increase: 0 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: text
  total:
    instructions: 12.08 M (no change)
    heap_increase: 99 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 4.20 M (no change)
    heap_increase: 66 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 7.88 M (no change)
    heap_increase: 33 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: variant_list
  total:
    instructions: 16.35 M (-0.07%) (change within noise threshold)
    heap_increase: 2 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 721.96 K (-0.07%) (change within noise threshold)
    heap_increase: 0 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 15.63 M (-0.07%) (change within noise threshold)
    heap_increase: 2 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: vec_int16
  total:
    instructions: 257.99 M (no change)
    heap_increase: 195 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 8.40 M (no change)
    heap_increase: 130 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 249.59 M (no change)
    heap_increase: 65 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: vec_nat
  total:
    instructions: 342.08 M (no change)
    heap_increase: 172 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 66.05 M (no change)
    heap_increase: 33 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 276.03 M (no change)
    heap_increase: 139 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: vec_nat32
  total:
    instructions: 260.09 M (no change)
    heap_increase: 387 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 16.79 M (no change)
    heap_increase: 258 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 243.29 M (no change)
    heap_increase: 129 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: vec_nat64
  total:
    instructions: 285.26 M (no change)
    heap_increase: 771 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 33.57 M (no change)
    heap_increase: 514 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 251.68 M (no change)
    heap_increase: 257 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: vec_service
  total:
    instructions: 95.53 M (no change)
    heap_increase: 1 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 689.58 K (no change)
    heap_increase: 0 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 94.83 M (no change)
    heap_increase: 1 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Benchmark: wide_record
  total:
    instructions: 47.83 M (-0.01%) (change within noise threshold)
    heap_increase: 18 pages (no change)
    stable_memory_increase: 0 pages (no change)

  1. Encoding (scope):
    calls: 1 (no change)
    instructions: 3.26 M (-0.13%) (change within noise threshold)
    heap_increase: 18 pages (no change)
    stable_memory_increase: 0 pages (no change)

  2. Decoding (scope):
    calls: 1 (no change)
    instructions: 44.56 M (-0.00%) (change within noise threshold)
    heap_increase: 0 pages (no change)
    stable_memory_increase: 0 pages (no change)

---------------------------------------------------

Summary:
  instructions:
    status:   No significant changes 👍
    counts:   [total 20 | regressed 0 | improved 0 | new 0 | unchanged 20]
    change:   [max +255.97K | p75 +1.71K | median 0 | p25 0 | min -10.72K]
    change %: [max +0.17% | p75 +0.01% | median 0.00% | p25 0.00% | min -0.07%]

  heap_increase:
    status:   No significant changes 👍
    counts:   [total 20 | regressed 0 | improved 0 | new 0 | unchanged 20]
    change:   [max 0 | p75 0 | median 0 | p25 0 | min 0]
    change %: [max 0.00% | p75 0.00% | median 0.00% | p25 0.00% | min 0.00%]

  stable_memory_increase:
    status:   No significant changes 👍
    counts:   [total 20 | regressed 0 | improved 0 | new 0 | unchanged 20]
    change:   [max 0 | p75 0 | median 0 | p25 0 | min 0]
    change %: [max 0.00% | p75 0.00% | median 0.00% | p25 0.00% | min 0.00%]

---------------------------------------------------
Successfully persisted results to canbench_results.yml

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR prepares the candid and candid_parser crates for release by fixing LEB128/SLEB128 decoding fast paths that could silently truncate large Nat/Int values near the 64-bit boundary, adding regression tests for those cases, and bumping crate versions + changelog accordingly.

Changes:

  • Fix Deserializer LEB128 fast paths to bail out to bignum decoding before any 64-bit truncation can occur.
  • Fix Int::decode small-value fast path to only treat the terminal LEB128 byte as authoritative for “fits in i64”.
  • Add regression tests covering boundary/large-magnitude round-trips; bump versions and update changelog/lockfiles for the release.

Reviewed changes

Copilot reviewed 7 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
rust/candid/tests/serde.rs Adds regression tests for Nat/Int round-trips across the fast-path boundary and large-magnitude Int::decode cases.
rust/candid/src/types/number.rs Tightens Int::decode fast-path fits_i64 logic to avoid truncation when continuation bytes follow.
rust/candid/src/de.rs Updates LEB128 fast-path readers to bail out earlier (before the 64-bit boundary) and clarifies behavior in docs.
rust/candid/Cargo.toml Bumps candid to 0.10.28 and pins candid_derive to the matching version.
rust/candid_derive/Cargo.toml Bumps candid_derive to 0.10.28.
rust/candid_parser/Cargo.toml Bumps candid_parser to 0.3.2.
rust/bench/Cargo.lock Updates bench lockfile versions to the new crate releases.
Cargo.lock Updates workspace lockfile to candid 0.10.28 / candid_parser 0.3.2.
CHANGELOG.md Adds a 2026-05-20 release entry describing the fixes and released versions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@lwshang lwshang marked this pull request as ready for review May 20, 2026 19:15
@lwshang lwshang requested a review from a team as a code owner May 20, 2026 19:15
@lwshang lwshang merged commit dd3d5ab into master May 20, 2026
15 checks passed
@lwshang lwshang deleted the fix/leb128-decode-truncation branch May 20, 2026 19:21
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.

3 participants