diff --git a/encode.go b/encode.go index 1245cdd..8595e4a 100644 --- a/encode.go +++ b/encode.go @@ -314,6 +314,13 @@ func (e *hjsonEncoder) str( e.parents[p] = struct{}{} defer delete(e.parents, p) } + // Bound the nesting depth so deeply-nested (non-cyclic) input cannot + // exhaust the stack. The cycle check above only stops repeated pointers; + // distinct pointers at every level slip past it. This mirrors the + // decoder's maxNestingDepth guard. + if e.pDepth > maxNestingDepth { + return fmt.Errorf("Exceeded max depth (%d)", maxNestingDepth) + } defer func() { e.pDepth-- }() } diff --git a/encode_test.go b/encode_test.go index 9e1480f..08e1fc9 100644 --- a/encode_test.go +++ b/encode_test.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "reflect" + "strings" "testing" "time" ) @@ -875,3 +876,28 @@ func TestStructComment(t *testing.T) { t.Errorf("Expected:\n%s\nGot:\n%s\n\n", expected, string(h)) } } + +func TestEncodeMaxDepth(t *testing.T) { + // Deeply nested, non-cyclic input must be rejected with an error rather than + // recursing until the goroutine stack overflows (a fatal, unrecoverable + // crash). The cycle check only stops repeated pointers; distinct pointers at + // every level slip past it, so a separate depth bound is required. + var deep interface{} = "leaf" + for i := 0; i < maxNestingDepth+100; i++ { + deep = []interface{}{deep} + } + if _, err := Marshal(deep); err == nil { + t.Error("expected an error for input nested past maxNestingDepth, got nil") + } else if !strings.Contains(err.Error(), "Exceeded max depth") { + t.Errorf("expected a max-depth error, got: %v", err) + } + + // Modest nesting must still encode without error. + var shallow interface{} = "leaf" + for i := 0; i < 100; i++ { + shallow = []interface{}{shallow} + } + if _, err := Marshal(shallow); err != nil { + t.Errorf("modest nesting should encode without error, got: %v", err) + } +}