Skip to content

joiner: semantic deduplication panics on a schema with an empty property value #417

Description

@erraggy

Summary

oastools join --semantic-dedup panics on a valid document that leaves a property empty. compareDeep dereferences both operands with no nil guard, and a schema map can legitimately hold a nil value for a present key.

Reproduction

Two documents, each with a schema whose property has no value. This is legal YAML and parses without error:

openapi: 3.1.0
info: {title: API a, version: "1.0.0"}
paths: {}
components:
  schemas:
    Thinga:
      type: object
      properties:
        name:
$ oastools join --semantic-dedup -o out.yaml crash-a.yaml crash-b.yaml
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x20]

github.com/erraggy/oastools/joiner.compareDocFields(0x0, 0x0, ...)
	joiner/equivalence.go:423
github.com/erraggy/oastools/joiner.compareCommonFields(0x0, 0x0, ...)
	joiner/equivalence.go:671
github.com/erraggy/oastools/joiner.compareDeep(0x0, 0x0, ...)
	joiner/equivalence.go:754

properties: {name:} parses to Properties["name"] == nil, which is the input the comparison cannot take.

Why deduplication reaches it

Both halves have to line up, which is why this is not more common:

  1. The two schemas must hash alike, or they never share a bucket and are never compared. hashSchema writes "nil" for a nil schema, so a nil property and a populated one hash differently — that pair is safe.
  2. Two schemas that are identical, both carrying the same nil value, do hash alike. They reach verifyEquivalence, and comparing them panics.

So the trigger is the ordinary one: the same schema shape appearing in two documents being joined.

joiner.CompareSchemas is also public API, and panics there for any nil-versus-present pair, without needing the hashes to agree.

Affected sites

Verified by comparison against a populated schema:

Site Result
Properties panics
DependentSchemas panics
AllOf / AnyOf / OneOf elements panics
PatternProperties, $defs guarded in #416

CompareSchemasWithOptions nil-guards its two arguments at the top, but compareDeep recurses without re-guarding, so every nested walk that can produce a nil is exposed.

Suggested fix

Guard where a nil can enter the recursion rather than at each call site, so a new nested walk cannot reintroduce it. compareDeep re-checking its operands the way CompareSchemasWithOptions already does would cover every site at once, including the two #416 patched individually, and would let those revert to the plain call.

Worth pairing with a guard in internal/driftguard that populates each nested-schema field with a nil and requires the comparison to survive: this is a class, and the field-by-field guards there caught the equivalent class for missing fields.

Not affected

  • parser.Schema.Equals nil-guards correctly at every level
  • The structural hash handles nil (hashSchema writes "nil")
  • Non-dedup joins, since nothing compares schemas

Context

Found while auditing #416 for further instances of the nil hazard CodeRabbit flagged there. That review caught it in compareSchemaMaps, the helper that PR added; these three sites predate it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingoas2.0Issues related to Swagger (OpenAPI Specification 2.0)oas3.xIssues related to OpenAPI Specification versions 3+

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions