Skip to content

Use merged-field counting for general query complexity limits - #4523

Open
andimarek wants to merge 6 commits into
masterfrom
codex/general-dos-protection
Open

andimarek wants to merge 6 commits into
masterfrom
codex/general-dos-protection

Conversation

@andimarek

@andimarek andimarek commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Extend Good Faith's merged-field calculation to general QueryComplexityLimits. Numeric defaults stay the same. Previously, general limits counted expanded AST occurrences independently of Good Faith's merged counts; now both use merged validation groups.

Which limits apply?

Limits apply independently to each operation in the validated document, including unselected operations.

Operation, with default settings Field limit Depth limit Additional Good Faith restrictions
Ordinary query, mutation or subscription, including __typename alone 100,000 100 None
Query selecting root __schema or __type, whether pure introspection or mixed with ordinary fields 500 20 Restricted coordinates below

Good Faith covers the whole mixed operation, including ordinary fields. Aliases/fragments and skipped root introspection selections also activate it. All branches count regardless of @skip/@include; variables are not coerced.

Good Faith permits at most one merged occurrence per operation of each: root __schema, root __type, __Type.fields, __Type.inputFields, __Type.interfaces, and __Type.possibleTypes. Aliases or distinct parent positions count separately toward these caps.

Configuration remains independent:

  • Custom general limits apply normally; introspection takes the smaller of each configured limit and its Good Faith limit (500 fields / depth 20).
  • QueryComplexityLimits.NONE disables general limits, but enabled Good Faith limits and coordinate caps still apply.
  • Disabling Good Faith leaves general limits in place. Disabling both removes these complexity protections; parsing and ordinary validation safeguards remain.

How AST versus merged counting changes acceptance

Both object and leaf fields count. AST counting charges for occurrences, including repeated fragment spreads. Merged counting combines compatible fields and their children by response name and common parent at the same position. Aliases and different positions contribute separately.

Selection Previous general AST count New general merged count
{ node { name name } } 3 2
{ node { ...F ...F } }, with fragment F on Node { name } 3 2
{ a: node { name } b: node { name } } 4 4

With a general field limit of 2, the first two examples now pass; aliases still fail. Depth remains 2 in all three. Repeated-fragment trees can likewise pass despite exceeding the old expanded-AST budget. No separate expanded-AST field budget remains, including for introspection. Its existing Good Faith merged-count and coordinate protections remain.

Merged counting can also be stricter for abstract types. Suppose I is implemented by A and B, all have name: String, and their child fields return I, A and B, respectively; Query.node returns I:

{
  node {
    child { a: name b: name c: name }
    ... on A { child { name } }
    ... on B { child { name } }
  }
}

This counts 9 AST fields but 11 merged validation fields: the shared child selections contribute to both concrete-parent groups, giving 1 for node plus 5 per child group. A field limit of 9 therefore changes from accepted to rejected. These are validation-group counts, not ENO/runtime-type counts or estimates of list lengths and resolver cost.

Merge state remains document-wide; count summaries remain per operation. Depth is checked before child collection, and long counts prevent overflow. The separate application-specific QueryComplexityCalculator API remains available.

Performance measurements and validation

Performance

Three async-profiler/JMH optimization cycles led to singleton-set reuse, canonical validation-state keys for the count cache, reusable traversal paths, fewer empty summaries, and reduced map/rule-array allocation.

Full-validation benchmarks cover 16 workloads on a dedicated Intel E3-1270 v6, Java 25: 3 forks, 45 seconds warmup and 30 seconds measurement per fork; introspection uses 75 seconds warmup. Parsing/schema setup are outside timing. Baseline: master c3884529, before subsequent unrelated upstream changes.

Workload Master → final, µs/op Mean change
One scalar 1.024 → 0.911 −11.0%
4,000-field tree 2095.358 → 1955.644 −6.7%
Fragment-heavy 1283.354 → 1066.123 −16.9%
Depth 90 46.798 → 40.983 −12.4%
Introspection 66.887 → 54.816 −18.0%
Mixed interfaces/objects 1254.765 → 1422.849 +13.4%

Counting is not free: DEFAULT versus NONE in the final implementation adds approximately 5–29% across four controls, including the required merge-pass ordering. Shared validator optimizations explain the net improvements over master. All 189 retained timed forks passed the warmup/measurement-drift review. The committed GeneralQueryComplexityBenchmark contains the fixtures.

Protection boundaries and validation

Merged counts bound response structure, not resolver cost, list sizes, or all validation work. Repeated identical fragment spreads can now be accepted where expanded-AST counting rejected them; aliased fanout remains bounded. Parser limits, fragment-cycle detection, error limits and merge caches remain in place. A pre-existing 1,500-fragment acyclic chain still overflows a 1 MiB stack on both master and this implementation, reproduced locally and on Linux.

Full tracked Java 25 tests/TCK and build checks passed (5,999 Spock cases and 190 TCK cases; 57 skips; jcstress excluded), plus 504 targeted cases on each of Java 11/17/21 on the measured revision. No per-class coverage regressions. Tests cover exact limits, aliases/fragments, operation isolation, directives, abstract types, disabled rules, Good Faith, conflict paths, cycles, depth bypasses and count overflow.

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

Test Report

Test Results

Java Version Total Passed Failed Errors Skipped
Java 17 6218 (+41 🟢) 6162 (+41 🟢) 0 (±0) 0 (±0) 56 (±0)
Java 21 6218 (+41 🟢) 6162 (+41 🟢) 0 (±0) 0 (±0) 56 (±0)
Java 25 6218 (+41 🟢) 6162 (+41 🟢) 0 (±0) 0 (±0) 56 (±0)
jcstress 32 (±0) 32 (±0) 0 (±0) 0 (±0) 0 (±0)
Total 18686 (+123 🟢) 18518 (+123 🟢) 0 (±0) 0 (±0) 168 (±0)

Code Coverage (Java 25)

Metric Covered Missed Coverage vs Master
Lines 30280 3079 90.8% ±0.0%
Branches 8936 1506 85.6% ±0.0%
Methods 8093 1186 87.2% ±0.0%

Changed Class Coverage (4 classes)

Class Line Branch Method
g.v.FragmentComplexityInfo removed removed removed
g.v.GoodFaithIntrospectionCounts removed removed removed
g.v.QueryComplexityCounts +100.0% 🟢 +100.0% 🟢 +100.0% 🟢
g.v.QueryComplexityLimits +7.1% 🟢 ±0.0% +12.5% 🟢

Full HTML report: build artifact jacoco-html-report

Updated: 2026-11-01 09:02:26 UTC

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant