From 98955a1b4857467734f7f4452e925255565bc75f Mon Sep 17 00:00:00 2001 From: eyusemiguzov Date: Wed, 12 Aug 2026 18:02:53 +0700 Subject: [PATCH] fix: struct held in the interface word dereferences its pointer field twice A struct whose only field is a pointer is carried inside the interface word, so the encoder receives the pointer value where it would otherwise receive the address of the struct. compileStruct copies the parent's indirect property onto the child, so the child is compiled as non-indirect even though it is reached by its own address, and the surviving PtrCode wrapper makes the child head a StructPtrHead whose prologue follows the pointer a second time. Depending on which child fields are set the result is a dropped object (#503) or an unrecoverable fatal error while the VM walks encoded bytes as a pointer. When the root struct is neither indirect nor reached by a pointer and the field is a single pointer to a struct, drop the redundant PtrCode, clear isNextOpPtrType and mark the child indirect. Types reachable from themselves are excluded: the recursive re-entry reuses the opcode block of the root, where the layout is the addressed one. Fixes #503 --- encode_test.go | 90 ++++++++++++++++++++++++++++++++++++ internal/encoder/compiler.go | 53 +++++++++++++++++++-- 2 files changed, 140 insertions(+), 3 deletions(-) diff --git a/encode_test.go b/encode_test.go index e71a9992..9e5b713f 100644 --- a/encode_test.go +++ b/encode_test.go @@ -2729,3 +2729,93 @@ func TestIssue459(t *testing.T) { assertErr(t, err) assertEq(t, "unexpected result", "{}", string(b)) } + +func TestIssue503(t *testing.T) { + type child struct { + Flag *bool `json:"flag,omitempty"` + Label *string `json:"label,omitempty"` + } + // a struct whose only field is a pointer travels inside the interface word + type root struct { + Child *child `json:"child,omitempty"` + } + type rootPlain struct { + Child *child `json:"child"` + } + type rootWide struct { + Child *child `json:"child,omitempty"` + Note string `json:"note"` + } + type rootNested struct { + Mid *root `json:"mid,omitempty"` + } + type zeroInt struct { + V int `json:"v"` + } + type zeroBool struct { + V bool `json:"v"` + } + type zeroString struct { + V string `json:"v"` + } + type zeroIntRoot struct { + C *zeroInt `json:"c,omitempty"` + } + type zeroBoolRoot struct { + C *zeroBool `json:"c,omitempty"` + } + type zeroStringRoot struct { + C *zeroString `json:"c,omitempty"` + } + + flag := true + label := "x" + + for _, v := range []interface{}{ + root{}, + root{Child: &child{}}, + root{Child: &child{Flag: &flag}}, + root{Child: &child{Label: &label}}, + root{Child: &child{Flag: &flag, Label: &label}}, + rootPlain{}, + rootPlain{Child: &child{}}, + rootPlain{Child: &child{Label: &label}}, + rootWide{Child: &child{Label: &label}, Note: "n"}, + rootNested{Mid: &root{Child: &child{Label: &label}}}, + zeroIntRoot{C: &zeroInt{V: 0}}, + zeroIntRoot{C: &zeroInt{V: 1}}, + zeroBoolRoot{C: &zeroBool{V: false}}, + zeroBoolRoot{C: &zeroBool{V: true}}, + zeroStringRoot{C: &zeroString{V: ""}}, + zeroStringRoot{C: &zeroString{V: "a"}}, + + // the same type reached by an address instead of the interface word + &root{Child: &child{Label: &label}}, + []root{{Child: &child{Label: &label}}, {}}, + map[string]root{"k": {Child: &child{Label: &label}}}, + struct { + Any interface{} `json:"any"` + }{Any: root{Child: &child{Label: &label}}}, + } { + expected, err := stdjson.Marshal(v) + assertErr(t, err) + + got, err := json.Marshal(v) + assertErr(t, err) + assertEq(t, "unexpected result", string(expected), string(got)) + + expectedIndent, err := stdjson.MarshalIndent(v, "", " ") + assertErr(t, err) + + gotIndent, err := json.MarshalIndent(v, "", " ") + assertErr(t, err) + assertEq(t, "unexpected indented result", string(expectedIndent), string(gotIndent)) + + var buf bytes.Buffer + assertErr(t, json.NewEncoder(&buf).Encode(v)) + assertEq(t, "unexpected streamed result", string(expected)+"\n", buf.String()) + + _, err = json.MarshalWithOption(v, json.Colorize(json.DefaultColorScheme)) + assertErr(t, err) + } +} diff --git a/internal/encoder/compiler.go b/internal/encoder/compiler.go index dca68061..d9c3de24 100644 --- a/internal/encoder/compiler.go +++ b/internal/encoder/compiler.go @@ -206,7 +206,7 @@ func (c *Compiler) typeToCode(typ *runtime.Type) (Code, error) { } return c.mapCode(typ) case reflect.Struct: - return c.structCode(typ, isPtr) + return c.structCode(typ, isPtr, true) case reflect.Int: return c.intCode(typ, isPtr) case reflect.Int8: @@ -269,7 +269,7 @@ func (c *Compiler) typeToCodeWithPtr(typ *runtime.Type, isPtr bool) (Code, error case reflect.Map: return c.mapCode(typ) case reflect.Struct: - return c.structCode(typ, isPtr) + return c.structCode(typ, isPtr, false) case reflect.Interface: return c.interfaceCode(typ, false) case reflect.Int: @@ -592,7 +592,8 @@ func (c *Compiler) mapValueCode(typ *runtime.Type) (Code, error) { } } -func (c *Compiler) structCode(typ *runtime.Type, isPtr bool) (*StructCode, error) { +// isRoot marks the struct the compilation started from: only it can travel inside the interface word +func (c *Compiler) structCode(typ *runtime.Type, isPtr, isRoot bool) (*StructCode, error) { typeptr := uintptr(unsafe.Pointer(typ)) if code, exists := c.structTypeToCode[typeptr]; exists { derefCode := *code @@ -630,6 +631,11 @@ func (c *Compiler) structCode(typ *runtime.Type, isPtr bool) (*StructCode, error if indirect { // if parent is indirect type, set child indirect property to true structCode.isIndirect = true + } else if isRoot && !isPtr && isSinglePtrStructField(field, structCode) { + // the interface word already holds the field pointer, nothing may follow it again + field.value = structCode + field.isNextOpPtrType = false + structCode.isIndirect = true } else { // if parent is not indirect type, set child indirect property to false. // but if parent's indirect is false and isPtr is true, then indirect must be true. @@ -650,6 +656,47 @@ func (c *Compiler) structCode(typ *runtime.Type, isPtr bool) (*StructCode, error return code, nil } +// isSinglePtrStructField reports whether the field is a single pointer to structCode. a self +// reachable type is excluded: its recursive re-entry reuses the root opcodes, addressed layout and all +func isSinglePtrStructField(field *StructFieldCode, structCode *StructCode) bool { + ptrCode, ok := field.value.(*PtrCode) + if !ok || ptrCode.ptrNum != 1 { + return false + } + + value, ok := ptrCode.value.(*StructCode) + + return ok && value == structCode && !hasRecursiveCode(structCode, map[*StructCode]struct{}{}) +} + +func hasRecursiveCode(code Code, seen map[*StructCode]struct{}) bool { + switch c := code.(type) { + case *StructCode: + if c.isRecursive { + return true + } + if _, ok := seen[c]; ok { + return false + } + seen[c] = struct{}{} + for _, field := range c.fields { + if hasRecursiveCode(field.value, seen) { + return true + } + } + case *PtrCode: + return hasRecursiveCode(c.value, seen) + case *SliceCode: + return hasRecursiveCode(c.value, seen) + case *ArrayCode: + return hasRecursiveCode(c.value, seen) + case *MapCode: + return hasRecursiveCode(c.key, seen) || hasRecursiveCode(c.value, seen) + } + + return false +} + func toElemType(t *runtime.Type) *runtime.Type { for t.Kind() == reflect.Ptr { t = t.Elem()