Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 4 additions & 3 deletions crates/wit-parser/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1436,9 +1436,10 @@ fn parse_docs<'a>(tokens: &mut Tokenizer<'a>) -> Result<Docs<'a>, lex::Error> {
Ok(docs)
}

/// Maximum nesting depth of types parsed with `Type::parse`, e.g.
/// `list<list<list<...>>>`.
const MAX_TYPE_DEPTH: usize = 100;
/// Maximum nesting depth of types, both within one type parsed with
/// `Type::parse`, e.g. `list<list<list<...>>>`, and through named types and
/// aliases once a package is resolved.
pub(crate) const MAX_TYPE_DEPTH: usize = 100;

impl<'a> Type<'a> {
fn parse(tokens: &mut Tokenizer<'a>) -> ParseResult<Self> {
Expand Down
13 changes: 4 additions & 9 deletions crates/wit-parser/src/decoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,10 @@ impl ComponentInfo {
Chunk::NeedMoreData(hint) => {
assert!(!eof); // otherwise an error would be returned

// Use the hint to preallocate more space, then read
// some more data into our buffer.
//
// Note that the buffer management here is not ideal,
// but it's compact enough to fit in an example!
let len = buffer.len();
buffer.extend((0..hint).map(|_| 0u8));
let n = reader.read(&mut buffer[len..])?;
buffer.truncate(len + n);
// Read up to the hint into our buffer, which grows with
// what is read: the hint comes from the input, so it may
// be far more than there is.
let n = (&mut reader).take(hint as u64).read_to_end(&mut buffer)?;
eof = n == 0;
continue;
}
Expand Down
40 changes: 40 additions & 0 deletions crates/wit-parser/src/resolve/mod.rs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For this change there's quite a lot of recursive processing of types in WIT and component-model types in general. This is why the validation of a component limits the depth of types to a fixed amount to ensure that all code downstream which processes types recursively is safe to do so. My guess is that this is not the only location throughout wit-parser that uses recursion and doesn't handle deeply-nested types well, and I don't think it's necessarily worthwhile to try to patch every single one of them into a stack.

Alternatively, I would personally be more partial to an approach like validation does, which is to basically just validate that types aren't too deep. For example when creating a Resolve or similar that could verify type depth pretty quickly (in O(types) time). The validation would match what's already a restriction for components anyway, and it would mean that each individual location of recursion throughout the wit-parser crate doesn't have to get updated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I see the usual limits is 100. Going with that.

Original file line number Diff line number Diff line change
Expand Up @@ -3535,6 +3535,7 @@ impl Remap {
// order should be sufficient. Also note though that the interface
// owner of a type isn't updated here due to interfaces not being known
// yet.
let first_new_type = resolve.types.len();
for (id, mut ty) in unresolved.types.into_iter().skip(foreign_types) {
let span = ty.span;
if !resolve.include_stability(&ty.stability, &pkgid, span)? {
Expand Down Expand Up @@ -3568,6 +3569,8 @@ impl Remap {
self.types.push(Some(new_id));
}

check_type_depth(resolve, first_new_type)?;

// Next transfer all interfaces into `Resolve`, updating type ids
// referenced along the way.
for (id, mut iface) in unresolved.interfaces.into_iter().skip(foreign_interfaces) {
Expand Down Expand Up @@ -4495,6 +4498,43 @@ impl Remap {
}
}

/// Errors if a type at index `first_new` or later in `resolve.types` is
/// nested `MAX_TYPE_DEPTH` or more types deep, counting aliases.
///
/// Types are topologically sorted, so each type's depth follows from the
/// depths of the earlier types it refers to in one pass, without recursion.
fn check_type_depth(resolve: &Resolve, first_new: usize) -> ResolveResult<()> {
struct Deepest<'a> {
depths: &'a [usize],
deepest: usize,
}
impl TypeIdVisitor for Deepest<'_> {
fn before_visit_type_id(&mut self, id: TypeId) -> bool {
self.deepest = self.deepest.max(self.depths[id.index()]);
// Only look at the types referred to directly.
false
}
}
let mut depths = Vec::with_capacity(resolve.types.len());
for (id, ty) in resolve.types.iter() {
let mut deepest = Deepest {
depths: &depths,
deepest: 0,
};
deepest.visit_type_def(resolve, ty);
let depth = deepest.deepest + 1;
if id.index() >= first_new && depth >= crate::ast::MAX_TYPE_DEPTH {
let msg = match &ty.name {
Some(name) => format!("type `{name}` is nested too deep"),
None => "type nesting too deep".to_string(),
};
return Err(ResolveError::new_semantic(ty.span, msg));
}
depths.push(depth);
}
Ok(())
}

struct MergeMap<'a> {
/// A map of package ids in `from` to those in `into` for those that are
/// found to be equivalent.
Expand Down

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not sure how necessary these are per your standards. But keeping as a guard that may be flagged later if the encoder behaviour changes and loop fails.

Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// parse-fail
//
// Each alias names the previous one, so `t99` is 100 types deep.

package a:b;

interface i {
type t0 = u32;
type t1 = t0;
type t2 = t1;
type t3 = t2;
type t4 = t3;
type t5 = t4;
type t6 = t5;
type t7 = t6;
type t8 = t7;
type t9 = t8;
type t10 = t9;
type t11 = t10;
type t12 = t11;
type t13 = t12;
type t14 = t13;
type t15 = t14;
type t16 = t15;
type t17 = t16;
type t18 = t17;
type t19 = t18;
type t20 = t19;
type t21 = t20;
type t22 = t21;
type t23 = t22;
type t24 = t23;
type t25 = t24;
type t26 = t25;
type t27 = t26;
type t28 = t27;
type t29 = t28;
type t30 = t29;
type t31 = t30;
type t32 = t31;
type t33 = t32;
type t34 = t33;
type t35 = t34;
type t36 = t35;
type t37 = t36;
type t38 = t37;
type t39 = t38;
type t40 = t39;
type t41 = t40;
type t42 = t41;
type t43 = t42;
type t44 = t43;
type t45 = t44;
type t46 = t45;
type t47 = t46;
type t48 = t47;
type t49 = t48;
type t50 = t49;
type t51 = t50;
type t52 = t51;
type t53 = t52;
type t54 = t53;
type t55 = t54;
type t56 = t55;
type t57 = t56;
type t58 = t57;
type t59 = t58;
type t60 = t59;
type t61 = t60;
type t62 = t61;
type t63 = t62;
type t64 = t63;
type t65 = t64;
type t66 = t65;
type t67 = t66;
type t68 = t67;
type t69 = t68;
type t70 = t69;
type t71 = t70;
type t72 = t71;
type t73 = t72;
type t74 = t73;
type t75 = t74;
type t76 = t75;
type t77 = t76;
type t78 = t77;
type t79 = t78;
type t80 = t79;
type t81 = t80;
type t82 = t81;
type t83 = t82;
type t84 = t83;
type t85 = t84;
type t86 = t85;
type t87 = t86;
type t88 = t87;
type t89 = t88;
type t90 = t89;
type t91 = t90;
type t92 = t91;
type t93 = t92;
type t94 = t93;
type t95 = t94;
type t96 = t95;
type t97 = t96;
type t98 = t97;
type t99 = t98;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type `t99` is nested too deep
--> tests/ui/parse-fail/type-nesting-too-deep-aliases.wit:107:8
|
107 | type t99 = t98;
| ^--
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// parse-fail
//
// Each record holds the previous one, so `r99` is 100 types deep.

package a:b;

interface i {
record r0 { x: u32 }
record r1 { x: r0 }
record r2 { x: r1 }
record r3 { x: r2 }
record r4 { x: r3 }
record r5 { x: r4 }
record r6 { x: r5 }
record r7 { x: r6 }
record r8 { x: r7 }
record r9 { x: r8 }
record r10 { x: r9 }
record r11 { x: r10 }
record r12 { x: r11 }
record r13 { x: r12 }
record r14 { x: r13 }
record r15 { x: r14 }
record r16 { x: r15 }
record r17 { x: r16 }
record r18 { x: r17 }
record r19 { x: r18 }
record r20 { x: r19 }
record r21 { x: r20 }
record r22 { x: r21 }
record r23 { x: r22 }
record r24 { x: r23 }
record r25 { x: r24 }
record r26 { x: r25 }
record r27 { x: r26 }
record r28 { x: r27 }
record r29 { x: r28 }
record r30 { x: r29 }
record r31 { x: r30 }
record r32 { x: r31 }
record r33 { x: r32 }
record r34 { x: r33 }
record r35 { x: r34 }
record r36 { x: r35 }
record r37 { x: r36 }
record r38 { x: r37 }
record r39 { x: r38 }
record r40 { x: r39 }
record r41 { x: r40 }
record r42 { x: r41 }
record r43 { x: r42 }
record r44 { x: r43 }
record r45 { x: r44 }
record r46 { x: r45 }
record r47 { x: r46 }
record r48 { x: r47 }
record r49 { x: r48 }
record r50 { x: r49 }
record r51 { x: r50 }
record r52 { x: r51 }
record r53 { x: r52 }
record r54 { x: r53 }
record r55 { x: r54 }
record r56 { x: r55 }
record r57 { x: r56 }
record r58 { x: r57 }
record r59 { x: r58 }
record r60 { x: r59 }
record r61 { x: r60 }
record r62 { x: r61 }
record r63 { x: r62 }
record r64 { x: r63 }
record r65 { x: r64 }
record r66 { x: r65 }
record r67 { x: r66 }
record r68 { x: r67 }
record r69 { x: r68 }
record r70 { x: r69 }
record r71 { x: r70 }
record r72 { x: r71 }
record r73 { x: r72 }
record r74 { x: r73 }
record r75 { x: r74 }
record r76 { x: r75 }
record r77 { x: r76 }
record r78 { x: r77 }
record r79 { x: r78 }
record r80 { x: r79 }
record r81 { x: r80 }
record r82 { x: r81 }
record r83 { x: r82 }
record r84 { x: r83 }
record r85 { x: r84 }
record r86 { x: r85 }
record r87 { x: r86 }
record r88 { x: r87 }
record r89 { x: r88 }
record r90 { x: r89 }
record r91 { x: r90 }
record r92 { x: r91 }
record r93 { x: r92 }
record r94 { x: r93 }
record r95 { x: r94 }
record r96 { x: r95 }
record r97 { x: r96 }
record r98 { x: r97 }
record r99 { x: r98 }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type `r99` is nested too deep
--> tests/ui/parse-fail/type-nesting-too-deep-records.wit:107:10
|
107 | record r99 { x: r98 }
| ^--
Loading