From 8d28cdba6eb10f53490ba41d1b5cb40506c2de22 Mon Sep 17 00:00:00 2001
From: tyranron <tyranron@gmail.com>
Date: Thu, 28 Jul 2022 14:33:16 +0300
Subject: [PATCH 1/2] Backport CVE-2022-31173 fix from GHSA-4rx6-g5vg-5f3j

Co-authored-by: ilslv <ilya.solovyiov@gmail.com>
---
 integration_tests/juniper_tests/Cargo.toml    |  1 +
 .../juniper_tests/src/cve_2022_31173.rs       | 56 +++++++++++++++
 integration_tests/juniper_tests/src/lib.rs    |  2 +
 juniper/CHANGELOG.md                          |  1 +
 .../validation/rules/no_fragment_cycles.rs    | 68 ++++++++++++-------
 .../rules/no_undefined_variables.rs           | 48 +++++++++----
 .../validation/rules/no_unused_fragments.rs   | 49 ++++++++-----
 .../validation/rules/no_unused_variables.rs   | 50 ++++++++++----
 .../rules/overlapping_fields_can_be_merged.rs | 67 +++++++++++++-----
 .../rules/variables_in_allowed_position.rs    | 57 ++++++++++++----
 10 files changed, 297 insertions(+), 102 deletions(-)
 create mode 100644 integration_tests/juniper_tests/src/cve_2022_31173.rs

diff --git a/integration_tests/juniper_tests/Cargo.toml b/integration_tests/juniper_tests/Cargo.toml
index e21f60f4..e3db4af9 100644
--- a/integration_tests/juniper_tests/Cargo.toml
+++ b/integration_tests/juniper_tests/Cargo.toml
@@ -7,6 +7,7 @@ publish = false
 [dependencies]
 derive_more = "0.99"
 futures = "0.3"
+itertools = "0.10"
 juniper = { path = "../../juniper" }
 juniper_subscriptions = { path = "../../juniper_subscriptions" }
 
diff --git a/integration_tests/juniper_tests/src/cve_2022_31173.rs b/integration_tests/juniper_tests/src/cve_2022_31173.rs
new file mode 100644
index 00000000..81d14c02
--- /dev/null
+++ b/integration_tests/juniper_tests/src/cve_2022_31173.rs
@@ -0,0 +1,56 @@
+//! Checks that long looping chain of fragments doesn't cause a stack overflow.
+//!
+//! ```graphql
+//! # Fragment loop example
+//! query {
+//!     ...a
+//! }
+//!
+//! fragment a on Query {
+//!     ...b
+//! }
+//!
+//! fragment b on Query {
+//!     ...a
+//! }
+//! ```
+
+use std::iter;
+
+use itertools::Itertools as _;
+use juniper::{graphql_object, EmptyMutation, EmptySubscription, Variables};
+
+struct Query;
+
+#[graphql_object]
+impl Query {
+    fn dummy() -> bool {
+        false
+    }
+}
+
+type Schema = juniper::RootNode<'static, Query, EmptyMutation, EmptySubscription>;
+
+#[tokio::test]
+async fn test() {
+    const PERM: &str = "abcefghijk";
+    const CIRCLE_SIZE: usize = 7500;
+
+    let query = iter::once(format!("query {{ ...{PERM} }} "))
+        .chain(
+            PERM.chars()
+                .permutations(PERM.len())
+                .map(|vec| vec.into_iter().collect::<String>())
+                .take(CIRCLE_SIZE)
+                .collect::<Vec<_>>()
+                .into_iter()
+                .circular_tuple_windows::<(_, _)>()
+                .map(|(cur, next)| format!("fragment {cur} on Query {{ ...{next} }} ")),
+        )
+        .collect::<String>();
+
+    let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new());
+    let _ = juniper::execute(&query, None, &schema, &Variables::new(), &())
+        .await
+        .unwrap_err();
+}
diff --git a/integration_tests/juniper_tests/src/lib.rs b/integration_tests/juniper_tests/src/lib.rs
index 5592c146..8728494e 100644
--- a/integration_tests/juniper_tests/src/lib.rs
+++ b/integration_tests/juniper_tests/src/lib.rs
@@ -7,6 +7,8 @@ mod codegen;
 #[cfg(test)]
 mod custom_scalar;
 #[cfg(test)]
+mod cve_2022_31173;
+#[cfg(test)]
 mod explicit_null;
 #[cfg(test)]
 mod infallible_as_field_error;
diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md
index 921b700c..6ae5c2c8 100644
--- a/juniper/CHANGELOG.md
+++ b/juniper/CHANGELOG.md
@@ -1,5 +1,6 @@
 # master
 
