This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git

commit 25a0ec407572329e221930a1b5bbb8d44a40ee26
Author: James Bognar <[email protected]>
AuthorDate: Sun Aug 16 21:22:14 2026 -0400

    READY-389/390: Shared parse-depth budget + narrowed OOM catch in 
ParserSession
    
    Adds a shared, configurable nesting-depth budget to ParserSession 
(READY-389),
    enforced by the CBOR and Protobuf parsers alongside the existing MsgPack 
depth
    guard, plus new CborParser_ParseDepth_Test / ProtobufParser_ParseDepth_Test
    covering it. Narrows ParserSession's OutOfMemoryError catch to wrap only the
    local allocation call it exists to guard (READY-390), instead of a broad
    try block that could mask unrelated OOMs. Combined into one commit because
    both items touch ParserSession.java / ParserSession_Test.java.
---
 .../juneau/marshall/cbor/CborParserSession.java    |  15 ++-
 .../marshall/msgpack/MsgPackParserSession.java     |  26 +----
 .../juneau/marshall/parser/ParserSession.java      | 105 +++++++++++++++++++--
 .../marshall/protobuf/ProtobufParserSession.java   |  16 +++-
 .../marshall/cbor/CborParser_ParseDepth_Test.java  |  73 ++++++++++++++
 .../juneau/marshall/parser/ParserSession_Test.java | 102 ++++++++++++++++++--
 .../protobuf/ProtobufParser_ParseDepth_Test.java   |  86 +++++++++++++++++
 7 files changed, 381 insertions(+), 42 deletions(-)

diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
index 8e8e76a30d..e10bfaa8a9 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
@@ -151,6 +151,19 @@ public class CborParserSession extends 
InputStreamParserSession implements Token
                }
        }
 
+       /*
+        * Workhorse entry point.  Wraps readAnything0 with the shared 
ParserSession recursion-depth guard so that
+        * an adversarial deeply-nested document fails with a ParseException 
instead of a StackOverflowError.
+        */
+       <T> T readAnything(ClassMeta<?> eType, CborInputStream is, Object 
outer, BeanPropertyMeta pMeta) throws IOException, ParseException, 
ExecutableException {
+               enterParseDepth();
+               try {
+                       return readAnything0(eType, is, outer, pMeta);
+               } finally {
+                       exitParseDepth();
+               }
+       }
+
        /*
         * Workhorse method.
         */
@@ -158,7 +171,7 @@ public class CborParserSession extends 
InputStreamParserSession implements Token
                "java:S3776", // Cognitive complexity acceptable for this 
specific logic
                "java:S6541"  // Single-threaded session contexts do not 
require synchronization
        })
