Skip to content

Commit 17bc4ca

Browse files
committed
Fix #736 (OOME for very deeply nested JsonPointers)
1 parent b886bc8 commit 17bc4ca

File tree

4 files changed

+97
-33
lines changed

4 files changed

+97
-33
lines changed

release-notes/CREDITS-2.x

+2
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ Doug Roper (htmldoug@github)
166166
* Contributed #733: Add `StreamReadCapability.EXACT_FLOATS` to indicate whether parser reports exact
167167
floating-point values or not
168168
(2.14.0)
169+
* Reported #736: `JsonPointer` quadratic memory use: OOME on deep inputs
170+
(2.14.0)
169171

170172
Alexander Eyers-Taylor (aeyerstaylor@github)
171173
* Reported #510: Fix ArrayIndexOutofBoundsException found by LGTM.com

release-notes/VERSION-2.x

+6-1
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@ JSON library.
1414
=== Releases ===
1515
------------------------------------------------------------------------
1616

17-
2.14.0-rc1 (25-Sep-2022)
17+
Unreleased:
18+
19+
#736: `JsonPointer` quadratic memory use: OOME on deep inputs
20+
(reported by Doug R)
21+
1822
2.14.0-rc2 (10-Oct-2022)
23+
2.14.0-rc1 (25-Sep-2022)
1924

2025
#478: Provide implementation of async JSON parser fed by `ByteBufferFeeder`
2126
(requested by Arjen P)

src/main/java/com/fasterxml/jackson/core/JsonPointer.java

+86-29
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
package com.fasterxml.jackson.core;
22

3+
import java.io.*;
4+
35
import com.fasterxml.jackson.core.io.NumberInput;
4-
import java.io.Externalizable;
5-
import java.io.IOException;
6-
import java.io.ObjectInput;
7-
import java.io.ObjectOutput;
8-
import java.io.ObjectStreamException;
9-
import java.io.Serializable;
106

117
/**
128
* Implementation of
@@ -17,6 +13,10 @@
1713
* It may be used in future for filtering of streaming JSON content
1814
* as well (not implemented yet for 2.3).
1915
*<p>
16+
* Note that the implementation was largely rewritten for Jackson 2.14 to
17+
* reduce memory usage by sharing backing "full path" representation for
18+
* nested instances.
19+
*<p>
2020
* Instances are fully immutable and can be cached, shared between threads.
2121
*
2222
* @author Tatu Saloranta
@@ -68,6 +68,8 @@ public class JsonPointer implements Serializable
6868
*<p>
6969
* NOTE: starting with 2.14, there is no accompanying
7070
* {@link #_asStringOffset} that MUST be considered with this String;
71+
* this {@code String} may contain preceding path, as it is now full path
72+
* of parent pointer, except for the outermost pointer instance.
7173
*/
7274
protected final String _asString;
7375

@@ -81,6 +83,10 @@ public class JsonPointer implements Serializable
8183
protected final int _matchingElementIndex;
8284

8385
/**
86+
* Lazily-calculated hash code: need to retain hash code now that we can no
87+
* longer rely on {@link #_asString} being the exact full representation (it
88+
* is often "more", including parent path).
89+
*
8490
* @since 2.14
8591
*/
8692
protected int _hashCode;
@@ -209,44 +215,69 @@ public static JsonPointer forPath(JsonStreamContext context,
209215
context = context.getParent();
210216
}
211217
}
212-
JsonPointer tail = null;
218+
219+
PointerSegment next = null;
220+
int approxLength = 0;
213221

214222
for (; context != null; context = context.getParent()) {
215223
if (context.inObject()) {
216-
String seg = context.getCurrentName();
217-
if (seg == null) { // is this legal?
218-
seg = "";
224+
String propName = context.getCurrentName();
225+
if (propName == null) { // is this legal?
226+
propName = "";
219227
}
220-
tail = new JsonPointer(_fullPath(tail, seg), 0, seg, tail);
228+
approxLength += 2 + propName.length();
229+
next = new PointerSegment(next, propName, -1);
230+
// tail = new JsonPointer(_fullPath(tail, seg), 0, seg, tail);
221231
} else if (context.inArray() || includeRoot) {
222232
int ix = context.getCurrentIndex();
223-
String ixStr = String.valueOf(ix);
224-
tail = new JsonPointer(_fullPath(tail, ixStr), 0, ixStr, ix, tail);
233+
approxLength += 6;
234+
next = new PointerSegment(next, null, ix);
235+
// tail = new JsonPointer(_fullPath(tail, ixStr), 0, ixStr, ix, tail);
225236
}
226237
// NOTE: this effectively drops ROOT node(s); should have 1 such node,
227238
// as the last one, but we don't have to care (probably some paths have
228239
// no root, for example)
229240
}
230-
if (tail == null) {
241+
if (next == null) {
231242
return EMPTY;
232243
}
233-
return tail;
234-
}
235244

236-
private static String _fullPath(JsonPointer tail, String segment)
237-
{
238-
if (tail == null) {
239-
StringBuilder sb = new StringBuilder(segment.length()+1);
240-
sb.append('/');
241-
_appendEscaped(sb, segment);
242-
return sb.toString();
245+
// And here the fun starts! We have the head, need to traverse
246+
// to compose full path String
247+
// final PointerSegment head = next;
248+
StringBuilder pathBuilder = new StringBuilder(approxLength);
249+
PointerSegment last = null;
250+
251+
for (; next != null; next = next.next) {
252+
// Let's find the last segment as well, for reverse traversal
253+
last = next;
254+
next.pathOffset = pathBuilder.length();
255+
pathBuilder.append('/');
256+
if (next.property != null) {
257+
_appendEscaped(pathBuilder, next.property);
258+
} else {
259+
pathBuilder.append(next.index);
260+
}
243261
}
244-
String tailDesc = tail._asString;
245-
StringBuilder sb = new StringBuilder(segment.length() + 1 + tailDesc.length());
246-
sb.append('/');
247-
_appendEscaped(sb, segment);
248-
sb.append(tailDesc);
249-
return sb.toString();
262+
final String fullPath = pathBuilder.toString();
263+
264+
// and then iteratively construct JsonPointer chain in reverse direction
265+
// (from innermost back to outermost)
266+
PointerSegment currSegment = last;
267+
JsonPointer currPtr = EMPTY;
268+
269+
for (; currSegment != null; currSegment = currSegment.prev) {
270+
if (currSegment.property != null) {
271+
currPtr = new JsonPointer(fullPath, currSegment.pathOffset,
272+
currSegment.property, currPtr);
273+
} else {
274+
int index = currSegment.index;
275+
currPtr = new JsonPointer(fullPath, currSegment.pathOffset,
276+
String.valueOf(index), index, currPtr);
277+
}
278+
}
279+
280+
return currPtr;
250281
}
251282

252283
private static void _appendEscaped(StringBuilder sb, String segment)
@@ -788,6 +819,32 @@ private static class PointerParent {
788819
}
789820
}
790821

822+
/**
823+
* Helper class used to contain a single segment when constructing JsonPointer
824+
* from context.
825+
*/
826+
private static class PointerSegment {
827+
public final PointerSegment next;
828+
public final String property;
829+
public final int index;
830+
831+
// Offset within external buffer, updated when constructing
832+
public int pathOffset;
833+
834+
// And we actually need 2-way traversal, it turns out so:
835+
public PointerSegment prev;
836+
837+
public PointerSegment(PointerSegment next, String pn, int ix) {
838+
this.next = next;
839+
property = pn;
840+
index = ix;
841+
// Ok not the cleanest thing but...
842+
if (next != null) {
843+
next.prev = this;
844+
}
845+
}
846+
}
847+
791848
/*
792849
/**********************************************************
793850
/* Support for JDK serialization (2.14+)

src/test/java/com/fasterxml/jackson/failing/JsonPointerOOME736Test.java renamed to src/test/java/com/fasterxml/jackson/core/jsonptr/JsonPointerOOME736Test.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.fasterxml.jackson.failing;
1+
package com.fasterxml.jackson.core.jsonptr;
22

33
import com.fasterxml.jackson.core.*;
44
import com.fasterxml.jackson.core.exc.StreamReadException;
@@ -7,8 +7,8 @@ public class JsonPointerOOME736Test extends BaseTest
77
{
88
// such as https://github.com/nst/JSONTestSuite/blob/master/test_parsing/n_structure_100000_opening_arrays.json
99
public void testDeepJsonPointer() throws Exception {
10-
int MAX_DEPTH = 100000;
11-
// Create nesting of 100k arrays
10+
int MAX_DEPTH = 120_000;
11+
// Create nesting of 120k arrays
1212
String INPUT = new String(new char[MAX_DEPTH]).replace("\0", "[");
1313
JsonParser parser = createParser(MODE_READER, INPUT);
1414
try {

0 commit comments

Comments
 (0)