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:
- 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.
- 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.
Summary
oastools join --semantic-deduppanics on a valid document that leaves a property empty.compareDeepdereferences 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:
properties: {name:}parses toProperties["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:
hashSchemawrites"nil"for a nil schema, so a nil property and a populated one hash differently — that pair is safe.verifyEquivalence, and comparing them panics.So the trigger is the ordinary one: the same schema shape appearing in two documents being joined.
joiner.CompareSchemasis 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:
PropertiesDependentSchemasAllOf/AnyOf/OneOfelementsPatternProperties,$defsCompareSchemasWithOptionsnil-guards its two arguments at the top, butcompareDeeprecurses 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.
compareDeepre-checking its operands the wayCompareSchemasWithOptionsalready 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/driftguardthat 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.Equalsnil-guards correctly at every levelhashSchemawrites"nil")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.