Skip to content

Commit 5740026

Browse files
committed
Fixed #260 (lazy byte[] allocation for longer binary payloads)
1 parent 9b82247 commit 5740026

File tree

8 files changed

+116
-11
lines changed

8 files changed

+116
-11
lines changed

cbor/src/main/java/com/fasterxml/jackson/dataformat/cbor/CBORParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2558,7 +2558,7 @@ protected byte[] _finishLongContiguousBytes(final int expLen) throws IOException
25582558
int count = Math.min(avail, left);
25592559
bb.write(_inputBuffer, _inputPtr, count);
25602560
_inputPtr += count;
2561-
left -= count;
2561+
left -= count;
25622562
}
25632563
return bb.toByteArray();
25642564
}

cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/fuzz/Fuzz32173LongTextTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class Fuzz32173LongTextTest extends CBORTestBase
1010
{
1111
private final ObjectMapper MAPPER = cborMapper();
1212

13-
public void testInvalidShortText() throws Exception
13+
public void testTruncatedLongText() throws Exception
1414
{
1515
final byte[] input = new byte[] {
1616
0x7A, // Text value

pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@
8484
<excludes>
8585
<exclude>com/fasterxml/jackson/**/failing/*.java</exclude>
8686
</excludes>
87+
<!-- 20-Mar-2021, tatu: limit heap for tests to catch some
88+
of "too big allocation" cases
89+
-->
90+
<argLine>-Xmx1024m</argLine>
8791
</configuration>
8892
</plugin>
8993
</plugins>

release-notes/CREDITS-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ Fabian Meumertzheim (fmeum@github)
172172
* Reported #259: (cbor) Failed to handle case of alleged String with length of
173173
Integer.MAX_VALUE
174174
(2.12.3)
175+
* Reported #260: (smile) Allocate byte[] lazily for longer Smile binary data payloads
176+
(2.12.3)
175177
* Reported #261 (cbor) CBORParser need to validate zero-length byte[] for BigInteger
176178
(2.12.3)
177179

release-notes/VERSION-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Modules:
1818
(reported by Fabian M)
1919
#259: (cbor) Failed to handle case of alleged String with length of Integer.MAX_VALUE
2020
(reported by Fabian M)
21+
#260: (smile) Allocate byte[] lazily for longer Smile binary data payloads
22+
(reported by Fabian M)
2123
#261 (cbor) CBORParser need to validate zero-length byte[] for BigInteger
2224
(reported by Fabian M)
2325

smile/src/main/java/com/fasterxml/jackson/dataformat/smile/SmileParser.java

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.fasterxml.jackson.core.*;
99
import com.fasterxml.jackson.core.io.IOContext;
1010
import com.fasterxml.jackson.core.sym.ByteQuadsCanonicalizer;
11+
import com.fasterxml.jackson.core.util.ByteArrayBuilder;
1112

1213
import static com.fasterxml.jackson.dataformat.smile.SmileConstants.BYTE_MARKER_END_OF_STRING;
1314

@@ -239,9 +240,11 @@ private final byte _nextByteGuaranteed() throws IOException
239240
}
240241

241242
protected final void _loadMoreGuaranteed() throws IOException {
242-
if (!_loadMore()) { _reportInvalidEOF(); }
243+
if (!_loadMore()) {
244+
_reportInvalidEOF();
245+
}
243246
}
244-
247+
245248
protected final boolean _loadMore() throws IOException
246249
{
247250
//_currInputRowStart -= _inputEnd;
@@ -1895,7 +1898,7 @@ protected final void _finishToken() throws IOException
18951898
_binaryValue = _read7BitBinaryWithLength();
18961899
return;
18971900
case 7: // binary, raw
1898-
_finishRawBinary();
1901+
_binaryValue = _finishRawBinary();
18991902
return;
19001903
}
19011904
}
@@ -2403,27 +2406,67 @@ private final void _decodeLongUnicode() throws IOException
24032406
_textBuffer.setCurrentLength(outPtr);
24042407
}
24052408

2406-
private final void _finishRawBinary() throws IOException
2409+
private final byte[] _finishRawBinary() throws IOException
24072410
{
24082411
int byteLen = _readUnsignedVInt();
2409-
_binaryValue = new byte[byteLen];
2412+
2413+
// 20-Mar-2021, tatu [dataformats-binary#260]: avoid eager allocation
2414+
// for very large content
2415+
if (byteLen > LONGEST_NON_CHUNKED_BINARY) {
2416+
return _finishLongContiguousBytes(byteLen);
2417+
}
2418+
2419+
final int expLen = byteLen;
2420+
final byte[] b = new byte[byteLen];
2421+
24102422
if (_inputPtr >= _inputEnd) {
2423+
if (!_loadMore()) {
2424+
_reportIncompleteBinaryRead(expLen, 0);
2425+
}
24112426
_loadMoreGuaranteed();
24122427
}
24132428
int ptr = 0;
24142429
while (true) {
24152430
int toAdd = Math.min(byteLen, _inputEnd - _inputPtr);
2416-
System.arraycopy(_inputBuffer, _inputPtr, _binaryValue, ptr, toAdd);
2431+
System.arraycopy(_inputBuffer, _inputPtr, b, ptr, toAdd);
24172432
_inputPtr += toAdd;
24182433
ptr += toAdd;
24192434
byteLen -= toAdd;
24202435
if (byteLen <= 0) {
2421-
return;
2436+
return b;
2437+
}
2438+
if (!_loadMore()) {
2439+
_reportIncompleteBinaryRead(expLen, ptr);
24222440
}
2423-
_loadMoreGuaranteed();
24242441
}
24252442
}
2426-
2443+
2444+
// @since 2.12.3
2445+
protected byte[] _finishLongContiguousBytes(final int expLen) throws IOException
2446+
{
2447+
int left = expLen;
2448+
2449+
// 20-Mar-2021, tatu: Let's NOT use recycled instance since we have much
2450+
// longer content and there is likely less benefit of trying to recycle
2451+
// segments
2452+
try (final ByteArrayBuilder bb = new ByteArrayBuilder(LONGEST_NON_CHUNKED_BINARY >> 1)) {
2453+
while (left > 0) {
2454+
int avail = _inputEnd - _inputPtr;
2455+
if (avail <= 0) {
2456+
if (!_loadMore()) {
2457+
_reportIncompleteBinaryRead(expLen, expLen-left);
2458+
}
2459+
avail = _inputEnd - _inputPtr;
2460+
}
2461+
int count = Math.min(avail, left);
2462+
bb.write(_inputBuffer, _inputPtr, count);
2463+
_inputPtr += count;
2464+
left -= count;
2465+
}
2466+
return bb.toByteArray();
2467+
}
2468+
}
2469+
24272470
/*
24282471
/**********************************************************
24292472
/* Internal methods, skipping
@@ -2693,6 +2736,13 @@ protected void _reportInvalidOther(int mask, int ptr) throws JsonParseException
26932736
_reportInvalidOther(mask);
26942737
}
26952738

2739+
// @since 2.12.3
2740+
protected void _reportIncompleteBinaryRead(int expLen, int actLen) throws IOException
2741+
{
2742+
_reportInvalidEOF(String.format(" for Binary value: expected %d bytes, only found %d",
2743+
expLen, actLen), _currToken);
2744+
}
2745+
26962746
/*
26972747
/**********************************************************
26982748
/* Internal methods, other

smile/src/main/java/com/fasterxml/jackson/dataformat/smile/SmileParserBase.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ public abstract class SmileParserBase extends ParserMinimalBase
2323
{
2424
protected final static String[] NO_STRINGS = new String[0];
2525

26+
// 2.12.3: [dataformats-binary#260] Avoid OOME/DoS for bigger binary;
27+
// read only up to 250k
28+
protected final static int LONGEST_NON_CHUNKED_BINARY = 250_000;
29+
2630
/*
2731
/**********************************************************
2832
/* Config
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.fasterxml.jackson.dataformat.smile.fuzz;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.util.Arrays;
5+
6+
import com.fasterxml.jackson.core.io.JsonEOFException;
7+
import com.fasterxml.jackson.databind.ObjectMapper;
8+
9+
import com.fasterxml.jackson.dataformat.smile.BaseTestForSmile;
10+
11+
// For [dataformats-binary#]
12+
public class Fuzz32180BinaryTest extends BaseTestForSmile
13+
{
14+
private final ObjectMapper MAPPER = smileMapper();
15+
16+
// Payload:
17+
public void testInvalidBinary() throws Exception
18+
{
19+
final byte[] input0 = new byte[] {
20+
0x3A, 0x29, 0x0A, 0x00, // smile signature
21+
(byte) 0xFD, // raw binary
22+
0x0F, 0x7E, 0x20,
23+
0x20, (byte) 0xFF, // 5 byte VInt for 0x7fe4083f (close to Integer.MAX_VALUE)
24+
// and one byte of binary payload
25+
0x00
26+
};
27+
// Let's expand slightly to avoid too early detection, ensure that we are
28+
// not only checking completely missing payload or such
29+
final byte[] input = Arrays.copyOf(input0, 65 * 1024);
30+
31+
try (ByteArrayInputStream bytes = new ByteArrayInputStream(input)) {
32+
try {
33+
/*JsonNode root =*/ MAPPER.readTree(bytes);
34+
} catch (JsonEOFException e) {
35+
verifyException(e, "Unexpected end-of-input for Binary value: expected 2145650751 bytes, only found 66550");
36+
} catch (OutOfMemoryError e) {
37+
// Just to make it easier to see on fail (not ideal but better than nothing)
38+
e.printStackTrace();
39+
throw e;
40+
}
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)