From a39dff9dc943b9e2045fc3d4944a5b0c721f7de1 Mon Sep 17 00:00:00 2001 From: Glen Date: Mon, 30 Mar 2026 17:37:29 +0200 Subject: [PATCH 1/3] [Fusion] Add depth limit to satisfiability validator --- .../SatisfiabilityValidator.cs | 11 ++++ .../SatisfiabilityValidatorTests.cs | 52 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs index 4f5f0b4ee46..2ed79559d92 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs @@ -18,6 +18,8 @@ namespace HotChocolate.Fusion; internal sealed class SatisfiabilityValidator { + private const int MaxRecursionDepth = 500; + private readonly SatisfiabilityOptions _options; private readonly RequirementsValidator _requirementsValidator; private readonly MutableSchemaDefinition _schema; @@ -58,6 +60,12 @@ private void VisitObjectType( MutableObjectTypeDefinition objectType, SatisfiabilityValidatorContext context) { + if (context.Depth >= MaxRecursionDepth) + { + return; + } + + context.Depth++; context.TypeContext.Push(objectType); foreach (var field in objectType.Fields) @@ -90,6 +98,7 @@ private void VisitObjectType( } context.TypeContext.Pop(); + context.Depth--; } private void VisitOutputField( @@ -427,4 +436,6 @@ internal sealed class SatisfiabilityValidatorContext public SatisfiabilityPath CycleDetectionPath { get; } = []; public HashSet FieldAccessCache { get; } = []; + + public int Depth { get; set; } } diff --git a/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SatisfiabilityValidatorTests.cs b/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SatisfiabilityValidatorTests.cs index fa480cc2f87..ff9dc5f3d51 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SatisfiabilityValidatorTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SatisfiabilityValidatorTests.cs @@ -1,3 +1,4 @@ +using System.Text; using HotChocolate.Fusion.Logging; using HotChocolate.Fusion.Options; using static HotChocolate.Fusion.CompositionTestHelper; @@ -2899,4 +2900,55 @@ type Cat implements Node { } }; } + + [Fact] + public void LargeTypeCycle_DoesNotCauseStackOverflow() + { + // arrange + // Creates a long type cycle (T1 → T2 → … → T500 → T1) across 5 identical schemas. + // Each schema provides every type and field. Without the recursion depth limit + // in VisitObjectType, the recursion depth is O(types × schemas) = 2500+ frames, + // which overflows the default 1 MB stack. + const int typeCount = 500; + const int schemaCount = 5; + var schemas = new string[schemaCount]; + + var sb = new StringBuilder(); + sb.AppendLine("type Query {"); + + for (var t = 1; t <= typeCount; t++) + { + sb.AppendLine($" t{t}ById(id: ID!): T{t} @lookup"); + } + + sb.AppendLine("}"); + + for (var t = 1; t <= typeCount; t++) + { + var next = (t % typeCount) + 1; + sb.AppendLine( + $"type T{t} @key(fields: \"id\") {{ id: ID! @shareable next: T{next} @shareable }}"); + } + + var sdl = sb.ToString(); + + for (var i = 0; i < schemaCount; i++) + { + schemas[i] = sdl; + } + + var merger = new SourceSchemaMerger( + CreateSchemaDefinitions(schemas), + new SourceSchemaMergerOptions { AddFusionDefinitions = false }); + + var schema = merger.Merge().Value; + var log = new CompositionLog(); + var satisfiabilityValidator = new SatisfiabilityValidator(schema, log); + + // act + var result = satisfiabilityValidator.Validate(); + + // assert + Assert.True(result.IsSuccess); + } } From eb674adf482465261157d9df83934d2acc3ebaff Mon Sep 17 00:00:00 2001 From: Glen Date: Mon, 30 Mar 2026 18:07:13 +0200 Subject: [PATCH 2/3] Address Copilot feedback --- .../CompositionResources.Designer.cs | 9 +++++++++ .../Properties/CompositionResources.resx | 3 +++ .../SatisfiabilityValidator.cs | 18 ++++++++++++++++++ .../SatisfiabilityValidatorTests.cs | 7 +++++++ 4 files changed, 37 insertions(+) diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/Properties/CompositionResources.Designer.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/Properties/CompositionResources.Designer.cs index d34e4766422..52a29be6614 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Composition/Properties/CompositionResources.Designer.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Composition/Properties/CompositionResources.Designer.cs @@ -1501,6 +1501,15 @@ internal static string SatisfiabilityValidator_CycleDetected { } } + /// + /// Looks up a localized string similar to Satisfiability validation reached the maximum recursion depth ({0}) while visiting type '{1}'. Validation of deeply nested fields may be incomplete.. + /// + internal static string SatisfiabilityValidator_MaxRecursionDepthReached { + get { + return ResourceManager.GetString("SatisfiabilityValidator_MaxRecursionDepthReached", resourceCulture); + } + } + /// /// Looks up a localized string similar to Type '{0}' implements the 'Node' interface, but no source schema provides a non-internal 'Query.node<Node>' lookup field for this type.. /// diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/Properties/CompositionResources.resx b/src/HotChocolate/Fusion/src/Fusion.Composition/Properties/CompositionResources.resx index 206ffcea0d6..8748bfaafc9 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Composition/Properties/CompositionResources.resx +++ b/src/HotChocolate/Fusion/src/Fusion.Composition/Properties/CompositionResources.resx @@ -495,6 +495,9 @@ Cycle detected: {0} -> {1}. + + Satisfiability validation reached the maximum recursion depth ({0}) while visiting type '{1}'. Validation of deeply nested fields may be incomplete. + Type '{0}' implements the 'Node' interface, but no source schema provides a non-internal 'Query.node<Node>' lookup field for this type. diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs index 2ed79559d92..c98fe925b4b 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs @@ -62,6 +62,22 @@ private void VisitObjectType( { if (context.Depth >= MaxRecursionDepth) { + if (!context.DepthLimitReached) + { + context.DepthLimitReached = true; + + _log.Write( + LogEntryBuilder.New() + .SetMessage( + string.Format( + SatisfiabilityValidator_MaxRecursionDepthReached, + MaxRecursionDepth, + objectType.Name)) + .SetCode(LogEntryCodes.Unsatisfiable) + .SetSeverity(LogSeverity.Warning) + .Build()); + } + return; } @@ -438,4 +454,6 @@ internal sealed class SatisfiabilityValidatorContext public HashSet FieldAccessCache { get; } = []; public int Depth { get; set; } + + public bool DepthLimitReached { get; set; } } diff --git a/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SatisfiabilityValidatorTests.cs b/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SatisfiabilityValidatorTests.cs index ff9dc5f3d51..449784350a5 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SatisfiabilityValidatorTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SatisfiabilityValidatorTests.cs @@ -2950,5 +2950,12 @@ public void LargeTypeCycle_DoesNotCauseStackOverflow() // assert Assert.True(result.IsSuccess); + var logEntry = Assert.Single(log); + Assert.Equal(LogSeverity.Warning, logEntry.Severity); + Assert.Equal(LogEntryCodes.Unsatisfiable, logEntry.Code); + Assert.Equal( + "Satisfiability validation reached the maximum recursion depth (500) " + + "while visiting type 'T500'. Validation of deeply nested fields may be incomplete.", + logEntry.Message); } } From c6daf995b7ccb07b1b9b82bc3e826dba3db97352 Mon Sep 17 00:00:00 2001 From: Glen Date: Tue, 31 Mar 2026 09:57:13 +0200 Subject: [PATCH 3/3] Address more Copilot feedback --- .../src/Fusion.Composition/SatisfiabilityValidator.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs index c98fe925b4b..1327b454440 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs @@ -69,10 +69,9 @@ private void VisitObjectType( _log.Write( LogEntryBuilder.New() .SetMessage( - string.Format( - SatisfiabilityValidator_MaxRecursionDepthReached, - MaxRecursionDepth, - objectType.Name)) + SatisfiabilityValidator_MaxRecursionDepthReached, + MaxRecursionDepth, + objectType.Name) .SetCode(LogEntryCodes.Unsatisfiable) .SetSeverity(LogSeverity.Warning) .Build());