From d46b35fcdd7cdbcf3b6b1bb4b6fb5a4aef54bff2 Mon Sep 17 00:00:00 2001 From: Martin Holovsky <2320048+martinholovsky@users.noreply.github.com> Date: Sat, 18 Jul 2026 00:32:24 +0200 Subject: [PATCH] encode: bound nesting depth to prevent stack overflow on deep input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The decoder gained a maxNestingDepth guard to fix a stack-exhaustion issue (GHSA-5wfc-hjrc-gq87 / GO-2026-5157), but the encoder was left unguarded. Its only depth-related logic is cycle detection, which triggers solely on a repeated pointer; a deeply nested, non-cyclic value (distinct pointer at every level) slips past it, so hjson.Marshal recurses until the goroutine stack overflows — a fatal, unrecoverable crash (DoS). Reuse the existing pDepth counter to also enforce maxNestingDepth on the encode side, returning the same "Exceeded max depth" error the decoder already returns. No behavior change for normal input (nothing legitimately nests 10000 deep). Reproduce (pre-fix): var v any = "x"; for i := 0; i < 3_000_000; i++ { v = []any{v} }; hjson.Marshal(v) -> fatal error: stack overflow. --- encode.go | 7 +++++++ encode_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) 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) + } +}