Skip to content

Commit de072d3

Browse files
committed
Fix eager allocation aspect of #186
1 parent 961b128 commit de072d3

File tree

6 files changed

+194
-71
lines changed

6 files changed

+194
-71
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ public int getMask() {
198198

199199
/**
200200
* Number of elements remaining in the current complex structure (if any),
201-
* when writing defined-length Arrays, Objects; marker {@link #INDEFINITE_LENGTH}
201+
* when writing defined-length Arrays, Objects; marker {code INDEFINITE_LENGTH}
202202
* otherwise.
203203
*/
204204
protected int _currentRemainingElements = INDEFINITE_LENGTH;

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

Lines changed: 89 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ private Feature(boolean defaultState) {
6363
private final static double MATH_POW_2_10 = Math.pow(2, 10);
6464
private final static double MATH_POW_2_NEG14 = Math.pow(2, -14);
6565

66+
// 2.11.4: [dataformats-binary#186] Avoid OOME/DoS for bigger binary;
67+
// read only up to 250k
68+
protected final static int LONGEST_NON_CHUNKED_BINARY = 250_000;
69+
6670
/*
6771
/**********************************************************
6872
/* Configuration
@@ -1706,13 +1710,15 @@ public int readBinaryValue(Base64Variant b64variant, OutputStream out) throws IO
17061710
}
17071711
}
17081712

1709-
private int _readAndWriteBytes(OutputStream out, int total) throws IOException
1713+
private int _readAndWriteBytes(OutputStream out, final int total) throws IOException
17101714
{
17111715
int left = total;
17121716
while (left > 0) {
17131717
int avail = _inputEnd - _inputPtr;
17141718
if (_inputPtr >= _inputEnd) {
1715-
loadMoreGuaranteed();
1719+
if (!loadMore()) {
1720+
_reportIncompleteBinaryRead(total, total-left);
1721+
}
17161722
avail = _inputEnd - _inputPtr;
17171723
}
17181724
int count = Math.min(avail, left);
@@ -2425,33 +2431,55 @@ private final int _nextChunkedByte2() throws IOException
24252431
// either way, got it now
24262432
return _inputBuffer[_inputPtr++];
24272433
}
2428-
2434+
2435+
/**
2436+
* Helper called to complete reading of binary data ("byte string") in
2437+
* case contents are needed.
2438+
*/
24292439
@SuppressWarnings("resource")
24302440
protected byte[] _finishBytes(int len) throws IOException
24312441
{
2442+
// Chunked?
24322443
// First, simple: non-chunked
2433-
if (len >= 0) {
2444+
if (len <= 0) {
24342445
if (len == 0) {
24352446
return NO_BYTES;
24362447
}
2437-
byte[] b = new byte[len];
2438-
if (_inputPtr >= _inputEnd) {
2439-
loadMoreGuaranteed();
2448+
return _finishChunkedBytes();
2449+
}
2450+
// Non-chunked, contiguous
2451+
if (len > LONGEST_NON_CHUNKED_BINARY) {
2452+
// [dataformats-binary#186]: avoid immediate allocation for longest
2453+
return _finishLongContiguousBytes(len);
2454+
}
2455+
2456+
final byte[] b = new byte[len];
2457+
final int expLen = len;
2458+
if (_inputPtr >= _inputEnd) {
2459+
if (!loadMore()) {
2460+
_reportIncompleteBinaryRead(expLen, 0);
24402461
}
2441-
int ptr = 0;
2442-
while (true) {
2443-
int toAdd = Math.min(len, _inputEnd - _inputPtr);
2444-
System.arraycopy(_inputBuffer, _inputPtr, b, ptr, toAdd);
2445-
_inputPtr += toAdd;
2446-
ptr += toAdd;
2447-
len -= toAdd;
2448-
if (len <= 0) {
2449-
return b;
2450-
}
2451-
loadMoreGuaranteed();
2462+
}
2463+
2464+
int ptr = 0;
2465+
while (true) {
2466+
int toAdd = Math.min(len, _inputEnd - _inputPtr);
2467+
System.arraycopy(_inputBuffer, _inputPtr, b, ptr, toAdd);
2468+
_inputPtr += toAdd;
2469+
ptr += toAdd;
2470+
len -= toAdd;
2471+
if (len <= 0) {
2472+
return b;
2473+
}
2474+
if (!loadMore()) {
2475+
_reportIncompleteBinaryRead(expLen, ptr);
24522476
}
24532477
}
2478+
}
24542479

2480+
// @since 2.12
2481+
protected byte[] _finishChunkedBytes() throws IOException
2482+
{
24552483
// or, if not, chunked...
24562484
ByteArrayBuilder bb = _getByteArrayBuilder();
24572485
while (true) {
@@ -2468,14 +2496,17 @@ protected byte[] _finishBytes(int len) throws IOException
24682496
throw _constructError("Mismatched chunk in chunked content: expected "+CBORConstants.MAJOR_TYPE_BYTES
24692497
+" but encountered "+type);
24702498
}
2471-
len = _decodeExplicitLength(ch & 0x1F);
2499+
int len = _decodeExplicitLength(ch & 0x1F);
24722500
if (len < 0) {
24732501
throw _constructError("Illegal chunked-length indicator within chunked-length value (type "+CBORConstants.MAJOR_TYPE_BYTES+")");
24742502
}
2503+
final int chunkLen = len;
24752504
while (len > 0) {
24762505
int avail = _inputEnd - _inputPtr;
24772506
if (_inputPtr >= _inputEnd) {
2478-
loadMoreGuaranteed();
2507+
if (!loadMore()) {
2508+
_reportIncompleteBinaryRead(chunkLen, chunkLen-len);
2509+
}
24792510
avail = _inputEnd - _inputPtr;
24802511
}
24812512
int count = Math.min(avail, len);
@@ -2486,7 +2517,33 @@ protected byte[] _finishBytes(int len) throws IOException
24862517
}
24872518
return bb.toByteArray();
24882519
}
2489-
2520+
2521+
// @since 2.12
2522+
protected byte[] _finishLongContiguousBytes(final int expLen) throws IOException
2523+
{
2524+
int left = expLen;
2525+
2526+
// 04-Dec-2020, tatu: Let's NOT use recycled instance since we have much
2527+
// longer content and there is likely less benefit of trying to recycle
2528+
// segments
2529+
try (final ByteArrayBuilder bb = new ByteArrayBuilder(LONGEST_NON_CHUNKED_BINARY >> 1)) {
2530+
while (left > 0) {
2531+
int avail = _inputEnd - _inputPtr;
2532+
if (avail <= 0) {
2533+
if (!loadMore()) {
2534+
_reportIncompleteBinaryRead(expLen, expLen-left);
2535+
}
2536+
avail = _inputEnd - _inputPtr;
2537+
}
2538+
int count = Math.min(avail, left);
2539+
bb.write(_inputBuffer, _inputPtr, count);
2540+
_inputPtr += count;
2541+
left -= count;
2542+
}
2543+
return bb.toByteArray();
2544+
}
2545+
}
2546+
24902547
protected final JsonToken _decodeFieldName() throws IOException
24912548
{
24922549
if (_inputPtr >= _inputEnd) {
@@ -2635,9 +2692,8 @@ protected final void _decodeNonStringName(int ch) throws IOException
26352692
} else if (type == CBORConstants.MAJOR_TYPE_INT_NEG) {
26362693
name = _numberToName(ch, true);
26372694
} else if (type == CBORConstants.MAJOR_TYPE_BYTES) {
2638-
/* 08-Sep-2014, tatu: As per [Issue#5], there are codecs
2639-
* (f.ex. Perl module "CBOR::XS") that use Binary data...
2640-
*/
2695+
// 08-Sep-2014, tatu: As per [Issue#5], there are codecs
2696+
// (f.ex. Perl module "CBOR::XS") that use Binary data...
26412697
final int blen = _decodeExplicitLength(ch & 0x1F);
26422698
byte[] b = _finishBytes(blen);
26432699
// TODO: Optimize, if this becomes commonly used & bottleneck; we have
@@ -3204,7 +3260,7 @@ private final int _decodeChunkedUTF8_4(int c) throws IOException
32043260
/**********************************************************
32053261
*/
32063262

3207-
protected final boolean loadMore() throws IOException
3263+
protected boolean loadMore() throws IOException
32083264
{
32093265
if (_inputStream != null) {
32103266
_currInputProcessed += _inputEnd;
@@ -3225,7 +3281,7 @@ protected final boolean loadMore() throws IOException
32253281
return false;
32263282
}
32273283

3228-
protected final void loadMoreGuaranteed() throws IOException {
3284+
protected void loadMoreGuaranteed() throws IOException {
32293285
if (!loadMore()) { _reportInvalidEOF(); }
32303286
}
32313287

@@ -3351,6 +3407,13 @@ protected void _reportInvalidOther(int mask, int ptr) throws JsonParseException
33513407
_reportInvalidOther(mask);
33523408
}
33533409

3410+
// @since 2.12
3411+
protected void _reportIncompleteBinaryRead(int expLen, int actLen) throws IOException
3412+
{
3413+
_reportInvalidEOF(String.format(" for Binary value: expected %d bytes, only found %d",
3414+
expLen, actLen), _currToken);
3415+
}
3416+
33543417
/*
33553418
/**********************************************************
33563419
/* Internal methods, other
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.fasterxml.jackson.dataformat.cbor;
2+
3+
import java.io.ByteArrayOutputStream;
4+
5+
import com.fasterxml.jackson.core.JsonParser;
6+
import com.fasterxml.jackson.core.JsonProcessingException;
7+
import com.fasterxml.jackson.core.JsonToken;
8+
9+
import com.fasterxml.jackson.databind.ObjectMapper;
10+
11+
// Mostly for [dataformats-binary#186]: corrupt encoding indicating humongous payload
12+
public class BrokenLongBinary186Test extends CBORTestBase
13+
{
14+
private final ObjectMapper MAPPER = cborMapper();
15+
16+
/*
17+
/**********************************************************************
18+
/* First regular, read-it-all access, from non-chunked
19+
/**********************************************************************
20+
*/
21+
22+
// [dataformats-binary#186]
23+
public void testCorruptVeryLongBinary() throws Exception {
24+
// Let's do about 2 GB to likely trigger failure
25+
_testCorruptLong(1_999_999_999, 95000);
26+
}
27+
28+
// [dataformats-binary#186]
29+
public void testCorruptQuiteLongBinary() throws Exception {
30+
// Value below limit for chunked handling
31+
_testCorruptLong(CBORParser.LONGEST_NON_CHUNKED_BINARY >> 1, 37);
32+
}
33+
34+
private void _testCorruptLong(int allegedLength, int actualIncluded) throws Exception
35+
{
36+
JsonParser p = MAPPER.createParser(_createBrokenDoc(allegedLength, actualIncluded));
37+
assertEquals(JsonToken.VALUE_EMBEDDED_OBJECT, p.nextToken());
38+
try {
39+
p.getBinaryValue();
40+
fail("Should fail");
41+
} catch (JsonProcessingException e) {
42+
verifyException(e, "Unexpected end-of-input for Binary value");
43+
verifyException(e, "expected "+allegedLength+" bytes, only found "+actualIncluded);
44+
}
45+
}
46+
47+
/*
48+
/**********************************************************************
49+
/* And then "streaming" access
50+
/**********************************************************************
51+
*/
52+
53+
// [dataformats-binary#186]
54+
public void testQuiteLongStreaming() throws Exception
55+
{
56+
// Can try bit shorter here, like 500 megs
57+
final int allegedLength = 500_000_000;
58+
59+
JsonParser p = MAPPER.createParser(_createBrokenDoc(allegedLength, 72000));
60+
assertEquals(JsonToken.VALUE_EMBEDDED_OBJECT, p.nextToken());
61+
try {
62+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
63+
p.readBinaryValue(bytes);
64+
fail("Should fail");
65+
} catch (JsonProcessingException e) {
66+
verifyException(e, "Unexpected end-of-input for Binary value");
67+
verifyException(e, "expected "+allegedLength+" bytes, only found 72000");
68+
}
69+
}
70+
71+
private byte[] _createBrokenDoc(int allegedLength, int actualIncluded) throws Exception
72+
{
73+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
74+
75+
if (allegedLength > 0xFFFF) {
76+
bytes.write((byte) (CBORConstants.PREFIX_TYPE_BYTES | CBORConstants.SUFFIX_UINT32_ELEMENTS));
77+
bytes.write((byte) (allegedLength >> 24));
78+
bytes.write((byte) (allegedLength >> 16));
79+
bytes.write((byte) (allegedLength >> 8));
80+
bytes.write((byte) allegedLength);
81+
} else { // assume shorter
82+
bytes.write((byte) (CBORConstants.PREFIX_TYPE_BYTES | CBORConstants.SUFFIX_UINT16_ELEMENTS));
83+
bytes.write((byte) (allegedLength >> 8));
84+
bytes.write((byte) allegedLength);
85+
}
86+
// but only include couple of bytes
87+
for (int i = 0; i < actualIncluded; ++i) {
88+
bytes.write((byte) i);
89+
}
90+
return bytes.toByteArray();
91+
}
92+
93+
}

cbor/src/test/java/com/fasterxml/jackson/dataformat/cbor/failing/BrokenLongBinary186Test.java

Lines changed: 0 additions & 42 deletions
This file was deleted.

release-notes/CREDITS-2.x

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,11 @@ John (iziamos@github)
116116
(2.10.0)
117117

118118
Paul Adolph (padolph@github)
119-
* Reported #185: Internal parsing of tagged arrays can lead to stack overflow
119+
* Reported #185: (cbor) Internal parsing of tagged arrays can lead to stack overflow
120120
(2.10.1)
121+
* Reported #186: (cbor) Eager allocation of byte buffer can cause `java.lang.OutOfMemoryError`
122+
exception
123+
(2.11.4)
121124

122125
Yanming Zhou (quaff@github)
123126
* Reported #188: Unexpected `MismatchedInputException` for `byte[]` value bound to `String`

release-notes/VERSION-2.x

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@ Project: jackson-datatypes-binaryModules:
88
=== Releases ===
99
------------------------------------------------------------------------
1010

11+
2.11.4 (not yet released)
12+
13+
#186: (cbor) Eager allocation of byte buffer can cause `java.lang.OutOfMemoryError`
14+
exception
15+
(reported by Paul A)
16+
1117
2.11.3 (02-Oct-2020)
1218

13-
#219: Cache record names to avoid hitting class loader
19+
#219: (avro) Cache record names to avoid hitting class loader
1420
(contributed by Marcos P)
1521

1622
2.11.2 (02-Aug-2020)

0 commit comments

Comments
 (0)