Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

* Bug fixes:
+ Bound the byte length of the type-table header, 64 KiB by default and configurable with `DecoderConfig::set_max_header_len`. Headers over the bound, far above any realistic interface, are now rejected; the value section is unaffected, and `set_max_type_len` still separately bounds the number of type-table entries.
+ Bound the size of a type named in a decoder diagnostic. What it costs to render a type follows that type's own width and depth, and a type reaching the decoder is chosen by the sender, so a diagnostic now elides one whose rendering would exceed a fixed budget instead of rendering it in full. This holds for every diagnostic that names a type, including the subtyping messages and the verbose form that `set_full_error_message(true)` selects; ordinary mismatches are unchanged and still name both types.

## 2026-09-22

Expand Down
46 changes: 31 additions & 15 deletions rust/candid/src/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use super::{
error::{Error, Result},
types::internal::{text_size, type_of, TypeId},
types::internal::{elide_large, text_size, type_of, TypeId},
Comment thread
lwshang marked this conversation as resolved.
Outdated
types::{Field, Label, SharedLabel, Type, TypeEnv, TypeInner},
CandidType,
};
Expand All @@ -21,6 +21,15 @@ use std::{collections::VecDeque, io::Cursor, mem::replace, rc::Rc};

const MAX_TYPE_LEN: i32 = 500;

/// Render a type table for a diagnostic, eliding entries too large to render.
fn describe_table(env: &crate::types::TypeEnv) -> String {
let mut out = String::new();
for (name, ty) in env.0.iter() {
let _ = writeln!(&mut out, "type {name} = {}", elide_large(ty));
}
out
}