-       <T> T readAnything(ClassMeta<?> eType, CborInputStream is, Object 
outer, BeanPropertyMeta pMeta) throws IOException, ParseException, 
ExecutableException {
+       private <T> T readAnything0(ClassMeta<?> eType, CborInputStream is, 
Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, 
ExecutableException {
 
                if (eType == null)
                        eType = object();
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
index e52aa04785..d672341d66 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
@@ -92,22 +92,9 @@ public class MsgPackParserSession extends 
InputStreamParserSession implements To
                return new Builder(assertArgNotNull(ARG_ctx, ctx));
        }
 
-       /**
-        * Maximum databind parse-recursion depth.
-        *
-        * <p>
-        * The databind {@link #readAnything(ClassMeta, MsgPackInputStream, 
Object, BeanPropertyMeta) readAnything}
-        * path recurses once per nesting level; this bound makes an 
adversarial deeply-nested document fail with a
-        * {@link ParseException} instead of a {@link StackOverflowError}.  The 
token-cursor path is iterative and
-        * unaffected.
-        */
-       private static final int MAX_PARSE_DEPTH = 1000;
-
        private final boolean nativeMode;
        private final int maxLength;
 
-       private int parseDepth;
-
        /**
         * Constructor.
         *
@@ -163,19 +150,16 @@ public class MsgPackParserSession extends 
InputStreamParserSession implements To
        }
 
        /*
-        * Workhorse entry point.  Wraps {@link #readAnything0} with a 
recursion-depth guard so that an
-        * adversarial deeply-nested document fails with a {@link 
ParseException} instead of a
-        * {@link StackOverflowError}.
+        * Workhorse entry point.  Wraps readAnything0 with the shared 
ParserSession recursion-depth guard so that
+        * an adversarial deeply-nested document fails with a ParseException 
instead of a StackOverflowError.  The
+        * token-cursor path is iterative and unaffected.
         */
        <T> T readAnything(ClassMeta<?> eType, MsgPackInputStream is, Object 
outer, BeanPropertyMeta pMeta) throws IOException, ParseException, 
ExecutableException {
-               if (++parseDepth > MAX_PARSE_DEPTH) {
-                       parseDepth--;
-                       throw new ParseException(this, "Maximum parse depth 
exceeded (%s).", MAX_PARSE_DEPTH);
-               }
+               enterParseDepth();
                try {
                        return readAnything0(eType, is, outer, pMeta);
                } finally {
-                       parseDepth--;
+                       exitParseDepth();
                }
        }
 
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parser/ParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parser/ParserSession.java
index 53c38e7213..8d6aa6343b 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parser/ParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parser/ParserSession.java
@@ -28,6 +28,7 @@ import java.time.*;
 import java.time.Duration;
 import java.time.temporal.*;
 import java.util.*;
+import java.util.function.*;
 
 import javax.xml.datatype.*;
 
@@ -70,11 +71,18 @@ public class ParserSession extends MarshallingSession {
        private static final String PROP_schema = "schema";
        private static final String PROP_trimStrings = "trimStrings";
        private static final String PROP_nulls = "nulls";
+       private static final String PROP_maxParseDepth = "maxParseDepth";
        private static final String PROP_ParserSession_javaMethod = 
"ParserSession.javaMethod";
        private static final String PROP_ParserSession_outer = 
"ParserSession.outer";
        private static final String PROP_ParserSession_schema = 
"ParserSession.schema";
        private static final String PROP_ParserSession_trimStrings = 
"ParserSession.trimStrings";
        private static final String PROP_ParserSession_nulls = 
"ParserSession.nulls";
+       private static final String PROP_ParserSession_maxParseDepth = 
"ParserSession.maxParseDepth";
+
+       /**
+        * Default value for {@link Builder#maxParseDepth(int)}.
+        */
+       protected static final int DEFAULT_MAX_PARSE_DEPTH = 1000;
 
        // Argument name constants for assertArgNotNull
        private static final String ARG_ctx = "ctx";
@@ -92,6 +100,7 @@ public class ParserSession extends MarshallingSession {
                private Object outer;
                private boolean trimStrings;
                private Nulls nulls;
+               private int maxParseDepth = DEFAULT_MAX_PARSE_DEPTH;
                private Parser ctx;
 
                /**
@@ -140,6 +149,21 @@ public class ParserSession extends MarshallingSession {
                        return self();
                }
 
+               /**
+                * The shared recursive-parse-depth budget honored by binary 
codec sessions (CBOR, Protobuf, MsgPack)
+                * at their recursive databind entry points.
+                *
+                * @param value
+                *      The new property value.
+                *      <br>Defaults to {@value 
ParserSession#DEFAULT_MAX_PARSE_DEPTH}.
+                * @return This object.
+                * @see ParserSession#enterParseDepth()
+                */
+               public SELF maxParseDepth(int value) {
+                       maxParseDepth = value;
+                       return self();
+               }
+
                @Override /* Overridden from Builder */
                public SELF property(String key, Object value) {
                        if (key == null) {
@@ -157,6 +181,8 @@ public class ParserSession extends MarshallingSession {
                                        return trimStrings(cvt(value, 
Boolean.class));
                                case PROP_nulls, PROP_ParserSession_nulls:
                                        return nulls(cvt(value, Nulls.class));
+                               case PROP_maxParseDepth, 
PROP_ParserSession_maxParseDepth:
+                                       return maxParseDepth(cvt(value, 
Integer.class));
                                default:
                                        super.property(key, value);
                                        return self();
@@ -315,6 +341,7 @@ public class ParserSession extends MarshallingSession {
        private final Object outer;
        private final boolean trimStrings;
        private final Nulls nulls;
+       private final int maxParseDepth;
        private final Parser ctx;
        private final ParserListener listener;
        private final Deque<StringBuilder> sbStack;
@@ -323,6 +350,7 @@ public class ParserSession extends MarshallingSession {
        private Object parentBean;
        private Position mark = new Position(-1);
        private ParserPipe pipe;
+       private int parseDepth;
 
        /**
         * Constructor.
@@ -339,6 +367,7 @@ public class ParserSession extends MarshallingSession {
                schema = builder.schema;
                trimStrings = builder.trimStrings;
                nulls = builder.nulls == null ? Nulls.NOT_SET : builder.nulls;
+               maxParseDepth = builder.maxParseDepth;
                listener = BeanInstantiator.createOrNull(ctx.getListener());
                sbStack = new ArrayDeque<>();
        }
@@ -688,8 +717,6 @@ public class ParserSession extends MarshallingSession {
                        throw e;
                } catch (@SuppressWarnings("unused") StackOverflowError e) {
                        throw new ParseException(this, "Depth too deep.  Stack 
overflow occurred.");
-               } catch (@SuppressWarnings("unused") OutOfMemoryError e) {
-                       throw new ParseException(this, "Out of memory occurred. 
 Input too large to parse.");
                } catch (IOException e) {
                        throw new ParseException(this, e, "I/O exception 
occurred.  exception=%s, message=%s.", cns(e), localizedMessage(e));
                } catch (Exception e) {
@@ -725,8 +752,6 @@ public class ParserSession extends MarshallingSession {
                        throw e;
                } catch (@SuppressWarnings("unused") StackOverflowError e) {
                        throw new ParseException(this, "Depth too deep.  Stack 
overflow occurred.");
-               } catch (@SuppressWarnings("unused") OutOfMemoryError e) {
-                       throw new ParseException(this, "Out of memory occurred. 
 Input too large to parse.");
                } catch (IOException e) {
                        throw new ParseException(this, e, "I/O exception 
occurred.  exception=%s, message=%s.", cns(e), localizedMessage(e));
                } catch (Exception e) {
@@ -803,8 +828,6 @@ public class ParserSession extends MarshallingSession {
                        throw e;
                } catch (@SuppressWarnings("unused") StackOverflowError e) {
                        throw new ParseException(this, "Depth too deep.  Stack 
overflow occurred.");
-               } catch (@SuppressWarnings("unused") OutOfMemoryError e) {
-                       throw new ParseException(this, "Out of memory occurred. 
 Input too large to parse.");
                } catch (Exception e) {
                        throw new ParseException(this, e, "Exception occurred.  
exception=%s, message=%s.", cns(e), localizedMessage(e));
                } finally {
@@ -844,8 +867,6 @@ public class ParserSession extends MarshallingSession {
        public final <T> void readToBeanConsumer(Object input, BeanConsumer<T> 
consumer, Class<T> elementType) throws ParseException, IOException {
                try (var p = createPipe(input)) {
                        doReadToBeanConsumer(p, consumer, elementType);
-               } catch (@SuppressWarnings("unused") OutOfMemoryError e) {
-                       throw new ParseException(this, "Out of memory occurred. 
 Input too large to parse.");
                }
        }
 
@@ -1350,6 +1371,71 @@ public class ParserSession extends MarshallingSession {
                return Iso8601Utils.parseTemporal(s, cm, getTemporalFormat(), 
getTimeZone());
        }
 
+       /**
+        * Enters one level of recursive databind parsing, enforcing the shared 
{@link Builder#maxParseDepth(int)}
+        * budget (default {@value #DEFAULT_MAX_PARSE_DEPTH}).
+        *
+        * <p>
+        * Binary codec sessions (CBOR, Protobuf, MsgPack) call this at their 
recursive databind entry point before
+        * descending into a nested array/map/message element, and must call 
{@link #exitParseDepth()} in a matching
+        * <jk>finally</jk> block regardless of outcome.  Exceeding the budget 
throws a {@link ParseException} so an
+        * adversarial deeply-nested document fails cleanly instead of 
exhausting the call stack with a
+        * {@link StackOverflowError}.
+        *
+        * <p>
+        * Intentionally not applied to the JSON/XML/text parsers, whose 
recursive-descent parsers already rely on
+        * {@link StackOverflowError} conversion at the outer parse boundary.
+        *
+        * @throws ParseException If the configured maximum parse depth was 
exceeded.
+        */
+       protected final void enterParseDepth() throws ParseException {
+               if (++parseDepth > maxParseDepth) {
+                       parseDepth--;
+                       throw new ParseException(this, "Maximum parse depth 
exceeded (%s).", maxParseDepth);
+               }
+       }
+
+       /**
+        * Exits one level of recursive databind parsing previously entered via 
{@link #enterParseDepth()}.
+        *
+        * <p>
+        * Must be called exactly once per successful {@link 
#enterParseDepth()} call, in a matching <jk>finally</jk>
+        * block so the depth counter is restored even when the nested parse 
throws.
+        */
+       protected final void exitParseDepth() {
+               parseDepth--;
+       }
+
+       /**
+        * Runs a local, parser-controlled allocation (e.g. pre-sizing an 
array/collection/buffer from a
+        * wire-declared count) and converts an {@link OutOfMemoryError} thrown 
specifically from it into a bounded
+        * {@link ParseException}.
+        *
+        * <p>
+        * Only an {@link OutOfMemoryError} thrown from {@code alloc}'s own 
execution is converted here.  Any other
+        * {@link OutOfMemoryError} &mdash; one raised elsewhere in the call 
stack, signaling a JVM that is
+        * genuinely running out of heap &mdash; is <b>not</b> this method's 
concern and is never observed by it; a
+        * dying JVM is not a client input-validation failure and must not be 
reported as one.  Format-specific
+        * parser sessions should route untrusted-length-driven local 
allocations through this helper instead of
+        * relying on a blanket catch at an outer parse boundary.
+        *
+        * @param <T> The allocated object's type.
+        * @param what A short human-readable description of what's being 
allocated, used in the exception message
+        *      if the allocation fails.
+        *      <br>Must not be <jk>null</jk>.
+        * @param alloc The allocation to attempt.
+        *      <br>Must not be <jk>null</jk>.
+        * @return The allocated object.
+        * @throws ParseException If {@code alloc} itself threw an {@link 
OutOfMemoryError}.
+        */
+       protected final <T> T allocateLocal(String what, Supplier<T> alloc) 
throws ParseException {
+               try {
+                       return alloc.get();
+               } catch (@SuppressWarnings("unused") OutOfMemoryError e) {
+                       throw new ParseException(this, "Out of memory occurred. 
 Input too large to parse.  Allocation: %s", what);
+               }
+       }
+
        /**
         * Marks the current position.
         */
@@ -1409,7 +1495,8 @@ public class ParserSession extends MarshallingSession {
                        .a(PROP_listener, listener)
                        .a(PROP_outer, outer)
                        .a(PROP_trimStrings, trimStrings)
-                       .a(PROP_nulls, nulls);
+                       .a(PROP_nulls, nulls)
+                       .a(PROP_maxParseDepth, maxParseDepth);
        }
 
        /**
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/protobuf/ProtobufParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/protobuf/ProtobufParserSession.java
index 3a1c65ac4f..ff0b3c2495 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/protobuf/ProtobufParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/protobuf/ProtobufParserSession.java
@@ -108,11 +108,25 @@ public class ProtobufParserSession extends 
InputStreamParserSession {
                return (T)readMessage(type, is, getOuter());
        }
 
+       /*
+        * Workhorse entry point.  Wraps readMessage0 with the shared 
ParserSession recursion-depth guard so that
+        * an adversarial deeply-nested message (via nested MESSAGE fields, 
TAGGED_REPEATED elements, or map
+        * entry/value sub-messages) fails with a ParseException instead of a 
StackOverflowError.
+        */
+       private Object readMessage(ClassMeta<?> type, ProtobufReader is, Object 
outer) throws IOException, ParseException, ExecutableException {
+               enterParseDepth();
+               try {
+                       return readMessage0(type, is, outer);
+               } finally {
+                       exitParseDepth();
+               }
+       }
+
        @SuppressWarnings({
                "java:S3776", // Cognitive complexity acceptable for the 
tag-loop dispatch
                "java:S6541"  // Brain method acceptable for the parse workhorse
        })
-       private Object readMessage(ClassMeta<?> type, ProtobufReader is, Object 
outer) throws IOException, ParseException, ExecutableException {
+       private Object readMessage0(ClassMeta<?> type, ProtobufReader is, 
Object outer) throws IOException, ParseException, ExecutableException {
                if (type.isMap())
                        return readMapMessage(type, is);
                var pcm = ctx.getProtobufClassMeta(type);
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborParser_ParseDepth_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborParser_ParseDepth_Test.java
new file mode 100644
index 0000000000..2d725261ee
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborParser_ParseDepth_Test.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.marshall.cbor;
+
+import static org.apache.juneau.commons.utils.StringUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.marshall.parser.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Recursion-depth guard tests for {@link CborParserSession}'s databind parse 
path (READY-389: shared
+ * {@link ParserSession} parse-depth budget applied to CBOR, mirroring 
MsgPack's prior per-codec guard).
+ *
+ * <p>
+ * Before this fix, {@code CborParserSession.readAnything} recursed once per 
nesting level with no depth
+ * cap of its own; an adversarial deeply-nested array/map would only fail once 
the JVM call stack was
+ * actually exhausted ({@link StackOverflowError}), rather than failing 
gracefully via a bounded
+ * {@link ParseException} the way MsgPack already did.
+ */
+class CborParser_ParseDepth_Test extends TestBase {
+
+       @Test void a01_deeplyNestedArraysFailWithParseException() {
+               // 1100 nested definite-length-1 array headers (CBOR major type 
4, additional info 1 -> 0x81) then a
+               // terminal UINT 0 (0x00) -> exceeds the shared ParserSession 
maxParseDepth budget (default 1000).
+               var sb = new StringBuilder();
+               for (var i = 0; i < 1100; i++)
+                       sb.append("81 ");
+               sb.append("00");
+               var input = sb.toString();
+               var e = assertThrows(ParseException.class, () -> 
CborParser.DEFAULT.read(fromSpacedHex(input), Object.class));
+               var msg = String.valueOf(e.getMessage());
+               // Graceful depth-failure ParseException; the soft 
maxParseDepth guard is expected to fire before any
+               // real StackOverflowError, but a constrained CI thread stack 
falling back to the StackOverflowError
+               // wrapper is also an acceptable pass (mirrors the MsgPack 
conformance test's tolerance).
+               assertTrue(
+                       msg.contains("Maximum parse depth exceeded") || 
msg.contains("Depth too deep"),
+                       "Expected a graceful depth-failure ParseException.  
Actual:\n" + msg);
+       }
+
+       @Test void a02_moderateNestingStillParses() throws Exception {
+               // 10 nested arrays then a terminal UINT 5 -> well within the 
depth budget; legitimate shallow
+               // payloads must be unaffected by the new guard.
+               var sb = new StringBuilder();
+               for (var i = 0; i < 10; i++)
+                       sb.append("81 ");
+               sb.append("05");
+               Object o = 
CborParser.DEFAULT.read(fromSpacedHex(sb.toString()), Object.class);
+               for (var i = 0; i < 10; i++) {
+                       var l = (List<?>) o;
+                       assertEquals(1, l.size());
+                       o = l.get(0);
+               }
+               assertEquals(5L, o);
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/parser/ParserSession_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/parser/ParserSession_Test.java
index 07b9e3a7e5..11c820e1d3 100644
--- 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/parser/ParserSession_Test.java
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/parser/ParserSession_Test.java
@@ -818,6 +818,9 @@ class ParserSession_Test extends TestBase {
                ExposingSession(JsonParser p) {
                        super(create(p));
                }
+               ExposingSession(Builder<?> builder) {
+                       super(builder);
+               }
                public boolean exposeIsTrimStrings() { return isTrimStrings(); }
                public boolean exposeIsAutoCloseStreams() { return 
isAutoCloseStreams(); }
                public boolean exposeIsUnbuffered() { return isUnbuffered(); }
@@ -840,6 +843,9 @@ class ParserSession_Test extends TestBase {
                public java.time.Period exposeParsePeriod(String s) { return 
readPeriod(s); }
                public void exposeMark() { mark(); }
                public void exposeUnmark() { unmark(); }
+               public void exposeEnterParseDepth() throws ParseException { 
enterParseDepth(); }
+               public void exposeExitParseDepth() { exitParseDepth(); }
+               public <T> T exposeAllocateLocal(String what, 
java.util.function.Supplier<T> alloc) throws ParseException { return 
allocateLocal(what, alloc); }
                public org.apache.juneau.marshall.collections.JsonMap 
exposeGetLastLocation() { return getLastLocation(); }
                public ParserPipe exposeSetPipe(ParserPipe pp) { return 
setPipe(pp); }
                public Map<String,Object> doParseIntoMap_callDirect() throws 
Exception {
@@ -976,10 +982,12 @@ class ParserSession_Test extends TestBase {
        }
 
        // 
-----------------------------------------------------------------------------------------------------------------
-       // t - allocation-failure guard: OutOfMemoryError from within a parse 
degrades to a bounded ParseException
+       // t - narrowed allocation-failure guard: an OutOfMemoryError from 
arbitrary downstream code (NOT routed
+       //     through allocateLocal) now propagates as a raw OutOfMemoryError 
instead of being wrapped as a
+       //     ParseException.  Only allocateLocal's own local allocation is 
converted (see category u below).
        // 
-----------------------------------------------------------------------------------------------------------------
 
-       /** Test session whose parse paths always fail with an 
OutOfMemoryError, to exercise the allocation-failure guard. */
+       /** Test session whose parse paths always fail with an 
OutOfMemoryError, to exercise the narrowed guard. */
        public static class OomSession extends ParserSession {
                OomSession(JsonParser p) {
                        super(create(p));
@@ -994,21 +1002,95 @@ class ParserSession_Test extends TestBase {
                }
        }
 
-       @Test void t01_readInner_outOfMemoryProducesParseException() {
+       @Test void t01_readInner_outOfMemoryPropagatesUnwrapped() {
+               // doRead is arbitrary downstream code, not a local allocation 
routed through allocateLocal -- the OOME
+               // must propagate as-is rather than being reported as a client 
parse error.
                var s = new OomSession(JsonParser.DEFAULT);
-               var e = assertThrows(ParseException.class, () -> s.read("{}", 
Object.class));
-               assertTrue(e.getMessage().contains("Out of memory"), 
e.getMessage());
+               assertThrows(OutOfMemoryError.class, () -> s.read("{}", 
Object.class));
        }
 
-       @Test void t02_readArgs_outOfMemoryProducesParseException() {
+       @Test void t02_readArgs_outOfMemoryPropagatesUnwrapped() {
                var s = new OomSession(JsonParser.DEFAULT);
-               var e = assertThrows(ParseException.class, () -> 
s.readArgs("[]", new Type[]{Object.class}));
-               assertTrue(e.getMessage().contains("Out of memory"), 
e.getMessage());
+               assertThrows(OutOfMemoryError.class, () -> s.readArgs("[]", new 
Type[]{Object.class}));
+       }
+
+       @Test void t03_readIntoCollection_outOfMemoryPropagatesUnwrapped() {
+               var s = new OomSession(JsonParser.DEFAULT);
+               assertThrows(OutOfMemoryError.class, () -> 
s.readIntoCollection("[]", new ArrayList<>(), Object.class));
        }
 
-       @Test void t03_readIntoCollection_outOfMemoryProducesParseException() {
+       @Test void t04_readToBeanConsumer_outOfMemoryPropagatesUnwrapped() {
+               // doReadToBeanConsumer's default impl calls doRead 
(List.class) internally, so this also exercises the
+               // removed outer-boundary catch on readToBeanConsumer.
                var s = new OomSession(JsonParser.DEFAULT);
-               var e = assertThrows(ParseException.class, () -> 
s.readIntoCollection("[]", new ArrayList<>(), Object.class));
+               BeanConsumer<Object> a = o -> { /* no-op */ };
+               assertThrows(OutOfMemoryError.class, () -> 
s.readToBeanConsumer("[]", a, Object.class));
+       }
+
+       // 
-----------------------------------------------------------------------------------------------------------------
+       // u - shared parse-depth budget (enterParseDepth/exitParseDepth + 
Builder#maxParseDepth)
+       // 
-----------------------------------------------------------------------------------------------------------------
+
+       @Test void u01_enterExitParseDepth_roundTripsWithinBudget() throws 
Exception {
+               var s = new ExposingSession(JsonParser.DEFAULT);
+               // Default budget (1000) comfortably allows a handful of nested 
enter/exit calls.
+               s.exposeEnterParseDepth();
+               s.exposeEnterParseDepth();
+               s.exposeExitParseDepth();
+               s.exposeExitParseDepth();
+               // Depth counter is back at 0; another full round-trip should 
still succeed.
+               s.exposeEnterParseDepth();
+               s.exposeExitParseDepth();
+       }
+
+       @Test void 
u02_enterParseDepth_exceedsConfiguredBudget_throwsParseException() throws 
Exception {
+               // Build a session with maxParseDepth(2) via the builder, then 
drive it past the budget.
+               var builder = (ParserSession.Builder<?>) 
ParserSession.create(JsonParser.DEFAULT);
+               builder.maxParseDepth(2);
+               var s = new ExposingSession(builder);
+               s.exposeEnterParseDepth();
+               s.exposeEnterParseDepth();
+               var e = assertThrows(ParseException.class, 
s::exposeEnterParseDepth);
+               assertTrue(e.getMessage().contains("Maximum parse depth 
exceeded (2)"), e.getMessage());
+       }
+
+       @Test void u03_builder_property_maxParseDepth_unqualified() throws 
Exception {
+               var builder = (ParserSession.Builder<?>) 
ParserSession.create(JsonParser.DEFAULT);
+               builder.property("maxParseDepth", "2");
+               var s = new ExposingSession(builder);
+               s.exposeEnterParseDepth();
+               s.exposeEnterParseDepth();
+               assertThrows(ParseException.class, s::exposeEnterParseDepth);
+       }
+
+       @Test void u04_builder_property_maxParseDepth_qualified() throws 
Exception {
+               var builder = (ParserSession.Builder<?>) 
ParserSession.create(JsonParser.DEFAULT);
+               builder.property("ParserSession.maxParseDepth", "1");
+               var s = new ExposingSession(builder);
+               s.exposeEnterParseDepth();
+               assertThrows(ParseException.class, s::exposeEnterParseDepth);
+       }
+
+       @Test void u05_builder_maxParseDepth_defaultIsAThousand() throws 
Exception {
+               var s = new ExposingSession(JsonParser.DEFAULT);
+               for (var i = 0; i < 1000; i++)
+                       s.exposeEnterParseDepth();
+               assertThrows(ParseException.class, s::exposeEnterParseDepth);
+       }
+
+       // 
-----------------------------------------------------------------------------------------------------------------
+       // v - allocateLocal: the ONLY remaining place where an 
OutOfMemoryError is converted to a bounded ParseException
+       // 
-----------------------------------------------------------------------------------------------------------------
+
+       @Test void v01_allocateLocal_success_returnsSupplierValue() throws 
Exception {
+               var s = new ExposingSession(JsonParser.DEFAULT);
+               var result = s.exposeAllocateLocal("test-array", () -> new 
int[10]);
+               assertEquals(10, result.length);
+       }
+
+       @Test void v02_allocateLocal_oomFromSupplier_wrappedAsParseException() {
+               var s = new ExposingSession(JsonParser.DEFAULT);
+               var e = assertThrows(ParseException.class, () -> 
s.exposeAllocateLocal("huge-buffer", () -> { throw new 
OutOfMemoryError("simulated"); }));
                assertTrue(e.getMessage().contains("Out of memory"), 
e.getMessage());
        }
 }
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/protobuf/ProtobufParser_ParseDepth_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/protobuf/ProtobufParser_ParseDepth_Test.java
new file mode 100644
index 0000000000..9bb7f74483
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/protobuf/ProtobufParser_ParseDepth_Test.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.marshall.protobuf;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.marshall.parser.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Recursion-depth guard tests for {@link ProtobufParserSession}'s 
nested-message parse path (READY-389:
+ * shared {@link ParserSession} parse-depth budget applied to Protobuf, 
mirroring MsgPack's prior per-codec
+ * guard).
+ *
+ * <p>
+ * Before this fix, {@code ProtobufParserSession.readMessage} recursed once 
per nested {@code MESSAGE} field
+ * (or map entry/value sub-message) with no depth cap; an adversarial 
deeply-nested message would only fail
+ * once the JVM call stack was actually exhausted ({@link 
StackOverflowError}), rather than failing gracefully
+ * via a bounded {@link ParseException}.
+ */
+class ProtobufParser_ParseDepth_Test extends TestBase {
+
+       public static class Nested {
+               public Nested child;
+               public Nested() {}
+       }
+
+       /**
+        * Hand-builds a protobuf message with {@code depth} levels of nesting 
via field 1 (a {@code Nested child}
+        * MESSAGE field), the innermost level being an empty (leafless) 
sub-message.
+        */
+       private static byte[] nestedMessage(int depth) {
+               var msg = new byte[0];
+               for (var i = 0; i < depth; i++) {
+                       var out = new ByteArrayOutputStream();
+                       var w = new ProtobufWriter(out);
+                       w.writeTag(1, WireType.LEN);
+                       w.writeLenDelimited(msg);
+                       msg = out.toByteArray();
+               }
+               return msg;
+       }
+
+       @Test
+       void a01_deeplyNestedMessagesFailWithParseException() {
+               // 1100 levels of nested "child" MESSAGE fields -> exceeds the 
shared ParserSession maxParseDepth
+               // budget (default 1000).
+               var msg = nestedMessage(1100);
+               var e = assertThrows(ParseException.class, () -> 
ProtobufParser.DEFAULT.read(msg, Nested.class));
+               var text = String.valueOf(e.getMessage());
+               assertTrue(
+                       text.contains("Maximum parse depth exceeded") || 
text.contains("Depth too deep"),
+                       "Expected a graceful depth-failure ParseException.  
Actual:\n" + text);
+       }
+
+       @Test
+       void a02_moderateNestingStillParses() throws Exception {
+               // 10 levels of nesting -> well within the depth budget; 
legitimate shallow payloads must be
+               // unaffected by the new guard.
+               var msg = nestedMessage(10);
+               var b = ProtobufParser.DEFAULT.read(msg, Nested.class);
+               var cur = b;
+               for (var i = 0; i < 10; i++) {
+                       assertNotNull(cur.child);
+                       cur = cur.child;
+               }
+               assertNull(cur.child);
+       }
+}

Reply via email to