Skip to content

Commit faaa4f7

Browse files
committed
Merge branch '3.1' into 3.2
2 parents a8c1aab + 7169756 commit faaa4f7

7 files changed

Lines changed: 220 additions & 16 deletions

File tree

release-notes/CREDITS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ Rob Spoor (@robtimus)
4545
* Reported #1561: Javadoc for 3.1.0 doesn't work (build with JDK 21)
4646
(3.1.2)
4747

48+
49+
Revanth Meesala (@revanthmeesala)
50+
* Contributed #1642: Fix maxDocumentLength bypass in async parser single-feedInput()
51+
case [GHSA-2c4j-63jj-9fqr]
52+
(3.1.6)
53+
4854
seonwoojung (@seonwooj0810)
4955
* Contributed #707: Add `JsonReadFeature.ALLOW_HEXADECIMAL_NUMBERS`
5056
for JSON5-style hexadecimal integer literals

release-notes/CREDITS-2.x

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,3 +530,8 @@ Patrick Strawderman (@kilink)
530530
* Contributed #1622: `UTF8JsonGenerator.writeBinary()` should allocate buffer
531531
based on supplied length
532532
(2.23.0)
533+
534+
Revanth Meesala (@revanthmeesala)
535+
* Contributed #1642: Fix maxDocumentLength bypass in async parser single-feedInput()
536+
case [GHSA-2c4j-63jj-9fqr]
537+
(2.18.10)

release-notes/VERSION

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ JSON library.
1919

2020
No changes since 3.2
2121

22+
3.2.2 (not yet released)
23+
24+
#1630: `UTF8StreamJsonParser.nextName()` ignores
25+
`JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS`
26+
(reported by @jng260)
27+
(fix by @cowtowncoder, w/ Claude code)
28+
#1642: Fix maxDocumentLength bypass in async parser single-feedInput() case
29+
[GHSA-2c4j-63jj-9fqr]
30+
(fix by Revanth M)
31+
2232
3.2.1 (10-Jul-2026)
2333

2434
No changes since 3.2.0
@@ -53,6 +63,9 @@ No changes since 3.2.0
5363
`JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS`
5464
(reported by @jng260)
5565
(fix by @cowtowncoder, w/ Claude code)
66+
#1642: Fix maxDocumentLength bypass in async parser single-feedInput() case
67+
[GHSA-2c4j-63jj-9fqr]
68+
(fix by Revanth M)
5669

5770
3.1.5 (07-Jul-2025)
5871

release-notes/VERSION-2.x

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,12 @@ No changes since 2.19.1
157157
(requested by Ilenia S)
158158
(fixed by @pjfanning)
159159

160+
2.18.10 (not yet released)
161+
162+
#1642: Fix maxDocumentLength bypass in async parser single-feedInput() case
163+
[GHSA-2c4j-63jj-9fqr]
164+
(fix by Revanth M)
165+
160166
2.18.9 (07-Jul-2026)
161167

162168
No changes since 2.18.8