+- Fix [CVE-2022-31173](https://github.com/graphql-rust/juniper/security/advisories/GHSA-4rx6-g5vg-5f3j).
 - Fix incorrect error when explicit `null` provided for `null`able list input parameter. ([#1086](https://github.com/graphql-rust/juniper/pull/1086))
 
 # [[0.15.9] 2022-02-02](https://github.com/graphql-rust/juniper/releases/tag/juniper-v0.15.9)
diff --git a/juniper/src/validation/rules/no_fragment_cycles.rs b/juniper/src/validation/rules/no_fragment_cycles.rs
index c5489e08..b1f119d8 100644
--- a/juniper/src/validation/rules/no_fragment_cycles.rs
+++ b/juniper/src/validation/rules/no_fragment_cycles.rs
@@ -7,19 +7,6 @@ use crate::{
     value::ScalarValue,
 };
 
-pub struct NoFragmentCycles<'a> {
-    current_fragment: Option<&'a str>,
-    spreads: HashMap<&'a str, Vec<Spanning<&'a str>>>,
-    fragment_order: Vec<&'a str>,
-}
-
-struct CycleDetector<'a> {
-    visited: HashSet<&'a str>,
-    spreads: &'a HashMap<&'a str, Vec<Spanning<&'a str>>>,
-    path_indices: HashMap<&'a str, usize>,
-    errors: Vec<RuleError>,
-}
-
 pub fn factory<'a>() -> NoFragmentCycles<'a> {
     NoFragmentCycles {
         current_fragment: None,
@@ -28,6 +15,12 @@ pub fn factory<'a>() -> NoFragmentCycles<'a> {
     }
 }
 
+pub struct NoFragmentCycles<'a> {
+    current_fragment: Option<&'a str>,
+    spreads: HashMap<&'a str, Vec<Spanning<&'a str>>>,
+    fragment_order: Vec<&'a str>,
+}
+
 impl<'a, S> Visitor<'a, S> for NoFragmentCycles<'a>
 where
     S: ScalarValue,
@@ -38,14 +31,12 @@ where
         let mut detector = CycleDetector {
             visited: HashSet::new(),
             spreads: &self.spreads,
-            path_indices: HashMap::new(),
             errors: Vec::new(),
         };
 
         for frag in &self.fragment_order {
             if !detector.visited.contains(frag) {
-                let mut path = Vec::new();
-                detector.detect_from(frag, &mut path);
+                detector.detect_from(frag);
             }
         }
 
@@ -91,19 +82,46 @@ where
     }
 }
 
