Skip to content

Stack overflow when custom UnmarshalYAML wraps unmarshal callback error #345

@markphelps

Description

@markphelps

LoadErrors.Is() causes infinite recursion (fatal stack overflow) when a custom UnmarshalYAML implementation wraps the error returned by the unmarshal callback with %w.

Minimal reproduction

package main

import (
    "errors"
    "fmt"
    "os"
    "time"

    "go.yaml.in/yaml/v4"
)

type Inner struct {
    Value string `yaml:"value"`
}

type Wrapper struct {
    Item Inner `yaml:"item"`
}

func (w *Wrapper) UnmarshalYAML(unmarshal func(any) error) error {
    var single Inner
    if err := unmarshal(&single); err != nil {
        // Standard Go idiom — wrapping with %w
        return fmt.Errorf("wrapper failed: %w", err)
    }
    return nil
}

type Root struct {
    Items []Wrapper `yaml:"items"`
}

func main() {
    done := make(chan struct{})
    go func() {
        data := []byte("items:\n  - not-an-object\n")
        var r Root
        err := yaml.Unmarshal(data, &r)
        if err != nil {
            // This triggers fatal error: stack overflow
            _ = errors.Is(err, errors.New("test"))
        }
        close(done)
    }()

    select {
    case <-done:
        fmt.Println("Completed")
    case <-time.After(3 * time.Second):
        fmt.Println("TIMEOUT: stack overflow")
        os.Exit(1)
    }
}

Root cause

yaml.Unmarshal reuses a single LoadErrors object. When the custom UnmarshalYAML wraps the callback's error with %w, and yaml.Unmarshal adds that wrapped error back to the same LoadErrors, it creates a cycle:

LoadErrors → ConstructError → fmt.wrapError → LoadErrors

LoadErrors.Is() calls errors.Is(err, target) on each ConstructError, which unwraps back to the same LoadErrors, causing infinite recursion.

Environment

  • go.yaml.in/yaml/v4 v4.0.0-rc.4
  • Go 1.26

Workaround

Custom UnmarshalYAML implementations must use %v instead of %w when wrapping errors from the unmarshal callback, but this is non-obvious and breaks standard Go error wrapping conventions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions