Skip to content

Commit

Permalink
fix: Infinite recursion when using mutually recursive $ref in unevalu…
Browse files Browse the repository at this point in the history
…atedProperties

Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
  • Loading branch information
Stranger6667 committed Oct 24, 2024
1 parent 383eb83 commit 091118b
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 13 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Fixed

- Infinite recursion when using mutually recursive `$ref` in `unevaluatedProperties`.

## [0.24.2] - 2024-10-24

### Fixed
Expand Down
4 changes: 4 additions & 0 deletions crates/jsonschema-py/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Fixed

- Infinite recursion when using mutually recursive `$ref` in `unevaluatedProperties`.

## [0.24.2] - 2024-10-24

### Fixed
Expand Down
80 changes: 80 additions & 0 deletions crates/jsonschema/src/keywords/unevaluated_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,3 +512,83 @@ pub(crate) fn compile<'a>(
}
}
}

#[cfg(test)]
mod tests {
use serde_json::json;

#[test]
fn test_unevaluated_items_with_recursion() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"$ref": "#/$defs/array_1"
}
],
"unevaluatedItems": false,
"$defs": {
"array_1": {
"type": "array",
"prefixItems": [
{
"type": "string"
},
{
"allOf": [
{
"$ref": "#/$defs/array_2"
}
],
"type": "array",
"unevaluatedItems": false
}
]
},
"array_2": {
"type": "array",
"prefixItems": [
{
"type": "number"
},
{
"allOf": [
{
"$ref": "#/$defs/array_1"
}
],
"type": "array",
"unevaluatedItems": false
}
]
}
}
});

let validator = crate::validator_for(&schema).expect("Schema should compile");

// This instance should fail validation because the nested array has an unevaluated item
let instance = json!([
"string",
[
42,
[
"string",
[
42,
"unexpected" // This item should cause validation to fail
]
]
]
]);

assert!(!validator.is_valid(&instance));
assert!(validator.validate(&instance).is_err());

// This instance should pass validation as all items are evaluated
let valid_instance = json!(["string", [42, ["string", [42]]]]);

assert!(validator.is_valid(&valid_instance));
assert!(validator.validate(&valid_instance).is_ok());
}
}
75 changes: 62 additions & 13 deletions crates/jsonschema/src/keywords/unevaluated_properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,36 @@ struct Draft2019PropertiesFilter {
dependent: Vec<(String, Self)>,
pattern_properties: Vec<(fancy_regex::Regex, SchemaNode)>,
ref_: Option<Box<Self>>,
recursive_ref: Option<LazyRecursiveRef>,
recursive_ref: Option<LazyReference<Self>>,
conditional: Option<Box<ConditionalFilter<Self>>>,
all_of: Option<CombinatorFilter<Self>>,
any_of: Option<CombinatorFilter<Self>>,
one_of: Option<CombinatorFilter<Self>>,
}

struct LazyRecursiveRef {
enum ReferenceFilter<T> {
Recursive(LazyReference<T>),
Default(Box<T>),
}

impl<F: PropertiesFilter> ReferenceFilter<F> {
fn mark_evaluated_properties<'i>(
&self,
instance: &'i Value,
properties: &mut AHashSet<&'i String>,
) {
match self {
ReferenceFilter::Recursive(filter) => filter
.get_or_init()
.mark_evaluated_properties(instance, properties),
ReferenceFilter::Default(filter) => {
filter.mark_evaluated_properties(instance, properties)
}
}
}
}

struct LazyReference<T> {
resource: Resource,
config: Arc<ValidationOptions>,
registry: Arc<Registry>,
Expand All @@ -123,10 +145,10 @@ struct LazyRecursiveRef {
vocabularies: VocabularySet,
location: Location,
draft: Draft,
inner: OnceCell<Box<Draft2019PropertiesFilter>>,
inner: OnceCell<Box<T>>,
}

impl LazyRecursiveRef {
impl<T: PropertiesFilter> LazyReference<T> {
fn new<'a>(ctx: &compiler::Context) -> Result<Self, ValidationError<'a>> {
let scopes = ctx.scopes();
let resolved = ctx.lookup_recursive_reference()?;
Expand All @@ -137,7 +159,7 @@ impl LazyRecursiveRef {
base_uri = resolver.resolve_against(&base_uri.borrow(), id)?;
}

Ok(LazyRecursiveRef {
Ok(LazyReference {
resource,
config: Arc::clone(ctx.config()),
registry: Arc::clone(&ctx.registry),
Expand All @@ -150,7 +172,7 @@ impl LazyRecursiveRef {
})
}

fn get_or_init(&self) -> &Draft2019PropertiesFilter {
fn get_or_init(&self) -> &T {
self.inner.get_or_init(|| {
let resolver = self
.registry
Expand All @@ -166,7 +188,7 @@ impl LazyRecursiveRef {
);

Box::new(
Draft2019PropertiesFilter::new(
T::new(
&ctx,
self.resource
.contents()
Expand Down Expand Up @@ -195,7 +217,7 @@ impl PropertiesFilter for Draft2019PropertiesFilter {

let mut recursive_ref = None;
if parent.contains_key("$recursiveRef") {
recursive_ref = Some(LazyRecursiveRef::new(ctx)?);
recursive_ref = Some(LazyReference::new(ctx)?);
}

let mut conditional = None;
Expand Down Expand Up @@ -398,7 +420,7 @@ struct DefaultPropertiesFilter {
properties: Vec<(String, SchemaNode)>,
dependent: Vec<(String, Self)>,
pattern_properties: Vec<(fancy_regex::Regex, SchemaNode)>,
ref_: Option<Box<Self>>,
ref_: Option<ReferenceFilter<Self>>,
dynamic_ref: Option<Box<Self>>,
conditional: Option<Box<ConditionalFilter<Self>>>,
all_of: Option<CombinatorFilter<Self>>,
Expand All @@ -414,10 +436,36 @@ impl PropertiesFilter for DefaultPropertiesFilter {
let mut ref_ = None;

if let Some(Value::String(reference)) = parent.get("$ref") {
let resolved = ctx.lookup(reference)?;
if let Value::Object(subschema) = resolved.contents() {
ref_ = Some(Box::new(Self::new(ctx, subschema)?));
}
if ctx.is_circular_reference(reference)? {
let scopes = ctx.scopes();
let resolved = ctx.lookup(reference)?;
let resource = ctx.draft().create_resource(resolved.contents().clone());
let resolver = resolved.resolver();
let mut base_uri = resolver.base_uri();
if let Some(id) = resource.id() {
base_uri = resolver.resolve_against(&base_uri.borrow(), id)?;
}

ref_ = Some(ReferenceFilter::Recursive(LazyReference {
resource,
config: Arc::clone(ctx.config()),
registry: Arc::clone(&ctx.registry),
base_uri,
scopes,
vocabularies: ctx.vocabularies().clone(),
location: ctx.location().clone(),
draft: ctx.draft(),
inner: OnceCell::default(),
}));
} else {
ctx.mark_seen(reference)?;
let resolved = ctx.lookup(reference)?;
if let Value::Object(subschema) = resolved.contents() {
ref_ = Some(ReferenceFilter::Default(Box::new(Self::new(
ctx, subschema,
)?)));
}
};
}

let mut dynamic_ref = None;
Expand Down Expand Up @@ -829,6 +877,7 @@ mod tests {
fn test_unevaluated_properties_with_recursion() {
// See GH-420
let schema = json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{
"$ref": "#/$defs/1_1"
Expand Down

0 comments on commit 091118b

Please sign in to comment.