From a8d69d90d56d852aa6752527fa25e08e9db3dab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Thu, 17 Sep 2026 23:07:53 +0200 Subject: [PATCH] fix(decode): terminate the schema walk on a self-embedded pointer struct Assisted-by: GLM 5.3 --- CHANGELOG.md | 4 ++++ decode.go | 11 ++++++++++- decode_test.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e620cb..eb86de3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Decoding into a struct that embeds a pointer to itself terminates. The + schema walk recursed through the embedded type forever, so such a + `Unmarshal` call hung the process; the walk now tracks the struct types on + the current path and stops when one repeats. - An array-of-tables header whose path runs through an inline table (`a = {b = {}}` followed by `[[a.b.c]]`) is rejected. The frozen-inline-table check covered `[table]` headers and dotted keys but not the intermediate diff --git a/decode.go b/decode.go index 754f432..be98175 100644 --- a/decode.go +++ b/decode.go @@ -255,8 +255,15 @@ type structSchema struct { func newStructSchema(t reflect.Type) structSchema { s := structSchema{byName: make(map[string]structFieldLoc, t.NumField())} + // A struct may embed a pointer to itself, which is legal Go, so the walk + // tracks the struct types on the current path and stops when one repeats; + // without the guard the recursion never terminates. A self-promoted key + // always loses to the shallower original, so skipping it changes nothing. + visiting := map[reflect.Type]bool{} var walk func(t reflect.Type, prefix []int, depth int) walk = func(t reflect.Type, prefix []int, depth int) { + visiting[t] = true + defer delete(visiting, t) for i := range t.NumField() { f := t.Field(i) if f.PkgPath != "" { // unexported @@ -277,7 +284,9 @@ func newStructSchema(t reflect.Type) structSchema { } switch { case ft.Kind() == reflect.Struct && !isScalarStruct(ft): - walk(ft, path, depth+1) + if !visiting[ft] { + walk(ft, path, depth+1) + } continue case ft.Kind() == reflect.Map && ft.Key().Kind() == reflect.String: s.embedMaps = append(s.embedMaps, path) diff --git a/decode_test.go b/decode_test.go index 3f2925e..b7a2b11 100644 --- a/decode_test.go +++ b/decode_test.go @@ -538,6 +538,34 @@ func TestUnmarshalEmbeddedPointerStruct(t *testing.T) { } } +// A struct embedding a pointer to itself is legal Go; decoding into it must +// terminate. The schema walk used to recurse through the embedded type +// forever. +func TestUnmarshalSelfEmbeddedPointerStructTerminates(t *testing.T) { + type SelfLink struct { + *SelfLink + X int `toml:"x"` + Y string `toml:"y"` + } + var n SelfLink + if err := Unmarshal([]byte("x = 1\ny = \"s\"\n"), &n); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if n.X != 1 || n.Y != "s" { + t.Fatalf("decoded: %+v", n) + } + + // A nil self pointer on the encode side stays skippable, as any nil + // embedded pointer is. + out, err := Marshal(SelfLink{X: 2}) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if want := "x = 2\ny = \"\"\n"; string(out) != want { + t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want) + } +} + type RoundTripExtra map[string]int type RoundTripMapCfg struct {