+type CycleDetectorState<'a> = (&'a str, Vec<&'a Spanning<&'a str>>, HashMap<&'a str, usize>);
+
+struct CycleDetector<'a> {
+    visited: HashSet<&'a str>,
+    spreads: &'a HashMap<&'a str, Vec<Spanning<&'a str>>>,
+    errors: Vec<RuleError>,
+}
+
 impl<'a> CycleDetector<'a> {
-    fn detect_from(&mut self, from: &'a str, path: &mut Vec<&'a Spanning<&'a str>>) {
+    fn detect_from(&mut self, from: &'a str) {
+        let mut to_visit = Vec::new();
+        to_visit.push((from, Vec::new(), HashMap::new()));
+
+        while let Some((from, path, path_indices)) = to_visit.pop() {
+            to_visit.extend(self.detect_from_inner(from, path, path_indices));
+        }
+    }
+
+    /// This function should be called only inside [`Self::detect_from()`], as
+    /// it's a recursive function using heap instead of a stack. So, instead of
+    /// the recursive call, we return a [`Vec`] that is visited inside
+    /// [`Self::detect_from()`].
+    fn detect_from_inner(
+        &mut self,
+        from: &'a str,
+        path: Vec<&'a Spanning<&'a str>>,
+        mut path_indices: HashMap<&'a str, usize>,
+    ) -> Vec<CycleDetectorState<'a>> {
         self.visited.insert(from);
 
         if !self.spreads.contains_key(from) {
-            return;
+            return Vec::new();
         }
 
-        self.path_indices.insert(from, path.len());
+        path_indices.insert(from, path.len());
 
+        let mut to_visit = Vec::new();
         for node in &self.spreads[from] {
-            let name = &node.item;
-            let index = self.path_indices.get(name).cloned();
+            let name = node.item;
+            let index = path_indices.get(name).cloned();
 
             if let Some(index) = index {
                 let err_pos = if index < path.len() {
@@ -114,14 +132,14 @@ impl<'a> CycleDetector<'a> {
 
                 self.errors
                     .push(RuleError::new(&error_message(name), &[err_pos.start]));
-            } else if !self.visited.contains(name) {
+            } else {
+                let mut path = path.clone();
                 path.push(node);
-                self.detect_from(name, path);
-                path.pop();
+                to_visit.push((name, path, path_indices.clone()));
             }
         }
 
-        self.path_indices.remove(from);
+        to_visit
     }
 }
 
diff --git a/juniper/src/validation/rules/no_undefined_variables.rs b/juniper/src/validation/rules/no_undefined_variables.rs
index 8f13f191..3b73e9b8 100644
--- a/juniper/src/validation/rules/no_undefined_variables.rs
+++ b/juniper/src/validation/rules/no_undefined_variables.rs
@@ -12,13 +12,6 @@ pub enum Scope<'a> {
     Fragment(&'a str),
 }
 
-pub struct NoUndefinedVariables<'a> {
-    defined_variables: HashMap<Option<&'a str>, (SourcePosition, HashSet<&'a str>)>,
-    used_variables: HashMap<Scope<'a>, Vec<Spanning<&'a str>>>,
-    current_scope: Option<Scope<'a>>,
-    spreads: HashMap<Scope<'a>, Vec<&'a str>>,
-}
-
 pub fn factory<'a>() -> NoUndefinedVariables<'a> {
     NoUndefinedVariables {
         defined_variables: HashMap::new(),
@@ -28,6 +21,13 @@ pub fn factory<'a>() -> NoUndefinedVariables<'a> {
     }
 }
 
+pub struct NoUndefinedVariables<'a> {
+    defined_variables: HashMap<Option<&'a str>, (SourcePosition, HashSet<&'a str>)>,
+    used_variables: HashMap<Scope<'a>, Vec<Spanning<&'a str>>>,
+    current_scope: Option<Scope<'a>>,
+    spreads: HashMap<Scope<'a>, Vec<&'a str>>,
+}
+
 impl<'a> NoUndefinedVariables<'a> {
     fn find_undef_vars(
         &'a self,
@@ -36,8 +36,34 @@ impl<'a> NoUndefinedVariables<'a> {
         unused: &mut Vec<&'a Spanning<&'a str>>,
         visited: &mut HashSet<Scope<'a>>,
     ) {
+        let mut to_visit = Vec::new();
+        if let Some(spreads) = self.find_undef_vars_inner(scope, defined, unused, visited) {
+            to_visit.push(spreads);
+        }
+        while let Some(spreads) = to_visit.pop() {
+            for spread in spreads {
+                if let Some(spreads) =
+                    self.find_undef_vars_inner(&Scope::Fragment(spread), defined, unused, visited)
+                {
+                    to_visit.push(spreads);
+                }
+            }
+        }
+    }
+
+    /// This function should be called only inside [`Self::find_undef_vars()`],
+    /// as it's a recursive function using heap instead of a stack. So, instead
+    /// of the recursive call, we return a [`Vec`] that is visited inside
+    /// [`Self::find_undef_vars()`].
+    fn find_undef_vars_inner(
+        &'a self,
+        scope: &Scope<'a>,
+        defined: &HashSet<&'a str>,
+        unused: &mut Vec<&'a Spanning<&'a str>>,
+        visited: &mut HashSet<Scope<'a>>,
+    ) -> Option<&'a Vec<&'a str>> {
         if visited.contains(scope) {
-            return;
+            return None;
         }
 
         visited.insert(scope.clone());
@@ -50,11 +76,7 @@ impl<'a> NoUndefinedVariables<'a> {
             }
         }
 
-        if let Some(spreads) = self.spreads.get(scope) {
-            for spread in spreads {
-                self.find_undef_vars(&Scope::Fragment(spread), defined, unused, visited);
-            }
-        }
+        self.spreads.get(scope)
     }
 }
 
diff --git a/juniper/src/validation/rules/no_unused_fragments.rs b/juniper/src/validation/rules/no_unused_fragments.rs
index 97b2bcf8..7e5ca7f6 100644
--- a/juniper/src/validation/rules/no_unused_fragments.rs
+++ b/juniper/src/validation/rules/no_unused_fragments.rs
@@ -7,18 +7,12 @@ use crate::{
     value::ScalarValue,
 };
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 pub enum Scope<'a> {
     Operation(Option<&'a str>),
     Fragment(&'a str),
 }
 
-pub struct NoUnusedFragments<'a> {
-    spreads: HashMap<Scope<'a>, Vec<&'a str>>,
-    defined_fragments: HashSet<Spanning<&'a str>>,
-    current_scope: Option<Scope<'a>>,
-}
-
 pub fn factory<'a>() -> NoUnusedFragments<'a> {
     NoUnusedFragments {
         spreads: HashMap::new(),
@@ -27,21 +21,42 @@ pub fn factory<'a>() -> NoUnusedFragments<'a> {
     }
 }
 
+pub struct NoUnusedFragments<'a> {
+    spreads: HashMap<Scope<'a>, Vec<&'a str>>,
+    defined_fragments: HashSet<Spanning<&'a str>>,
+    current_scope: Option<Scope<'a>>,
+}
+
 impl<'a> NoUnusedFragments<'a> {
-    fn find_reachable_fragments(&self, from: &Scope<'a>, result: &mut HashSet<&'a str>) {
-        if let Scope::Fragment(name) = *from {
+    fn find_reachable_fragments(&'a self, from: Scope<'a>, result: &mut HashSet<&'a str>) {
+        let mut to_visit = Vec::new();
+        to_visit.push(from);
+
+        while let Some(from) = to_visit.pop() {
+            if let Some(next) = self.find_reachable_fragments_inner(from, result) {
+                to_visit.extend(next.iter().map(|s| Scope::Fragment(s)));
+            }
+        }
+    }
+
+    /// This function should be called only inside
+    /// [`Self::find_reachable_fragments()`], as it's a recursive function using
+    /// heap instead of a stack. So, instead of the recursive call, we return a
+    /// [`Vec`] that is visited inside [`Self::find_reachable_fragments()`].
+    fn find_reachable_fragments_inner(
+        &'a self,
+        from: Scope<'a>,
+        result: &mut HashSet<&'a str>,
+    ) -> Option<&'a Vec<&'a str>> {
+        if let Scope::Fragment(name) = from {
             if result.contains(name) {
-                return;
+                return None;
             } else {
                 result.insert(name);
             }
         }
 
-        if let Some(spreads) = self.spreads.get(from) {
-            for spread in spreads {
-                self.find_reachable_fragments(&Scope::Fragment(spread), result)
-            }
-        }
+        self.spreads.get(&from)
     }
 }
 
@@ -59,7 +74,7 @@ where
             }) = *def
             {
                 let op_name = name.as_ref().map(|s| s.item);
-                self.find_reachable_fragments(&Scope::Operation(op_name), &mut reachable);
+                self.find_reachable_fragments(Scope::Operation(op_name), &mut reachable);
             }
         }
 
@@ -96,7 +111,7 @@ where
     ) {
         if let Some(ref scope) = self.current_scope {
             self.spreads
-                .entry(scope.clone())
+                .entry(*scope)
                 .or_insert_with(Vec::new)
                 .push(spread.item.name.item);
         }
diff --git a/juniper/src/validation/rules/no_unused_variables.rs b/juniper/src/validation/rules/no_unused_variables.rs
index 35e5f933..66b92299 100644
--- a/juniper/src/validation/rules/no_unused_variables.rs
+++ b/juniper/src/validation/rules/no_unused_variables.rs
@@ -12,13 +12,6 @@ pub enum Scope<'a> {
     Fragment(&'a str),
 }
 
-pub struct NoUnusedVariables<'a> {
-    defined_variables: HashMap<Option<&'a str>, HashSet<&'a Spanning<&'a str>>>,
-    used_variables: HashMap<Scope<'a>, Vec<&'a str>>,
-    current_scope: Option<Scope<'a>>,
-    spreads: HashMap<Scope<'a>, Vec<&'a str>>,
-}
-
 pub fn factory<'a>() -> NoUnusedVariables<'a> {
     NoUnusedVariables {
         defined_variables: HashMap::new(),
@@ -28,16 +21,49 @@ pub fn factory<'a>() -> NoUnusedVariables<'a> {
     }
 }
 
+pub struct NoUnusedVariables<'a> {
+    defined_variables: HashMap<Option<&'a str>, HashSet<&'a Spanning<&'a str>>>,
+    used_variables: HashMap<Scope<'a>, Vec<&'a str>>,
+    current_scope: Option<Scope<'a>>,
+    spreads: HashMap<Scope<'a>, Vec<&'a str>>,
+}
+
 impl<'a> NoUnusedVariables<'a> {
     fn find_used_vars(
-        &self,
+        &'a self,
         from: &Scope<'a>,
         defined: &HashSet<&'a str>,
         used: &mut HashSet<&'a str>,
         visited: &mut HashSet<Scope<'a>>,
     ) {
+        let mut to_visit = Vec::new();
+        if let Some(spreads) = self.find_used_vars_inner(from, defined, used, visited) {
+            to_visit.push(spreads);
+        }
+        while let Some(spreads) = to_visit.pop() {
+            for spread in spreads {
+                if let Some(spreads) =
+                    self.find_used_vars_inner(&Scope::Fragment(spread), defined, used, visited)
+                {
+                    to_visit.push(spreads);
+                }
+            }
+        }
+    }
+
+    /// This function should be called only inside [`Self::find_used_vars()`],
+    /// as it's a recursive function using heap instead of a stack. So, instead
+    /// of the recursive call, we return a [`Vec`] that is visited inside
+    /// [`Self::find_used_vars()`].
+    fn find_used_vars_inner(
+        &'a self,
+        from: &Scope<'a>,
+        defined: &HashSet<&'a str>,
+        used: &mut HashSet<&'a str>,
+        visited: &mut HashSet<Scope<'a>>,
+    ) -> Option<&'a Vec<&'a str>> {
         if visited.contains(from) {
-            return;
+            return None;
         }
 
         visited.insert(from.clone());
@@ -50,11 +76,7 @@ impl<'a> NoUnusedVariables<'a> {
             }
         }
 
-        if let Some(spreads) = self.spreads.get(from) {
-            for spread in spreads {
-                self.find_used_vars(&Scope::Fragment(spread), defined, used, visited);
-            }
-        }
+        self.spreads.get(from)
     }
 }
 
diff --git a/juniper/src/validation/rules/overlapping_fields_can_be_merged.rs b/juniper/src/validation/rules/overlapping_fields_can_be_merged.rs
index 30ec9226..5aa82120 100644
--- a/juniper/src/validation/rules/overlapping_fields_can_be_merged.rs
+++ b/juniper/src/validation/rules/overlapping_fields_can_be_merged.rs
@@ -274,30 +274,61 @@ impl<'a, S: Debug> OverlappingFieldsCanBeMerged<'a, S> {
     ) where
         S: ScalarValue,
     {
-        let fragment = match self.named_fragments.get(fragment_name) {
-            Some(f) => f,
-            None => return,
-        };
+        let mut to_check = Vec::new();
+        if let Some(fragments) = self.collect_conflicts_between_fields_and_fragment_inner(
+            conflicts,
+            field_map,
+            fragment_name,
+            mutually_exclusive,
+            ctx,
+        ) {
+            to_check.push((fragment_name, fragments))
+        }
+
+        while let Some((fragment_name, fragment_names2)) = to_check.pop() {
+            for fragment_name2 in fragment_names2 {
+                // Early return on fragment recursion, as it makes no sense.
+                // Fragment recursions are prevented by `no_fragment_cycles` validator.
+                if fragment_name == fragment_name2 {
+                    return;
+                }
+                if let Some(fragments) = self.collect_conflicts_between_fields_and_fragment_inner(
+                    conflicts,
+                    field_map,
+                    fragment_name2,
+                    mutually_exclusive,
+                    ctx,
+                ) {
+                    to_check.push((fragment_name2, fragments));
+                };
+            }
+        }
+    }
+
+    /// This function should be called only inside
+    /// [`Self::collect_conflicts_between_fields_and_fragment()`], as it's a
+    /// recursive function using heap instead of a stack. So, instead of the
+    /// recursive call, we return a [`Vec`] that is visited inside
+    /// [`Self::collect_conflicts_between_fields_and_fragment()`].
+    fn collect_conflicts_between_fields_and_fragment_inner(
+        &self,
+        conflicts: &mut Vec<Conflict>,
+        field_map: &AstAndDefCollection<'a, S>,
+        fragment_name: &str,
+        mutually_exclusive: bool,
+        ctx: &ValidatorContext<'a, S>,
+    ) -> Option<Vec<&'a str>>
+    where
+        S: ScalarValue,
+    {
+        let fragment = self.named_fragments.get(fragment_name)?;
 
         let (field_map2, fragment_names2) =
             self.get_referenced_fields_and_fragment_names(fragment, ctx);
 
         self.collect_conflicts_between(conflicts, mutually_exclusive, field_map, &field_map2, ctx);
 
-        for fragment_name2 in fragment_names2 {
-            // Early return on fragment recursion, as it makes no sense.
-            // Fragment recursions are prevented by `no_fragment_cycles` validator.
-            if fragment_name == fragment_name2 {
-                return;
-            }
-            self.collect_conflicts_between_fields_and_fragment(
-                conflicts,
-                field_map,
-                fragment_name2,
-                mutually_exclusive,
-                ctx,
-            );
-        }
+        Some(fragment_names2)
     }
 
     fn collect_conflicts_between(
diff --git a/juniper/src/validation/rules/variables_in_allowed_position.rs b/juniper/src/validation/rules/variables_in_allowed_position.rs
index 9e7e3fc3..ad1e9884 100644
--- a/juniper/src/validation/rules/variables_in_allowed_position.rs
+++ b/juniper/src/validation/rules/variables_in_allowed_position.rs
@@ -17,13 +17,6 @@ pub enum Scope<'a> {
     Fragment(&'a str),
 }
 
-pub struct VariableInAllowedPosition<'a, S: Debug + 'a> {
-    spreads: HashMap<Scope<'a>, HashSet<&'a str>>,
-    variable_usages: HashMap<Scope<'a>, Vec<(Spanning<&'a String>, Type<'a>)>>,
-    variable_defs: HashMap<Scope<'a>, Vec<&'a (Spanning<&'a str>, VariableDefinition<'a, S>)>>,
-    current_scope: Option<Scope<'a>>,
-}
-
 pub fn factory<'a, S: Debug>() -> VariableInAllowedPosition<'a, S> {
     VariableInAllowedPosition {
         spreads: HashMap::new(),
@@ -33,16 +26,54 @@ pub fn factory<'a, S: Debug>() -> VariableInAllowedPosition<'a, S> {
     }
 }
 
+pub struct VariableInAllowedPosition<'a, S: Debug + 'a> {
+    spreads: HashMap<Scope<'a>, HashSet<&'a str>>,
+    variable_usages: HashMap<Scope<'a>, Vec<(Spanning<&'a String>, Type<'a>)>>,
+    #[allow(clippy::type_complexity)]
+    variable_defs: HashMap<Scope<'a>, Vec<&'a (Spanning<&'a str>, VariableDefinition<'a, S>)>>,
+    current_scope: Option<Scope<'a>>,
+}
+
 impl<'a, S: Debug> VariableInAllowedPosition<'a, S> {
-    fn collect_incorrect_usages(
-        &self,
+    fn collect_incorrect_usages<'me>(
+        &'me self,
         from: &Scope<'a>,
         var_defs: &[&'a (Spanning<&'a str>, VariableDefinition<S>)],
         ctx: &mut ValidatorContext<'a, S>,
         visited: &mut HashSet<Scope<'a>>,
     ) {
+        let mut to_visit = Vec::new();
+        if let Some(spreads) = self.collect_incorrect_usages_inner(from, var_defs, ctx, visited) {
+            to_visit.push(spreads);
+        }
+
+        while let Some(spreads) = to_visit.pop() {
+            for spread in spreads {
+                if let Some(spreads) = self.collect_incorrect_usages_inner(
+                    &Scope::Fragment(spread),
+                    var_defs,
+                    ctx,
+                    visited,
+                ) {
+                    to_visit.push(spreads);
+                }
+            }
+        }
+    }
+
+    /// This function should be called only inside
+    /// [`Self::collect_incorrect_usages()`], as it's a recursive function using
+    /// heap instead of a stack. So, instead of the recursive call, we return a
+    /// [`Vec`] that is visited inside [`Self::collect_incorrect_usages()`].
+    fn collect_incorrect_usages_inner<'me>(
+        &'me self,
+        from: &Scope<'a>,
+        var_defs: &[&'a (Spanning<&'a str>, VariableDefinition<S>)],
+        ctx: &mut ValidatorContext<'a, S>,
+        visited: &mut HashSet<Scope<'a>>,
+    ) -> Option<&'me HashSet<&'a str>> {
         if visited.contains(from) {
-            return;
+            return None;
         }
 
         visited.insert(from.clone());
@@ -75,11 +106,7 @@ impl<'a, S: Debug> VariableInAllowedPosition<'a, S> {
             }
         }
 
-        if let Some(spreads) = self.spreads.get(from) {
-            for spread in spreads {
-                self.collect_incorrect_usages(&Scope::Fragment(spread), var_defs, ctx, visited);
-            }
-        }
+        self.spreads.get(from)
     }
 }
 

From 6fd7a591cf43255712f102892a46c732ee193f9e Mon Sep 17 00:00:00 2001
From: tyranron <tyranron@gmail.com>
Date: Thu, 28 Jul 2022 17:12:27 +0300
Subject: [PATCH 2/2] Release `juniper` 0.15.10

---
 juniper/CHANGELOG.md             | 2 +-
 juniper/Cargo.toml               | 2 +-
 juniper/src/lib.rs               | 2 +-
 juniper_actix/Cargo.toml         | 4 ++--
 juniper_codegen/Cargo.toml       | 2 +-
 juniper_graphql_ws/Cargo.toml    | 2 +-
 juniper_hyper/Cargo.toml         | 4 ++--
 juniper_iron/Cargo.toml          | 4 ++--
 juniper_rocket/Cargo.toml        | 4 ++--
 juniper_subscriptions/Cargo.toml | 2 +-
 juniper_warp/Cargo.toml          | 4 ++--
 11 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md
index 6ae5c2c8..1b9e8e80 100644
--- a/juniper/CHANGELOG.md
+++ b/juniper/CHANGELOG.md
@@ -1,4 +1,4 @@
-# master
+# [[0.15.10] 2022-07-28](https://github.com/graphql-rust/juniper/releases/tag/juniper-v0.15.10)
 
 - Fix [CVE-2022-31173](https://github.com/graphql-rust/juniper/security/advisories/GHSA-4rx6-g5vg-5f3j).
 - Fix incorrect error when explicit `null` provided for `null`able list input parameter. ([#1086](https://github.com/graphql-rust/juniper/pull/1086))
diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml
index 3b083370..96c4b5ae 100644
--- a/juniper/Cargo.toml
+++ b/juniper/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "juniper"
-version = "0.15.9"
+version = "0.15.10"
 authors = [
     "Magnus Hallin <mhallin@fastmail.com>",
     "Christoph Herzog <chris@theduke.at>",
diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs
index 85b0bafb..f7c9452e 100644
--- a/juniper/src/lib.rs
+++ b/juniper/src/lib.rs
@@ -90,7 +90,7 @@ Juniper has not reached 1.0 yet, thus some API instability should be expected.
 [bson]: https://crates.io/crates/bson
 
 */
-#![doc(html_root_url = "https://docs.rs/juniper/0.15.9")]
+#![doc(html_root_url = "https://docs.rs/juniper/0.15.10")]
 #![warn(missing_docs)]
 
 // Required for using `juniper_codegen` macros inside this crate to resolve absolute `::juniper`
diff --git a/juniper_actix/Cargo.toml b/juniper_actix/Cargo.toml
index c59e0dfa..e8aecb31 100644
--- a/juniper_actix/Cargo.toml
+++ b/juniper_actix/Cargo.toml
@@ -18,7 +18,7 @@ http = "0.2.4"
 actix-web = "4.0.0-beta.8"
 actix-web-actors = "4.0.0-beta.6"
 
-juniper = { version = "0.15.9", path = "../juniper", default-features = false }
+juniper = { version = "0.15.10", path = "../juniper", default-features = false }
 juniper_graphql_ws = { version = "0.2.6", path = "../juniper_graphql_ws", optional = true }
 
 anyhow = "1.0"
@@ -35,7 +35,7 @@ tokio = "1"
 async-stream = "0.3"
 actix-test = "0.1.0-beta.3"
 
-juniper = { version = "0.15.9", path = "../juniper", features = ["expose-test-schema"] }
+juniper = { version = "0.15.10", path = "../juniper", features = ["expose-test-schema"] }
 
 bytes = "1.0"
 env_logger = "0.8"
diff --git a/juniper_codegen/Cargo.toml b/juniper_codegen/Cargo.toml
index dfe54e63..85449f41 100644
--- a/juniper_codegen/Cargo.toml
+++ b/juniper_codegen/Cargo.toml
@@ -26,4 +26,4 @@ syn = { version = "1.0.60", features = ["extra-traits", "full", "parsing"], defa
 [dev-dependencies]
 derive_more = "0.99.7"
 futures = "0.3"
-juniper = { version = "0.15.9", path = "../juniper" }
+juniper = { version = "0.15.10", path = "../juniper" }
diff --git a/juniper_graphql_ws/Cargo.toml b/juniper_graphql_ws/Cargo.toml
index e7f1ce34..b9b12b89 100644
--- a/juniper_graphql_ws/Cargo.toml
+++ b/juniper_graphql_ws/Cargo.toml
@@ -10,7 +10,7 @@ repository = "https://github.com/graphql-rust/juniper"
 keywords = ["apollo", "graphql", "graphql-ws", "juniper"]
 
 [dependencies]
-juniper = { version = "0.15.9", path = "../juniper", default-features = false }
+juniper = { version = "0.15.10", path = "../juniper", default-features = false }
 juniper_subscriptions = { version = "0.15.6", path = "../juniper_subscriptions" }
 serde = { version = "1.0.8", features = ["derive"], default-features = false }
 tokio = { version = "1", features = ["macros", "rt", "time"], default-features = false }
diff --git a/juniper_hyper/Cargo.toml b/juniper_hyper/Cargo.toml
index 6b0cffa4..3808b69d 100644
--- a/juniper_hyper/Cargo.toml
+++ b/juniper_hyper/Cargo.toml
@@ -10,14 +10,14 @@ repository = "https://github.com/graphql-rust/juniper"
 
 [dependencies]
 futures = "0.3.1"
-juniper = { version = "0.15.9", path = "../juniper", default-features = false }
+juniper = { version = "0.15.10", path = "../juniper", default-features = false }
 hyper = {version = "0.14", features = ["server", "runtime"]}
 serde_json = "1.0"
 tokio = "1"
 url = "2"
 
 [dev-dependencies]
-juniper = { version = "0.15.9", path = "../juniper", features = ["expose-test-schema"] }
+juniper = { version = "0.15.10", path = "../juniper", features = ["expose-test-schema"] }
 pretty_env_logger = "0.4"
 reqwest = { version = "0.11", features = ["blocking", "rustls-tls"] }
 tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
diff --git a/juniper_iron/Cargo.toml b/juniper_iron/Cargo.toml
index 46239046..a98b04c7 100644
--- a/juniper_iron/Cargo.toml
+++ b/juniper_iron/Cargo.toml
@@ -13,13 +13,13 @@ repository = "https://github.com/graphql-rust/juniper"
 
 [dependencies]
 futures = "0.3.1"
-juniper = { version = "0.15.9", path = "../juniper" }
+juniper = { version = "0.15.10", path = "../juniper" }
 iron = ">= 0.5, < 0.7"
 serde_json = "1.0.2"
 urlencoded = ">= 0.5, < 0.7"
 
 [dev-dependencies]
-juniper = { version = "0.15.9", path = "../juniper", features = ["expose-test-schema"] }
+juniper = { version = "0.15.10", path = "../juniper", features = ["expose-test-schema"] }
 iron-test = "0.6"
 logger = "0.4"
 mount = "0.4"
diff --git a/juniper_rocket/Cargo.toml b/juniper_rocket/Cargo.toml
index 7b68c0a2..f92315ce 100644
--- a/juniper_rocket/Cargo.toml
+++ b/juniper_rocket/Cargo.toml
@@ -13,9 +13,9 @@ repository = "https://github.com/graphql-rust/juniper"
 
 [dependencies]
 futures = "0.3.1"
-juniper = { version = "0.15.9", path = "../juniper", default-features = false }
+juniper = { version = "0.15.10", path = "../juniper", default-features = false }
 rocket = { version = "0.5.0-rc.1", default-features = false }
 serde_json = "1.0.2"
 
 [dev-dependencies]
-juniper = { version = "0.15.9", path = "../juniper", features = ["expose-test-schema"] }
+juniper = { version = "0.15.10", path = "../juniper", features = ["expose-test-schema"] }
diff --git a/juniper_subscriptions/Cargo.toml b/juniper_subscriptions/Cargo.toml
index d7bb9781..623de98d 100644
--- a/juniper_subscriptions/Cargo.toml
+++ b/juniper_subscriptions/Cargo.toml
@@ -10,7 +10,7 @@ repository = "https://github.com/graphql-rust/juniper"
 
 [dependencies]
 futures = "0.3.1"
-juniper = { version = "0.15.9", path = "../juniper", default-features = false }
+juniper = { version = "0.15.10", path = "../juniper", default-features = false }
 
 [dev-dependencies]
 serde_json = "1.0"
diff --git a/juniper_warp/Cargo.toml b/juniper_warp/Cargo.toml
index 11c10449..59b0328a 100644
--- a/juniper_warp/Cargo.toml
+++ b/juniper_warp/Cargo.toml
@@ -14,7 +14,7 @@ subscriptions = ["juniper_graphql_ws"]
 [dependencies]
 anyhow = "1.0"
 futures = "0.3.1"
-juniper = { version = "0.15.9", path = "../juniper", default-features = false }
+juniper = { version = "0.15.10", path = "../juniper", default-features = false }
 juniper_graphql_ws = { version = "0.2.6", path = "../juniper_graphql_ws", optional = true }
 serde = { version = "1.0.75", features = ["derive"] }
 serde_json = "1.0.24"
@@ -24,7 +24,7 @@ warp = "0.3"
 
 [dev-dependencies]
 env_logger = "0.8"
-juniper = { version = "0.15.9", path = "../juniper", features = ["expose-test-schema"] }
+juniper = { version = "0.15.10", path = "../juniper", features = ["expose-test-schema"] }
 log = "0.4"
 percent-encoding = "2.1"
 tokio = { version = "1", features = ["macros", "rt-multi-thread"] }