From a00ebd7cee76037e5e0c6a43b1771ae78001c781 Mon Sep 17 00:00:00 2001 From: Urvi Date: Fri, 29 May 2026 10:57:41 -0700 Subject: [PATCH] decoder: raise default max depth to 1500 and add DecodeUnlimitedDepth The previous default decoding depth of 250 was too shallow for some legitimately deep XDR. Raise the default to 1500 for untrusted, user-supplied input, and add a DecodeUnlimitedDepth sentinel (math.MaxUint) so callers decoding trusted XDR emitted by stellar-core can disable the limit entirely. Co-Authored-By: Claude Opus 4.8 (1M context) --- xdr3/decode.go | 9 +++++++-- xdr3/decode_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/xdr3/decode.go b/xdr3/decode.go index 1863440..11a0ceb 100644 --- a/xdr3/decode.go +++ b/xdr3/decode.go @@ -32,8 +32,12 @@ const maxInt32 = math.MaxInt32 var errMaxSlice = "data exceeds max slice limit" var errIODecode = "%s while decoding %d bytes" -// DecodeDefaultMaxDepth is the default maximum decoding depth -const DecodeDefaultMaxDepth = 250 +// DecodeDefaultMaxDepth is the default maximum decoding depth. +const DecodeDefaultMaxDepth = 1500 + +// DecodeUnlimitedDepth disables the maximum decoding depth limit. Only use it +// for trusted input. +const DecodeUnlimitedDepth = uint(math.MaxUint) // MaxPrealloc is the maximum number of elements pre-allocated when decoding // variable-length arrays. Arrays larger than this are grown incrementally via @@ -45,6 +49,7 @@ type DecodeOptions struct { // MaxDepth is the maximum decoding depth (i.e. maximum nesting of data structures). // It prevents infinite recursions in cyclic datastructures and determines the maximum callstack growth. // If set to 0, DecodeDefaultMaxDepth will be used. + // Set it to DecodeUnlimitedDepth to disable the limit. MaxDepth uint // MaxInputLen sets the maximum input size. It is used by the decoder to sanity-check diff --git a/xdr3/decode_test.go b/xdr3/decode_test.go index 747ebf9..a39f85f 100644 --- a/xdr3/decode_test.go +++ b/xdr3/decode_test.go @@ -1157,6 +1157,41 @@ func TestDecodeMaxDepth(t *testing.T) { assertError(t, "", err, &UnmarshalError{ErrorCode: ErrMaxDecodingDepth}) } +// linkedNode builds arbitrarily deep nesting for depth-limit tests. +type linkedNode struct { + Next *linkedNode +} + +func buildLinkedChain(depth int) *linkedNode { + var head *linkedNode + for i := 0; i < depth; i++ { + head = &linkedNode{Next: head} + } + return head +} + +func TestDecodeUnlimitedDepth(t *testing.T) { + depth := DecodeDefaultMaxDepth + 100 + var buf bytes.Buffer + if _, err := Marshal(&buf, buildLinkedChain(depth)); err != nil { + t.Fatalf("unexpected marshal error: %v", err) + } + + // Default limit rejects the deeply nested input. + bufCopy := buf + var s linkedNode + _, err := NewDecoder(&bufCopy).Decode(&s) + assertError(t, "", err, &UnmarshalError{ErrorCode: ErrMaxDecodingDepth}) + + // DecodeUnlimitedDepth decodes it. + bufCopy = buf + var s2 linkedNode + _, err = NewDecoderWithOptions(&bufCopy, DecodeOptions{MaxDepth: DecodeUnlimitedDepth}).Decode(&s2) + if err != nil { + t.Fatalf("unexpected error decoding with unlimited depth: %v", err) + } +} + func TestDecodeMaxAllocationCheck_ImplicitLenReader(t *testing.T) { var buf bytes.Buffer _, err := Marshal(&buf, "thisstringis23charslong")