Skip to content

InternalizeRefs stack-overflows on a recursive discriminated union reached through an external $ref #1205

Description

@lytkin84

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
    (GenerateInlinedSpecInternalizeRefs), 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions