Skip to content

Commit c76b385

Browse files
authored
[Fusion] Add depth limit to satisfiability validator (#9486)
1 parent 89150fc commit c76b385

File tree

4 files changed

+99
-0
lines changed

4 files changed

+99
-0
lines changed

src/HotChocolate/Fusion/src/Fusion.Composition/Properties/CompositionResources.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/HotChocolate/Fusion/src/Fusion.Composition/Properties/CompositionResources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,9 @@
495495
<data name="SatisfiabilityValidator_CycleDetected" xml:space="preserve">
496496
<value>Cycle detected: {0} -&gt; {1}.</value>
497497
</data>
498+
<data name="SatisfiabilityValidator_MaxRecursionDepthReached" xml:space="preserve">
499+
<value>Satisfiability validation reached the maximum recursion depth ({0}) while visiting type '{1}'. Validation of deeply nested fields may be incomplete.</value>
500+
</data>
498501
<data name="SatisfiabilityValidator_NodeTypeHasNoNodeLookup" xml:space="preserve">
499502
<value>Type '{0}' implements the 'Node' interface, but no source schema provides a non-internal 'Query.node&lt;Node&gt;' lookup field for this type.</value>
500503
</data>

src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ namespace HotChocolate.Fusion;
1818

1919
internal sealed class SatisfiabilityValidator
2020
{
21+
private const int MaxRecursionDepth = 500;
22+
2123
private readonly SatisfiabilityOptions _options;
2224
private readonly RequirementsValidator _requirementsValidator;
2325
private readonly MutableSchemaDefinition _schema;
@@ -58,6 +60,27 @@ private void VisitObjectType(
5860
MutableObjectTypeDefinition objectType,
5961
SatisfiabilityValidatorContext context)
6062
{
63+
if (context.Depth >= MaxRecursionDepth)
64+
{
65+
if (!context.DepthLimitReached)
66+
{
67+
context.DepthLimitReached = true;
68+
69+
_log.Write(
70+
LogEntryBuilder.New()
71+
.SetMessage(
72+
SatisfiabilityValidator_MaxRecursionDepthReached,
73+
MaxRecursionDepth,
74+
objectType.Name)
75+
.SetCode(LogEntryCodes.Unsatisfiable)
76+
.SetSeverity(LogSeverity.Warning)
77+
.Build());
78+
}
79+
80+
return;
81+
}
82+
83+
context.Depth++;
6184
context.TypeContext.Push(objectType);
6285

6386
foreach (var field in objectType.Fields)
@@ -90,6 +113,7 @@ private void VisitObjectType(
90113
}
91114

92115
context.TypeContext.Pop();
116+
context.Depth--;
93117
}
94118

95119
private void VisitOutputField(
@@ -427,4 +451,8 @@ internal sealed class SatisfiabilityValidatorContext
427451
public SatisfiabilityPath CycleDetectionPath { get; } = [];
428452

429453
public HashSet<FieldAccessCacheKey> FieldAccessCache { get; } = [];
454+
455+
public int Depth { get; set; }
456+
457+
public bool DepthLimitReached { get; set; }
430458
}

src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SatisfiabilityValidatorTests.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Text;
12
using HotChocolate.Fusion.Logging;
23
using HotChocolate.Fusion.Options;
34
using static HotChocolate.Fusion.CompositionTestHelper;
@@ -2899,4 +2900,62 @@ type Cat implements Node {
28992900
}
29002901
};
29012902
}
2903+
2904+
[Fact]
2905+
public void LargeTypeCycle_DoesNotCauseStackOverflow()
2906+
{
2907+
// arrange
2908+
// Creates a long type cycle (T1 → T2 → … → T500 → T1) across 5 identical schemas.
2909+
// Each schema provides every type and field. Without the recursion depth limit
2910+
// in VisitObjectType, the recursion depth is O(types × schemas) = 2500+ frames,
2911+
// which overflows the default 1 MB stack.
2912+
const int typeCount = 500;
2913+
const int schemaCount = 5;
2914+
var schemas = new string[schemaCount];
2915+
2916+
var sb = new StringBuilder();
2917+
sb.AppendLine("type Query {");
2918+
2919+
for (var t = 1; t <= typeCount; t++)
2920+
{
2921+
sb.AppendLine($" t{t}ById(id: ID!): T{t} @lookup");
2922+
}
2923+
2924+
sb.AppendLine("}");
2925+
2926+
for (var t = 1; t <= typeCount; t++)
2927+
{
2928+
var next = (t % typeCount) + 1;
2929+
sb.AppendLine(
2930+
$"type T{t} @key(fields: \"id\") {{ id: ID! @shareable next: T{next} @shareable }}");
2931+
}
2932+
2933+
var sdl = sb.ToString();
2934+
2935+
for (var i = 0; i < schemaCount; i++)
2936+
{
2937+
schemas[i] = sdl;
2938+
}
2939+
2940+
var merger = new SourceSchemaMerger(
2941+
CreateSchemaDefinitions(schemas),
2942+
new SourceSchemaMergerOptions { AddFusionDefinitions = false });
2943+
2944+
var schema = merger.Merge().Value;
2945+
var log = new CompositionLog();
2946+
var satisfiabilityValidator = new SatisfiabilityValidator(schema, log);
2947+
2948+
// act
2949+
var result = satisfiabilityValidator.Validate();
2950+
2951+
// assert
2952+
Assert.True(result.IsSuccess);
2953+
var logEntry = Assert.Single(log);
2954+
Assert.Equal(LogSeverity.Warning, logEntry.Severity);
2955+
Assert.Equal(LogEntryCodes.Unsatisfiable, logEntry.Code);
2956+
Assert.Equal(
2957+
"Satisfiability validation reached the maximum recursion depth (500) "
2958+
+ "while visiting type 'T500'. Validation of deeply nested fields may be incomplete.",
2959+
logEntry.Message);
2960+
}
29022961
}

0 commit comments

Comments
 (0)