src/main/java/tools/jackson/core/json/async/NonBlockingByteArrayJsonParser.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,28 @@ public ByteArrayFeeder nonBlockingInputFeeder() {
3434
@Override
3535
public void feedInput(final byte[] buf, final int start, final int end) throws JacksonException
3636
{
37-
// Must not have remaining input
38-
if (_inputPtr < _inputEnd) {
39-
_reportError("Still have %d undecoded bytes, should not call 'feedInput'", _inputEnd - _inputPtr);
40-
}
4137
if (end < start) {
4238
_reportError("Input end (%d) may not be before start (%d)", end, start);
4339
}
4440
// and shouldn't have been marked as end-of-input
4541
if (_endOfInput) {
4642
_reportError("Already closed, cannot feed more input");
4743
}
44+
// Must not have remaining input
45+
if (_inputPtr < _inputEnd) {
46+
_reportError("Still have %d undecoded bytes, should not call 'feedInput'", _inputEnd - _inputPtr);
47+
}
48+
// 06-Sep-2023, tatu: [core#1046] Enforce max doc length limit
49+
// 17-Jul-2026, revanthm: [core#1642] Must include buffer being fed, not just
50+
// previously fed ones, so that a single feedInput() call carrying the
51+
// whole document is checked against its real length. Also: validate
52+
// before updating any state, to leave parser untouched if this throws
53+
_streamReadConstraints.validateDocumentLength(
54+
_currInputProcessed + _origBufferLen + (end - start));
55+
4856
// Time to update pointers first
4957
_currInputProcessed += _origBufferLen;
5058

51-
// 06-Sep-2023, tatu: [core#1046] Enforce max doc length limit
52-
_streamReadConstraints.validateDocumentLength(_currInputProcessed);
53-
5459
// Also need to adjust row start, to work as if it extended into the past wrt new buffer
5560
_currInputRowStart = start - (_inputEnd - _currInputRowStart);
5661

src/main/java/tools/jackson/core/json/async/NonBlockingByteBufferJsonParser.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,8 @@ public NonBlockingInputFeeder nonBlockingInputFeeder() {
3737
}
3838

3939
@Override
40-
public void feedInput(final ByteBuffer byteBuffer) throws JacksonException {
41-
// Must not have remaining input
42-
if (_inputPtr < _inputEnd) {
43-
_reportError("Still have %d undecoded bytes, should not call 'feedInput'", _inputEnd - _inputPtr);
44-
}
45-
40+
public void feedInput(final ByteBuffer byteBuffer) throws JacksonException
41+
{
4642
final int start = byteBuffer.position();
4743
final int end = byteBuffer.limit();
4844

@@ -53,12 +49,21 @@ public void feedInput(final ByteBuffer byteBuffer) throws JacksonException {
5349
if (_endOfInput) {
5450
_reportError("Already closed, cannot feed more input");
5551
}
52+
// Must not have remaining input
53+
if (_inputPtr < _inputEnd) {
54+
_reportError("Still have %d undecoded bytes, should not call 'feedInput'", _inputEnd - _inputPtr);
55+
}
56+
// 06-Sep-2023, tatu: [core#1046] Enforce max doc length limit
57+
// 17-Jul-2026, revanthm: [core#1642] Must include buffer being fed, not just
58+
// previously fed ones, so that a single feedInput() call carrying the
59+
// whole document is checked against its real length. Also: validate
60+
// before updating any state, to leave parser untouched if this throws
61+
_streamReadConstraints.validateDocumentLength(
62+
_currInputProcessed + _origBufferLen + (end - start));
63+
5664
// Time to update pointers first
5765
_currInputProcessed += _origBufferLen;
5866

59-
// 06-Sep-2023, tatu: [core#1046] Enforce max doc length limit
60-
_streamReadConstraints.validateDocumentLength(_currInputProcessed);
61-
6267
// Also need to adjust row start, to work as if it extended into the past wrt new buffer
6368
_currInputRowStart = start - (_inputEnd - _currInputRowStart);
6469

src/test/java/tools/jackson/core/unittest/constraints/LargeDocReadTest.java

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package tools.jackson.core.unittest.constraints;
22

33
import java.io.IOException;
4+
import java.nio.ByteBuffer;
5+
import java.util.Arrays;
46

57
import org.junit.jupiter.api.Test;
68

79
import tools.jackson.core.JsonParser;
10+
import tools.jackson.core.JsonToken;
811
import tools.jackson.core.ObjectReadContext;
912
import tools.jackson.core.StreamReadConstraints;
13+
import tools.jackson.core.async.ByteArrayFeeder;
14+
import tools.jackson.core.async.ByteBufferFeeder;
1015
import tools.jackson.core.exc.StreamConstraintsException;
1116
import tools.jackson.core.json.JsonFactory;
1217
import tools.jackson.core.unittest.async.AsyncTestBase;
@@ -108,6 +113,140 @@ void largeNameWithSmallLimitAsync() throws Exception
108113
}
109114
}
110115

116+
// [core#1642] maxDocumentLength must also be enforced when the caller feeds
117+
// the whole document via a single feedInput() call (e.g. pre-buffered input),
118+
// not just when input arrives split across multiple feedInput() calls.
119+
@Test
120+
void largeNameWithSmallLimitAsyncSingleFeed() throws Exception
121+
{
122+
final byte[] doc = utf8Bytes(generateJSON(12_000));
123+
124+
// first with byte[] backend: bytesPerRead >= doc.length so the whole
125+
// document goes through in exactly one feedInput() call
126+
try (AsyncReaderWrapper p = asyncForBytes(JSON_F_DOC_10K, doc.length, doc, 1)) {
127+
consumeAsync(p);
128+
fail("expected StreamConstraintsException");
129+
} catch (StreamConstraintsException e) {
130+
verifyMaxDocLen(JSON_F_DOC_10K, e);
131+
}
132+
133+
// then with byte buffer backend, same single-call condition
134+
try (AsyncReaderWrapper p = asyncForByteBuffer(JSON_F_DOC_10K, doc.length, doc, 1)) {
135+
consumeAsync(p);
136+
fail("expected StreamConstraintsException");
137+
} catch (StreamConstraintsException e) {
138+
verifyMaxDocLen(JSON_F_DOC_10K, e);
139+
}
140+
}
141+
142+
// [core#1642] Boundary check: a single feedInput() call carrying EXACTLY
143+
// maxDocumentLength bytes must still parse successfully -- validateDocumentLength()
144+
// rejects only len > maxDocumentLength, so the limit itself is inclusive.
145+
// This pins down "bytes fed, not consumed" semantics and guards against a
146+
// future off-by-one in the single-feed fix.
147+
@Test
148+
void largeNameWithSmallLimitAsyncSingleFeedAtBoundary() throws Exception
149+
{
150+
final long limit = JSON_F_DOC_10K.streamReadConstraints().getMaxDocumentLength();
151+
final byte[] doc = utf8Bytes(generateExactLengthJSON((int) limit));
152+
assertEquals(limit, doc.length);
153+
154+
// first with byte[] backend: bytesPerRead >= doc.length so the whole
155+
// document goes through in exactly one feedInput() call
156+
try (AsyncReaderWrapper p = asyncForBytes(JSON_F_DOC_10K, doc.length, doc, 1)) {
157+
consumeAsync(p);
158+
}
159+
160+
// then with byte buffer backend, same single-call condition
161+
try (AsyncReaderWrapper p = asyncForByteBuffer(JSON_F_DOC_10K, doc.length, doc, 1)) {
162+
consumeAsync(p);
163+
}
164+
}
165+
166+
// [core#1642] Same boundary, but reached across MANY feedInput() calls: bytes
167+
// fed must accumulate to exactly maxDocumentLength and still parse, verifying
168+
// the single-feed fix did not start double-counting incrementally fed buffers.
169+
@Test
170+
void largeNameWithSmallLimitAsyncMultiFeedAtBoundary() throws Exception
171+
{
172+
final long limit = JSON_F_DOC_10K.streamReadConstraints().getMaxDocumentLength();
173+
final byte[] doc = utf8Bytes(generateExactLengthJSON((int) limit));
174+
assertEquals(limit, doc.length);
175+
176+
// 1000 bytes per call, so exactly 10 feedInput() calls totalling the limit
177+
try (AsyncReaderWrapper p = asyncForBytes(JSON_F_DOC_10K, 1000, doc, 1)) {
178+
consumeAsync(p);
179+
}
180+
try (AsyncReaderWrapper p = asyncForByteBuffer(JSON_F_DOC_10K, 1000, doc, 1)) {
181+
consumeAsync(p);
182+
}
183+
}
184+
185+
// [core#1642] A rejected feedInput() must not corrupt the running byte count:
186+
// validation happens BEFORE any state is updated, so a caller that catches the
187+
// StreamConstraintsException and keeps feeding still gets an accurate total
188+
// (the rejected call's predecessor must not be counted twice).
189+
@Test
190+
void docLengthCountIntactAfterRejectedFeedBytes() throws Exception
191+
{
192+
try (JsonParser p = JSON_F_DOC_10K.createNonBlockingByteArrayParser(ObjectReadContext.empty())) {
193+
final ByteArrayFeeder feeder = (ByteArrayFeeder) p.nonBlockingInputFeeder();
194+
195+
// 5000 fed, well under the 10000 limit
196+
feeder.feedInput(whitespace(5000), 0, 5000);
197+
assertToken(JsonToken.NOT_AVAILABLE, p.nextToken());
198+
199+
// would reach 11000: rejected, and must leave the count at 5000
200+
try {
201+
feeder.feedInput(whitespace(6000), 0, 6000);
202+
fail("expected StreamConstraintsException");
203+
} catch (StreamConstraintsException e) {
204+
verifyMaxDocLen(JSON_F_DOC_10K, e);
205+
}
206+
207+
// 5000 more == 10000 total: at the limit, so must still be accepted
208+
feeder.feedInput(whitespace(5000), 0, 5000);
209+
assertToken(JsonToken.NOT_AVAILABLE, p.nextToken());
210+
211+
// and one byte past it must report the true total, not an inflated one
212+
try {
213+
feeder.feedInput(whitespace(1), 0, 1);
214+
fail("expected StreamConstraintsException");
215+
} catch (StreamConstraintsException e) {
216+
verifyException(e, "Document length (10001)");
217+
}
218+
}
219+
}
220+
221+
// [core#1642] as above, for the ByteBuffer-backed parser
222+
@Test
223+
void docLengthCountIntactAfterRejectedFeedByteBuffer() throws Exception
224+
{
225+
try (JsonParser p = JSON_F_DOC_10K.createNonBlockingByteBufferParser(ObjectReadContext.empty())) {
226+
final ByteBufferFeeder feeder = (ByteBufferFeeder) p.nonBlockingInputFeeder();
227+
228+
feeder.feedInput(ByteBuffer.wrap(whitespace(5000)));
229+
assertToken(JsonToken.NOT_AVAILABLE, p.nextToken());
230+
231+
try {
232+
feeder.feedInput(ByteBuffer.wrap(whitespace(6000)));
233+
fail("expected StreamConstraintsException");
234+
} catch (StreamConstraintsException e) {
235+
verifyMaxDocLen(JSON_F_DOC_10K, e);
236+
}
237+
238+
feeder.feedInput(ByteBuffer.wrap(whitespace(5000)));
239+
assertToken(JsonToken.NOT_AVAILABLE, p.nextToken());
240+
241+
try {
242+
feeder.feedInput(ByteBuffer.wrap(whitespace(1)));
243+
fail("expected StreamConstraintsException");
244+
} catch (StreamConstraintsException e) {
245+
verifyException(e, "Document length (10001)");
246+
}
247+
}
248+
}
249+
111250
// [core#1575] DataInput with maxDocumentLength should enforce the limit
112251
@Test
113252
void dataInputWithDocLengthLimitEnforced() throws Exception
@@ -157,6 +296,31 @@ private void consumeAsync(AsyncReaderWrapper w) throws IOException {
157296
}
158297
}
159298

299+
// Builds a valid JSON array whose UTF-8 byte length is exactly {@code exactLen},
300+
// using trailing whitespace padding before the closing bracket (all-ASCII content,
301+
// so char length == byte length).
302+
private String generateExactLengthJSON(final int exactLen) {
303+
final StringBuilder sb = new StringBuilder();
304+
sb.append('[');
305+
while (sb.length() < exactLen - 10) {
306+
sb.append("1,");
307+
}
308+
sb.append('1');
309+
while (sb.length() < exactLen - 1) {
310+
sb.append(' ');
311+
}
312+
sb.append(']');
313+
return sb.toString();
314+
}
315+
316+
// Content that is valid-but-tokenless, so buffers can be fed and fully consumed
317+
// without producing tokens: lets tests exercise feedInput() accounting directly.
318+
private byte[] whitespace(final int len) {
319+
final byte[] b = new byte[len];
320+
Arrays.fill(b, (byte) ' ');
321+
return b;
322+
}
323+
160324
private String generateJSON(final int docLen) {
161325
final StringBuilder sb = new StringBuilder();
162326
sb.append("[");

0 commit comments

Comments
 (0)