CompoundType::push (engine/src/types.rs:1006-1018) enforces a hard 32-layer cap on nested
Array/Map types, returning None once self.len >= 32. Its only caller,
CompoundType::from_type (types.rs:958-970), treats that None as fatal:
// engine/src/types.rs
match match ty {
...
Type::Array(ty) => ty.push(Layer::Array),
...
} {
Some(ty) => ty,
None => panic!("Could not convert type to compound type"), // types.rs:968
}
Two producers feed this panic, neither validating depth beforehand:
engine's own Deserialize impl for Type/CompoundType, reachable from the FFI's execution-context snapshot/restore JSON format ($lists entry's type field).
ffi's CType::push (ffi/src/lib.rs:61-69) mirrors CompoundType but has no matching 32-layer guard of its own — it accumulates layer bits into a u32/u8 pair indefinitely. The panic only fires when the accumulated CType is converted back via impl From<CType> for Type (ffi/src/lib.rs:88-102), reachable via ordinary, well-formed repeated calls to wirefilter_create_array_type during scheme/type construction.
None of wirefilter_deserialize_json_to_execution_context, wirefilter_add_type_field_to_scheme,
wirefilter_add_always_list_to_scheme, wirefilter_add_never_list_to_scheme, or
wirefilter_serialize_type_to_json wraps this in catch_panic.
The real trigger is depth 34, not 33 — the recursive layer-accounting means the cap is hit one
level later than a naive count suggests (each layer's conversion pushes onto the previous level's
already-built CompoundType).
Confirmed crossing the real C ABI boundary, via a compiled C test program (built against this
repo's own ffi/tests/ctests harness) that constructs
{"$lists":[{"type":<34 nested Array wrappers>,"data":{}}]} and calls
wirefilter_deserialize_json_to_execution_context:
thread '...' panicked at engine/src/types.rs:968:21:
Could not convert type to compound type
thread caused non-unwinding panic. aborting.
... (signal: 6, SIGABRT: process abort signal)
This is a genuine process abort observed from the C caller's side, not an in-process Rust-only panic.
wirefilter_enable_panic_catcher() is never called in this test (defaults to disabled), consistent
with an ordinary embedding that hasn't explicitly enabled it.
Reachability: whether attacker-supplied JSON reaches the deserialization path (vs. only
host-produced serialized state) depends on the embedding. The FFI/CType path requires the ability to
construct a scheme with 34+ nested array/map layers — a schema/type-construction-time actor, not raw
per-request traffic. The wasm bindings (wasm/src/lib.rs) expose only Scheme::try_from(fields) —
scheme construction only, no JSON-ingestion API — so this doesn't apply to the wasm surface as
currently shipped.
Not memory corruption — a clean panic/abort (DoS) in both paths.
Suggested fix: add an explicit depth check (reject at deserialize time, or in CType::push, once
depth would exceed 32) that returns a normal error instead of relying on the internal cap to panic.
Give CType::push the same if self.len >= 32 { None } guard CompoundType::push already has, and
propagate that as an error rather than panicking downstream. Wrap the five consuming FFI functions in
catch_panic regardless, as defense in depth.
Found with the rust-in-peace pipeline.
CompoundType::push(engine/src/types.rs:1006-1018) enforces a hard 32-layer cap on nestedArray/Maptypes, returningNoneonceself.len >= 32. Its only caller,CompoundType::from_type(types.rs:958-970), treats thatNoneas fatal:Two producers feed this panic, neither validating depth beforehand:
engine's ownDeserializeimpl forType/CompoundType, reachable from the FFI's execution-context snapshot/restore JSON format ($listsentry'stypefield).ffi'sCType::push(ffi/src/lib.rs:61-69) mirrorsCompoundTypebut has no matching 32-layer guard of its own — it accumulates layer bits into au32/u8pair indefinitely. The panic only fires when the accumulatedCTypeis converted back viaimpl From<CType> for Type(ffi/src/lib.rs:88-102), reachable via ordinary, well-formed repeated calls towirefilter_create_array_typeduring scheme/type construction.None of
wirefilter_deserialize_json_to_execution_context,wirefilter_add_type_field_to_scheme,wirefilter_add_always_list_to_scheme,wirefilter_add_never_list_to_scheme, orwirefilter_serialize_type_to_jsonwraps this incatch_panic.The real trigger is depth 34, not 33 — the recursive layer-accounting means the cap is hit one
level later than a naive count suggests (each layer's conversion pushes onto the previous level's
already-built
CompoundType).Confirmed crossing the real C ABI boundary, via a compiled C test program (built against this
repo's own
ffi/tests/ctestsharness) that constructs{"$lists":[{"type":<34 nested Array wrappers>,"data":{}}]}and callswirefilter_deserialize_json_to_execution_context:This is a genuine process abort observed from the C caller's side, not an in-process Rust-only panic.
wirefilter_enable_panic_catcher()is never called in this test (defaults to disabled), consistentwith an ordinary embedding that hasn't explicitly enabled it.
Reachability: whether attacker-supplied JSON reaches the deserialization path (vs. only
host-produced serialized state) depends on the embedding. The FFI/
CTypepath requires the ability toconstruct a scheme with 34+ nested array/map layers — a schema/type-construction-time actor, not raw
per-request traffic. The wasm bindings (
wasm/src/lib.rs) expose onlyScheme::try_from(fields)—scheme construction only, no JSON-ingestion API — so this doesn't apply to the wasm surface as
currently shipped.
Not memory corruption — a clean panic/abort (DoS) in both paths.
Suggested fix: add an explicit depth check (reject at deserialize time, or in
CType::push, oncedepth would exceed 32) that returns a normal error instead of relying on the internal cap to panic.
Give
CType::pushthe sameif self.len >= 32 { None }guardCompoundType::pushalready has, andpropagate that as an error rather than panicking downstream. Wrap the five consuming FFI functions in
catch_panicregardless, as defense in depth.Found with the rust-in-peace pipeline.