Skip to content

openapi: a $ref naming its own location with a trailing empty segment hangs the compiler #238

Description

@OmarAlJarrah

A path item whose $ref names its own location followed by an empty pointer segment deadlocks Compile. It never returns and no diagnostic is emitted.

Reproducer

A valid OpenAPI 3.1.0 document — real version, real info, one path item — whose only defect is the trailing / on the last pointer:

openapi: 3.1.0
info: {title: t, version: '1'}
paths:
  /a:
    $ref: '#/components/pathItems/A'
components:
  pathItems:
    A:
      $ref: '#/components/pathItems/A/'

openapi.New().Compile(ctx, ...) on this source does not return. The header is spelled out because the shape is easy to dismiss as garbage input; it is not, and the same hang occurs with the header stripped down.

Where it blocks

The same non-reentrant RWMutex as the reference-through-itself class already guarded in #229: Reference.resolve holds the write lock across its pointer walk, and the walk read-locks every reference it passes through, so a pointer that re-enters the reference currently resolving blocks forever (speakeasy-api/openapi v1.24.0, openapi/reference.go GetObject at :293).

sync.(*RWMutex).RLock(...)
github.com/speakeasy-api/openapi/openapi.(*Reference[...]).GetObject(...)
github.com/speakeasy-api/openapi/jsonpointer.getNavigableNoderTarget(...)

Why the existing guard misses it

refScan.outsideCycle is meant to refuse exactly this (chainReenters, "reference resolves through itself"), and it catches the same document when the self-reference is spelled without the trailing separator. The trailing empty segment is what escapes it: nodeview.PointerPath returns an incomplete walk for #/components/pathItems/A/ — there is no "" key to descend into — and the escape needs tracing from there rather than assuming the incomplete-walk branch is the whole of it.

Impact

morphic compile hangs with no timeout and no diagnostic on a spec a user can plausibly write, and there is no way to interrupt it short of killing the process. It also makes the test suite unrunnable once the input reaches the fuzz corpus: go test ./... panics at the 10-minute timeout, and because a hung seed is written back into testdata/fuzz/FuzzCycleDetector/, every subsequent run replays it.

Notes

Found by FuzzCycleDetector while running the gate; reproduced against c317d6e on main. The generated seed is not committed — the reproducer above stands on its own.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions