Skip to content

Memory leak: the shared validation_aborted_error keeps every traceback it is raised through #287

Closed

Description

@riodw

Since aab6d50 (first released in 3.3.0a3), validate() raises one shared, module-level ValidationAbortedError once max_errors is reached. When an existing exception object is raised again, Python adds the new frames to its existing __traceback__. So every aborted validation lengthens the singleton's traceback, and that traceback keeps those frames, and with them each request's document and validation context, alive for the life of the process.

Any client can trigger this by sending a query with more than 100 validation errors. In a long-running server, memory grows with every such request.

Reproduction (graphql-core 3.3.0, CPython 3.12):

import gc, tracemalloc
from graphql import build_schema, parse, validate
from graphql.validation.validate import validation_aborted_error

schema = build_schema("type Query { a: Int }")
document = parse("{ " + " ".join(f"f{i}" for i in range(101)) + " }")

tracemalloc.start()
validate(schema, document)
gc.collect()
base = tracemalloc.get_traced_memory()[0]
for _ in range(200):
    validate(schema, document)
gc.collect()
tb, n = validation_aborted_error.__traceback__, 0
while tb:
    n, tb = n + 1, tb.tb_next
print(n, (tracemalloc.get_traced_memory()[0] - base) // 1024, "KiB")

Result:

Repeated calls Traceback entries Memory retained
1 16 147 KiB
10 88 1,389 KiB
100 808 12,549 KiB
200 1,608 24,820 KiB

That is about 124 KiB more per aborted validation, with no upper limit. The same script on 3.2.13, which creates a new error each time, levels off at about 259 KiB.

Suggested fix: create a new ValidationAbortedError on each validate() call instead of reusing the module-level instance. Clearing __traceback__ before each raise would also work, but every caller's error list would still share one mutable error object.

Activity

  1. Cito commented on Oct 25, 2026

    @Cito
    Member

    Thanks a lot for the detailed report, @riodw! Confirmed and reproduced: this affects 3.3.0 and the pre-releases since 3.3.0a3, but not 3.2.x.

    The fix is in #289. Besides creating a new error for every aborted validation, it also makes sure that the aborted error does not keep the frames alive through the context of an exception that was being handled when the error limit was reached (this can happen with the comparison budget of OverlappingFieldsCanBeMergedRule).

    I'm going to release 3.3.1 with this fix shortly, together with a security advisory. Reviews of #289 or checks of the release are still very welcome.

  2. Cito commented on Oct 25, 2026

    @Cito
    Member

    The fix has been released in GraphQL-core 3.3.1, together with a security advisory. Thanks again, @riodw!

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions