From e3875cda95a0a1981c9c8d37f00771a6cc17a148 Mon Sep 17 00:00:00 2001 From: Daniel Eades Date: Sun, 12 Jul 2026 07:07:37 +0100 Subject: [PATCH] Fix integer underflow in all_mutually_exclusive for empty subschemas An empty subschema list (e.g. from a degenerate "anyOf": []) caused `len - 1` to underflow: a panic in debug builds and an effectively unbounded iteration in release builds. Return true for fewer than two subschemas since mutual exclusivity is vacuously satisfied. --- typify-impl/src/convert.rs | 17 +++++++++++++++++ typify-impl/src/util.rs | 19 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs index 5c902a4c..4d19aa5b 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -2294,6 +2294,23 @@ mod tests { } } + #[test] + fn test_empty_any_of() { + // A degenerate "anyOf" with no subschemas should not panic (or hang + // in release builds) while checking for mutual exclusivity. + let schema_json = r#" + { + "title": "EmptyAnyOf", + "anyOf": [] + } + "#; + + let schema: RootSchema = serde_json::from_str(schema_json).unwrap(); + + let mut type_space = TypeSpace::default(); + let _ = type_space.add_type(&schema.schema.into()).unwrap(); + } + #[test] fn test_overridden_conversion() { let schema_json = r#" diff --git a/typify-impl/src/util.rs b/typify-impl/src/util.rs index 54d232b9..982a2469 100644 --- a/typify-impl/src/util.rs +++ b/typify-impl/src/util.rs @@ -48,6 +48,12 @@ pub(crate) fn all_mutually_exclusive( definitions: &BTreeMap, ) -> bool { let len = subschemas.len(); + // With fewer than two subschemas there are no pairs to compare, so the + // schemas are vacuously mutually exclusive. This also avoids underflow + // in `len - 1` below when `subschemas` is empty. + if len < 2 { + return true; + } // Consider all pairs (0..len - 1) .flat_map(|ii| (ii + 1..len).map(move |jj| (ii, jj))) @@ -1001,7 +1007,10 @@ mod tests { }; use crate::{ - util::{decode_segment, sanitize, schemas_mutually_exclusive, Case, ReorderedInstanceType}, + util::{ + all_mutually_exclusive, decode_segment, sanitize, schemas_mutually_exclusive, Case, + ReorderedInstanceType, + }, Name, }; @@ -1107,6 +1116,14 @@ mod tests { assert!(schemas_mutually_exclusive(&b, &a, &BTreeMap::new())); } + #[test] + fn test_all_mutually_exclusive_empty() { + // An empty slice of subschemas has no pairs to compare, so it's + // vacuously true. This should not panic (previously `len - 1` + // underflowed for an empty slice). + assert!(all_mutually_exclusive(&[], &BTreeMap::new())); + } + #[test] fn test_decode_segment() { assert_eq!(decode_segment("foo~1bar"), "foo/bar");