/// Use this struct to deserialize a sequence of Rust values (heterogeneous) from IDL binary message.
pub struct IDLDeserialize<'de> {
de: Deserializer<'de>,
Expand Down Expand Up @@ -73,9 +82,7 @@ impl<'de> IDLDeserialize<'de> {
self.de.expect_type = expected_type;
self.de.wire_type = TypeInner::Null.into();
return T::deserialize(&mut self.de);
} else if self.de.config.full_error_message
|| text_size(&expected_type, MAX_TYPE_LEN).is_ok()
{
} else if text_size(&expected_type, MAX_TYPE_LEN).is_ok() {
return Err(Error::msg(format!(
"No more values on the wire, the expected type {expected_type} is not opt, null, or reserved"
)));
Expand All @@ -94,9 +101,8 @@ impl<'de> IDLDeserialize<'de> {
self.de.wire_type = ty.clone();

let mut v = T::deserialize(&mut self.de).with_context(|| {
if self.de.config.full_error_message
|| (text_size(&ty, MAX_TYPE_LEN).is_ok()
&& text_size(&expected_type, MAX_TYPE_LEN).is_ok())
if text_size(&ty, MAX_TYPE_LEN).is_ok()
&& text_size(&expected_type, MAX_TYPE_LEN).is_ok()
{
format!("Fail to decode argument {ind} from {ty} to {expected_type}")
} else {
Expand Down Expand Up @@ -389,12 +395,13 @@ impl<'de> Deserializer<'de> {
let (before, after) = hex.split_at(pos);
let mut res = format!("input: {before}_{after}\n");
if !self.table.0.is_empty() {
write!(&mut res, "table: {}", self.table).unwrap();
write!(&mut res, "table: {}", describe_table(&self.table)).unwrap();
}
write!(
&mut res,
"wire_type: {}, expect_type: {}",
self.wire_type, self.expect_type
elide_large(&self.wire_type),
elide_large(&self.expect_type)
)
.unwrap();
if let Some(field) = &self.field_name {
Expand Down Expand Up @@ -507,9 +514,8 @@ impl<'de> Deserializer<'de> {
&self.expect_type,
)
.with_context(|| {
if self.config.full_error_message
|| (text_size(&self.wire_type, MAX_TYPE_LEN).is_ok()
&& text_size(&self.expect_type, MAX_TYPE_LEN).is_ok())
if text_size(&self.wire_type, MAX_TYPE_LEN).is_ok()
&& text_size(&self.expect_type, MAX_TYPE_LEN).is_ok()
{
format!(
"{} is not a subtype of {}",
Expand Down Expand Up @@ -619,7 +625,7 @@ impl<'de> Deserializer<'de> {
} else {
return Err(Error::subtype(format!(
"{} cannot be deserialized to int",
self.wire_type
elide_large(&self.wire_type)
)));
}
}
Expand All @@ -628,7 +634,12 @@ impl<'de> Deserializer<'de> {
let int = match self.wire_type.as_ref() {
TypeInner::Int => Int::decode(&mut self.input).map_err(Error::msg)?,
TypeInner::Nat => Int(Nat::decode(&mut self.input).map_err(Error::msg)?.0.into()),
t => return Err(Error::subtype(format!("{t} cannot be deserialized to int"))),
_ => {
return Err(Error::subtype(format!(
"{} cannot be deserialized to int",
elide_large(&self.wire_type)
)))
}
};
self.add_cost((self.input.position() - bignum_pos) as usize)?;
bytes.extend_from_slice(&int.0.to_signed_bytes_le());
Expand Down Expand Up @@ -1038,7 +1049,12 @@ impl<'de> de::Deserializer<'de> for &mut Deserializer<'de> {
TypeInner::Int => decode_int(&mut self.input)?,
TypeInner::Nat => i128::try_from(decode_nat(&mut self.input)?)
.map_err(|_| Error::msg("Cannot convert nat to i128"))?,
t => return Err(Error::subtype(format!("{t} cannot be deserialized to int"))),
_ => {
return Err(Error::subtype(format!(
"{} cannot be deserialized to int",
elide_large(&self.wire_type)
)))
}
};
visitor.visit_i128(value)
}
Expand Down
29 changes: 29 additions & 0 deletions rust/candid/src/types/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,35 @@ impl fmt::Display for TypeInner {
write!(f, "{:?}", self)
}
}
/// Budget, in rendered characters, for a type named in a diagnostic.
pub(crate) const MAX_DIAGNOSTIC_TYPE_LEN: i32 = 500;

/// Stands in for a type too large to put in a diagnostic.
pub(crate) const ELIDED_TYPE: &str = "(type elided: too large to render)";

/// Renders a type for a diagnostic, eliding it when rendering would be unreasonable.
///
/// What it costs to render a type follows that type's own width and depth, so an
/// outsized one must not be rendered at all: [`text_size`] settles that against a
/// budget before any of the work is done. Types reaching a decoder come from the wire
/// and so are chosen by the sender, which is why the budget holds for every diagnostic
/// naming one, however verbose the caller asked its errors to be.
pub(crate) fn elide_large(t: &Type) -> ElidedType<'_> {
ElidedType(t)
}

pub(crate) struct ElidedType<'a>(&'a Type);

impl fmt::Display for ElidedType<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if text_size(self.0, MAX_DIAGNOSTIC_TYPE_LEN).is_ok() {
write!(f, "{}", self.0)
Comment thread
lwshang marked this conversation as resolved.
Comment thread
lwshang marked this conversation as resolved.
} else {
f.write_str(ELIDED_TYPE)
}
}
}

#[allow(clippy::result_unit_err)]
pub fn text_size(t: &Type, limit: i32) -> Result<i32, ()> {
use TypeInner::*;
Expand Down
57 changes: 43 additions & 14 deletions rust/candid/src/types/subtype.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::internal::{find_type, Field, Label, Type, TypeInner};
use super::internal::{elide_large, find_type, Field, Label, Type, TypeInner};
use crate::types::TypeEnv;
use crate::utils::RecursionDepth;
use crate::{Error, Result};
Expand Down Expand Up @@ -291,7 +291,7 @@ fn subtype_collect_(
Ok(Null | Reserved | Opt(_))
) => {}
(_, Opt(_)) => {
let msg = format!("WARNING: {t1} <: {t2} due to special subtyping rules involving optional types/fields (see https://github.com/dfinity/candid/blob/c7659ca/spec/Candid.md#upgrading-and-subtyping). This means the two interfaces have diverged, which could cause data loss.");
let msg = format!("WARNING: {} <: {} due to special subtyping rules involving optional types/fields (see https://github.com/dfinity/candid/blob/c7659ca/spec/Candid.md#upgrading-and-subtyping). This means the two interfaces have diverged, which could cause data loss.", elide_large(t1), elide_large(t2));
match report {
OptReport::Silence => (),
OptReport::Warning => eprintln!("{msg}"),
Expand Down Expand Up @@ -324,13 +324,15 @@ fn subtype_collect_(
path: path.clone(),
message: if is_input {
format!(
"new service requires field {id} (type {ty2}), \
which old callers don't provide and is not optional"
"new service requires field {id} (type {}), \
which old callers don't provide and is not optional",
elide_large(ty2)
)
} else {
format!(
"new type is missing required field {id} (type {ty2}), \
which is expected by the old type and is not optional"
"new type is missing required field {id} (type {}), \
which is expected by the old type and is not optional",
elide_large(ty2)
)
},
});
Expand Down Expand Up @@ -428,7 +430,11 @@ fn subtype_collect_(
(_, _) => {
errors.push(Incompatibility {
path: path.clone(),
message: format!("{t1} is not a subtype of {t2}"),
message: format!(
"{} is not a subtype of {}",
elide_large(t1),
elide_large(t2)
),
});
}
}
Expand Down Expand Up @@ -573,7 +579,7 @@ fn subtype_(
Ok(())
}
(_, Opt(_)) => {
let msg = format!("WARNING: {t1} <: {t2} due to special subtyping rules involving optional types/fields (see https://github.com/dfinity/candid/blob/c7659ca/spec/Candid.md#upgrading-and-subtyping). This means the two interfaces have diverged, which could cause data loss.");
let msg = format!("WARNING: {} <: {} due to special subtyping rules involving optional types/fields (see https://github.com/dfinity/candid/blob/c7659ca/spec/Candid.md#upgrading-and-subtyping). This means the two interfaces have diverged, which could cause data loss.", elide_large(t1), elide_large(t2));
match report {
OptReport::Silence => (),
OptReport::Warning => eprintln!("{msg}"),
Expand All @@ -587,15 +593,22 @@ fn subtype_(
match fields.get(id) {
Some(ty1) => {
subtype_(report, gamma, env, ty1, ty2, depth).with_context(|| {
format!("Record field {id}: {ty1} is not a subtype of {ty2}")
format!(
"Record field {id}: {} is not a subtype of {}",
elide_large(ty1),
elide_large(ty2)
)
})?
}
None => {
if !matches!(
env.trace_type_with_depth(ty2, depth)?.as_ref(),
Null | Reserved | Opt(_)
) {
return Err(Error::msg(format!("Record field {id}: {ty2} is only in the expected type and is not of type opt, null or reserved")));
return Err(Error::msg(format!(
"Record field {id}: {} is only in the expected type and is not of type opt, null or reserved",
elide_large(ty2)
)));
}
}
}
Expand All @@ -608,7 +621,11 @@ fn subtype_(
match fields.get(id) {
Some(ty2) => {
subtype_(report, gamma, env, ty1, ty2, depth).with_context(|| {
format!("Variant field {id}: {ty1} is not a subtype of {ty2}")
format!(
"Variant field {id}: {} is not a subtype of {}",
elide_large(ty1),
elide_large(ty2)
)
})?
}
None => {
Expand All @@ -626,7 +643,11 @@ fn subtype_(
match meths.get(name) {
Some(ty1) => {
subtype_(report, gamma, env, ty1, ty2, depth).with_context(|| {
format!("Method {name}: {ty1} is not a subtype of {ty2}")
format!(
"Method {name}: {} is not a subtype of {}",
elide_large(ty1),
elide_large(ty2)
)
})?
}
None => {
Expand Down Expand Up @@ -657,7 +678,11 @@ fn subtype_(
(_, Class(_, t)) => subtype_(report, gamma, env, t1, t, depth),
(Unknown, _) => unreachable!(),
(_, Unknown) => unreachable!(),
(_, _) => Err(Error::msg(format!("{t1} is not a subtype of {t2}"))),
(_, _) => Err(Error::msg(format!(
"{} is not a subtype of {}",
elide_large(t1),
elide_large(t2)
))),
}
}

Expand Down Expand Up @@ -770,7 +795,11 @@ fn equal_impl(
}
(Unknown, _) => unreachable!(),
(_, Unknown) => unreachable!(),
(_, _) => Err(Error::msg(format!("{t1} is not equal to {t2}"))),
(_, _) => Err(Error::msg(format!(
"{} is not equal to {}",
elide_large(t1),
elide_large(t2)
))),
Comment thread
lwshang marked this conversation as resolved.
}
}

Expand Down
Loading
Loading