From 7667db6a8334422489692ad9ffe87802629f3400 Mon Sep 17 00:00:00 2001 From: "carpentry-heartbeat[bot]" Date: Fri, 12 Jun 2026 07:21:26 +0200 Subject: [PATCH 1/3] Add recursion depth limit to JSON parser Prevent stack overflow on deeply nested input (e.g. thousands of nested arrays or objects) by tracking nesting depth during parsing. A new global json-max-depth (default 128) caps how deep containers can nest. When exceeded, parsing returns a DepthLimitExceeded error instead of crashing. Depth is counted only for containers (arrays and objects), so a flat array of any length is unaffected. The limit is reset on each call to JSON.parse. --- json.carp | 33 +++++++++++++++++++++++++++++---- test/json.carp | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/json.carp b/json.carp index 8dbe30c..add9d3f 100644 --- a/json.carp +++ b/json.carp @@ -68,7 +68,8 @@ where the error was detected.") (ExpectedColon []) (ExpectedCommaOrEnd []) (ExpectedStringKey []) - (TrailingContent [])) + (TrailingContent []) + (DepthLimitExceeded [])) (doc ParseError "wraps a `ParseErrorKind` together with the byte position in the input where the error was detected.") @@ -93,6 +94,8 @@ neither of which is representable in JSON.") (def json-idx 0) (def json-len 0) +(def json-depth 0) +(def json-max-depth 128) (def *parse-error* (the (Maybe JSON.ParseError) (Maybe.Nothing))) (def *serialize-error* (the (Maybe JSON.SerializeError) (Maybe.Nothing))) (def *json-indent-width* 0) @@ -389,8 +392,28 @@ neither of which is representable in JSON.") (= c \t) (json-parse-true s) (= c \f) (json-parse-false s) (= c \n) (json-parse-null s) - (= c \[) (json-parse-array s) - (= c \{) (json-parse-object s) + (= c \[) + (do + (set! json-depth (Int.inc json-depth)) + (let-do [result + (if (> json-depth json-max-depth) + (do + (json-set-err! (JSON.ParseErrorKind.DepthLimitExceeded)) + (JSON.Null)) + (json-parse-array s))] + (set! json-depth (Int.dec json-depth)) + result)) + (= c \{) + (do + (set! json-depth (Int.inc json-depth)) + (let-do [result + (if (> json-depth json-max-depth) + (do + (json-set-err! (JSON.ParseErrorKind.DepthLimitExceeded)) + (JSON.Null)) + (json-parse-object s))] + (set! json-depth (Int.dec json-depth)) + result)) (or (= c \-) (Char.num? c)) (json-parse-number s) (do (json-set-err! (JSON.ParseErrorKind.UnexpectedChar c)) (JSON.Null))))))) @@ -408,6 +431,7 @@ was detected. Use `JSON.parse-error-str` to format the error for display.") (do (set! json-idx 0) (set! json-len (String.length s)) + (set! json-depth 0) (set! *parse-error* (the (Maybe JSON.ParseError) (Maybe.Nothing))) (let-do [j (json-parse-value s)] (unless-do (json-failed?) @@ -446,7 +470,8 @@ message, without position information.") (JSON.ParseErrorKind.ExpectedCommaOrEnd) @"expected ',' or closing bracket" (JSON.ParseErrorKind.ExpectedStringKey) @"expected string key" - (JSON.ParseErrorKind.TrailingContent) @"unexpected trailing content")) + (JSON.ParseErrorKind.TrailingContent) @"unexpected trailing content" + (JSON.ParseErrorKind.DepthLimitExceeded) @"nesting depth limit exceeded")) (doc parse-error-str "formats a `ParseError` as a human-readable message including the byte position where the error was detected.") diff --git a/test/json.carp b/test/json.carp index 7ef9bcb..2e806c0 100644 --- a/test/json.carp +++ b/test/json.carp @@ -315,6 +315,53 @@ (assert-true test (parse-err? "'single'") "reject single-quoted string") + ; ========================================================================= + ; Depth limit + ; ========================================================================= + + ; 128 nested arrays (at the limit) should succeed + (assert-true test + (let-do [s @""] + (for [i 0 128] (set! s (String.append &s "["))) + (set! s (String.append &s "1")) + (for [i 0 128] (set! s (String.append &s "]"))) + (parse-ok? &s)) + "parse arrays nested to exactly the depth limit") + + ; 129 nested arrays (one over) should fail + (assert-true test + (let-do [s @""] + (for [i 0 129] (set! s (String.append &s "["))) + (set! s (String.append &s "1")) + (for [i 0 129] (set! s (String.append &s "]"))) + (parse-err? &s)) + "reject arrays nested beyond the depth limit") + + ; Verify the error kind is DepthLimitExceeded + (assert-true test + (let-do [s @""] + (for [i 0 129] (set! s (String.append &s "["))) + (set! s (String.append &s "1")) + (for [i 0 129] (set! s (String.append &s "]"))) + (match (err-kind &s) + (Maybe.Just (JSON.ParseErrorKind.DepthLimitExceeded)) true + _ false)) + "excess nesting gives DepthLimitExceeded") + + ; Deeply nested objects should also be rejected + (assert-true test + (let-do [s @""] + (for [i 0 129] (set! s (String.append &s "{\"k\":"))) + (set! s (String.append &s "1")) + (for [i 0 129] (set! s (String.append &s "}"))) + (parse-err? &s)) + "reject objects nested beyond the depth limit") + + ; Flat input is unaffected + (assert-true test + (parse-ok? "[1,2,3,4,5,6,7,8,9,10]") + "flat array is unaffected by depth limit") + ; ========================================================================= ; Serializer (str) ; ========================================================================= From 1b20023b0efad2f03a1a36ffc964b34ef9417b84 Mon Sep 17 00:00:00 2001 From: "carpentry-heartbeat[bot]" Date: Fri, 12 Jun 2026 14:20:44 +0200 Subject: [PATCH 2/3] fix nested-if-chain lint warnings in surrogate pair handling Convert nested if chains to cond expressions in json-parse-string (lines 200, 202, 212) to resolve angler nested-if-chain warnings. --- json.carp | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/json.carp b/json.carp index add9d3f..e786e47 100644 --- a/json.carp +++ b/json.carp @@ -197,9 +197,9 @@ neither of which is representable in JSON.") (do (set! json-idx (Int.inc json-idx)) (let [cp (json-read-hex4 s)] - (if (json-failed?) - (break) - (if (and (>= cp 55296) (<= cp 56319)) + (cond + (json-failed?) (break) + (and (>= cp 55296) (<= cp 56319)) (if (and (< (Int.inc json-idx) json-len) (and (= (String.char-at s json-idx) \\) @@ -209,27 +209,27 @@ neither of which is representable in JSON.") (do (set! json-idx (+ json-idx 2)) (let [low (json-read-hex4 s)] - (if (json-failed?) - (break) - (if (and (>= low 56320) (<= low 57343)) + (cond + (json-failed?) (break) + (and (>= low 56320) (<= low 57343)) (let [full-cp (+ (+ (* (- cp 55296) 1024) (- low 56320)) 65536)] (json-push-codepoint! &bytes full-cp)) - (do - (json-set-err! - (JSON.ParseErrorKind.InvalidLowSurrogate)) - (break)))))) + (do + (json-set-err! + (JSON.ParseErrorKind.InvalidLowSurrogate)) + (break))))) (do (json-set-err! (JSON.ParseErrorKind.LoneHighSurrogate)) (break))) - (if (and (>= cp 56320) (<= cp 57343)) - (do - (json-set-err! (JSON.ParseErrorKind.LoneLowSurrogate)) - (break)) - (json-push-codepoint! &bytes cp)))))) + (and (>= cp 56320) (<= cp 57343)) + (do + (json-set-err! (JSON.ParseErrorKind.LoneLowSurrogate)) + (break)) + (json-push-codepoint! &bytes cp)))) (do (json-set-err! (JSON.ParseErrorKind.InvalidEscape e)) (break)))))) From 1b3a488c6ce1d64627185a49e517c09450903034 Mon Sep 17 00:00:00 2001 From: carpentry-agent Date: Fri, 12 Jun 2026 22:00:35 +0200 Subject: [PATCH 3/3] fix carp-fmt formatting in depth-limit let-do bindings --- json.carp | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/json.carp b/json.carp index e786e47..ddd9714 100644 --- a/json.carp +++ b/json.carp @@ -395,23 +395,21 @@ neither of which is representable in JSON.") (= c \[) (do (set! json-depth (Int.inc json-depth)) - (let-do [result - (if (> json-depth json-max-depth) - (do - (json-set-err! (JSON.ParseErrorKind.DepthLimitExceeded)) - (JSON.Null)) - (json-parse-array s))] + (let-do [result (if (> json-depth json-max-depth) + (do + (json-set-err! (JSON.ParseErrorKind.DepthLimitExceeded)) + (JSON.Null)) + (json-parse-array s))] (set! json-depth (Int.dec json-depth)) result)) (= c \{) (do (set! json-depth (Int.inc json-depth)) - (let-do [result - (if (> json-depth json-max-depth) - (do - (json-set-err! (JSON.ParseErrorKind.DepthLimitExceeded)) - (JSON.Null)) - (json-parse-object s))] + (let-do [result (if (> json-depth json-max-depth) + (do + (json-set-err! (JSON.ParseErrorKind.DepthLimitExceeded)) + (JSON.Null)) + (json-parse-object s))] (set! json-depth (Int.dec json-depth)) result)) (or (= c \-) (Char.num? c)) (json-parse-number s)