diff --git a/internal/core/export/export_test.go b/internal/core/export/export_test.go index 5e7071301..379520156 100644 --- a/internal/core/export/export_test.go +++ b/internal/core/export/export_test.go @@ -377,3 +377,24 @@ func TestFromAPI(t *testing.T) { }) } } + +// TestNoPanicCyclicBuiltinValidator is a regression test for a stack overflow +// in vertex() when a BuiltinValidator argument contains a vertex that is +// already on the export stack. For example, exporting +// +// #c: { a?: matchN(1, [#c]) } +// +// caused infinite recursion: vertex(#c) → structComposite → vertex(a) → +// builtinValidator → value([#c]) → vertex(#c) → … +func TestNoPanicCyclicBuiltinValidator(t *testing.T) { + cuetdtest.FullMatrix.Run(t, "", func(t *testing.T, m *cuetdtest.M) { + ctx := m.CueContext() + v := ctx.CompileString(`#c: { a?: matchN(1, [#c]) }`) + if err := v.Err(); err != nil { + t.Fatal(err) + } + r, x := value.ToInternal(v) + // Must complete without stack overflow. + _, _ = export.Def(r, "", x) + }) +} diff --git a/internal/core/export/value.go b/internal/core/export/value.go index 352d5b57a..883d52144 100644 --- a/internal/core/export/value.go +++ b/internal/core/export/value.go @@ -44,6 +44,14 @@ func (e *exporter) bareValue(v adt.Value) ast.Expr { // value with a reference in graph mode. func (e *exporter) vertex(n *adt.Vertex) (result ast.Expr) { + // Guard against infinite recursion when a vertex cycles back to itself + // through BuiltinValidator arguments or other value-level cycles. + for i := range e.stack { + if e.stack[i].node == n { + return ast.NewIdent("_") + } + } + var attrs []*ast.Attribute if e.cfg.ShowAttributes { attrs = ExtractDeclAttrs(n)