Summary
(*T).InternalizeRefs infinitely recurses (derefSchema → stack overflow) when the
document contains an external $ref to a schema that is a recursive
oneOf + discriminator union (a variant refers back to the union). The same
recursive union defined inline in a single file internalizes fine, and a plain
self-recursive object referenced externally is also fine — so the bug is specific to
the discriminator-mapping recursion on externally-internalized schemas: the
pointer-keyed visited-set (isVisitedSchema) does not stop the recursion once the
schemas are being copied in from another file.
Environment
- kin-openapi v0.140.0 (latest) — also reproduces on v0.135.0
- Go 1.24
- Surfaced originally via oapi-codegen v2.7.0
embedded-spec: true + import-mapping
(GenerateInlinedSpec → InternalizeRefs), but the repro below is pure kin-openapi.
Minimal reproduction
ext.yaml — a recursive discriminated union:
openapi: 3.1.0
info: {title: ext, version: 1.0.0}
paths: {}
components:
schemas:
Comp:
oneOf:
- $ref: '#/components/schemas/Container'
discriminator:
propertyName: type
mapping:
container: '#/components/schemas/Container'
Container:
type: object
required: [type]
properties:
type: { type: string }
children:
type: array
items: { $ref: '#/components/schemas/Comp' } # variant -> back to the union
a.yaml — references the union across files:
openapi: 3.1.0
info: {title: a, version: 1.0.0}
paths: {}
components:
schemas:
Probe:
type: object
properties:
deep: { $ref: './ext.yaml#/components/schemas/Comp' }
main.go:
package main
import (
"context"
"fmt"
"os"
"github.com/getkin/kin-openapi/openapi3"
)
func main() {
l := openapi3.NewLoader()
l.IsExternalRefsAllowed = true
doc, err := l.LoadFromFile(os.Args[1])
if err != nil {
fmt.Println("load error:", err)
os.Exit(1)
}
doc.InternalizeRefs(context.Background(), nil)
fmt.Println("OK: no overflow")
}
Run: go run . a.yaml
Expected
InternalizeRefs terminates and internalizes Comp/Container (it does exactly this
when the union is inlined in a single file).
Actual
runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflow
...
github.com/getkin/kin-openapi/openapi3.(*T).derefSchema(...)
.../openapi3/internalize_refs.go:364
github.com/getkin/kin-openapi/openapi3.(*T).derefSchema(...)
.../openapi3/internalize_refs.go:343
...
github.com/getkin/kin-openapi/openapi3.(*T).InternalizeRefs(...)
.../openapi3/internalize_refs.go:511
Comparison (isolates the trigger)
| Case |
Result |
| Recursive union inline in one file (no external ref) |
OK |
Plain self-recursive object (Node.child -> Node) via external ref |
OK |
Recursive oneOf+discriminator union via external ref (above) |
stack overflow |
Analysis
derefSchema guards re-entry with if s == nil || doc.isVisitedSchema(s) { return },
which terminates the inline case. With an external ref, the discriminator-mapping
branch (for k := range s.Discriminator.Mapping { ... addSchemaToSpec(s2); derefSchema(s2.Value); s.Discriminator.Mapping[k] = MappingRef(*s2) })
appears to descend into freshly-internalized copies that the pointer-keyed visited-set
doesn't recognize, so the recursion never terminates. This may be related to the
transitive-external-internalization behavior added in #618 / PR #655.
Summary
(*T).InternalizeRefsinfinitely recurses (derefSchema→ stack overflow) when thedocument contains an external
$refto a schema that is a recursiveoneOf+discriminatorunion (a variant refers back to the union). The samerecursive union defined inline in a single file internalizes fine, and a plain
self-recursive object referenced externally is also fine — so the bug is specific to
the discriminator-mapping recursion on externally-internalized schemas: the
pointer-keyed visited-set (
isVisitedSchema) does not stop the recursion once theschemas are being copied in from another file.
Environment
embedded-spec: true+import-mapping(
GenerateInlinedSpec→InternalizeRefs), but the repro below is pure kin-openapi.Minimal reproduction
ext.yaml— a recursive discriminated union:a.yaml— references the union across files:main.go:Run:
go run . a.yamlExpected
InternalizeRefsterminates and internalizesComp/Container(it does exactly thiswhen the union is inlined in a single file).
Actual
Comparison (isolates the trigger)
Node.child -> Node) via external refoneOf+discriminatorunion via external ref (above)Analysis
derefSchemaguards re-entry withif s == nil || doc.isVisitedSchema(s) { return },which terminates the inline case. With an external ref, the discriminator-mapping
branch (
for k := range s.Discriminator.Mapping { ... addSchemaToSpec(s2); derefSchema(s2.Value); s.Discriminator.Mapping[k] = MappingRef(*s2) })appears to descend into freshly-internalized copies that the pointer-keyed visited-set
doesn't recognize, so the recursion never terminates. This may be related to the
transitive-external-internalization behavior added in #618 / PR #655.