Skip to content

Commit c989076

Browse files
authored
Fix StackOverflowError by deeply nested server-timestamps. (#4771)
* Fix StackOverflowError by deeply nested server-timestamps. * Add comments about the Thread usage.
1 parent 8929123 commit c989076

File tree

3 files changed

+87
-0
lines changed

3 files changed

+87
-0
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Unreleased
22
* [feature] Add support for disjunctions in queries (`OR` queries).
3+
* [fixed] Fixed stack overflow caused by deeply nested server timestamps (#4702).
34

45
# 24.4.4
56
* [changed] Relaxed certain query validations performed by the SDK (#4231).

firebase-firestore/src/main/java/com/google/firebase/firestore/model/ServerTimestamps.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ public static Value valueOf(Timestamp localWriteTime, @Nullable Value previousVa
6262
.putFields(TYPE_KEY, encodedType)
6363
.putFields(LOCAL_WRITE_TIME_KEY, encodeWriteTime);
6464

65+
// We should avoid storing deeply nested server timestamp map values, because we never use the
66+
// intermediate "previous values".
67+
// For example:
68+
// previous: 42L, add: t1, result: t1 -> 42L
69+
// previous: t1, add: t2, result: t2 -> 42L (NOT t2 -> t1 -> 42L)
70+
// previous: t2, add: t3, result: t3 -> 42L (NOT t3 -> t2 -> t1 -> 42L)
71+
// previous: t3, add: t4, result: t4 -> 42L (NOT t4 -> t3 -> t2 -> t1 -> 42L)
72+
// `getPreviousValue` recursively traverses server timestamps to find the least recent Value.
73+
previousValue =
74+
isServerTimestamp(previousValue) ? getPreviousValue(previousValue) : previousValue;
6575
if (previousValue != null) {
6676
mapRepresentation.putFields(PREVIOUS_VALUE_KEY, previousValue);
6777
}

firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteLocalStoreTest.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,29 @@
3232
import static java.util.Collections.emptyList;
3333
import static java.util.Collections.singletonList;
3434

35+
import com.google.firebase.Timestamp;
3536
import com.google.firebase.firestore.FieldValue;
3637
import com.google.firebase.firestore.core.Query;
38+
import com.google.firebase.firestore.model.DocumentKey;
3739
import com.google.firebase.firestore.model.FieldIndex;
40+
import com.google.firebase.firestore.model.FieldPath;
41+
import com.google.firebase.firestore.model.ObjectValue;
42+
import com.google.firebase.firestore.model.ServerTimestamps;
43+
import com.google.firebase.firestore.model.mutation.FieldMask;
44+
import com.google.firebase.firestore.model.mutation.FieldTransform;
45+
import com.google.firebase.firestore.model.mutation.PatchMutation;
46+
import com.google.firebase.firestore.model.mutation.Precondition;
47+
import com.google.firebase.firestore.model.mutation.ServerTimestampOperation;
48+
import com.google.firestore.v1.Value;
49+
import java.util.ArrayList;
3850
import java.util.Arrays;
3951
import java.util.Collection;
4052
import java.util.Collections;
53+
import java.util.HashMap;
54+
import java.util.HashSet;
55+
import java.util.List;
56+
import java.util.Map;
57+
import java.util.concurrent.atomic.AtomicReference;
4158
import org.junit.Test;
4259
import org.junit.runner.RunWith;
4360
import org.robolectric.RobolectricTestRunner;
@@ -290,4 +307,63 @@ public void testIndexesServerTimestamps() {
290307
assertOverlayTypes(keyMap("coll/a", CountingQueryEngine.OverlayType.Set));
291308
assertQueryReturned("coll/a");
292309
}
310+
311+
@Test
312+
public void testDeeplyNestedServerTimestamps() {
313+
Timestamp timestamp = Timestamp.now();
314+
Value initialServerTimestamp = ServerTimestamps.valueOf(timestamp, null);
315+
Map<String, Value> fields =
316+
new HashMap<String, Value>() {
317+
{
318+
put("timestamp", ServerTimestamps.valueOf(timestamp, initialServerTimestamp));
319+
}
320+
};
321+
FieldPath path = FieldPath.fromSingleSegment("timestamp");
322+
FieldMask mask =
323+
FieldMask.fromSet(
324+
new HashSet<FieldPath>() {
325+
{
326+
add(path);
327+
}
328+
});
329+
FieldTransform fieldTransform =
330+
new FieldTransform(path, ServerTimestampOperation.getInstance());
331+
List<FieldTransform> fieldTransforms =
332+
new ArrayList<FieldTransform>() {
333+
{
334+
add(fieldTransform);
335+
}
336+
};
337+
// The purpose of this test is to ensure that deeply nested server timestamps do not result in
338+
// a stack overflow error. Below we use a `Thread` object to create a large number of mutations
339+
// because the `Thread` class allows us to specify the maximum stack size.
340+
AtomicReference<Throwable> error = new AtomicReference<>();
341+
Thread thread =
342+
new Thread(
343+
Thread.currentThread().getThreadGroup(),
344+
() -> {
345+
try {
346+
for (int i = 0; i < 1000; ++i) {
347+
writeMutation(
348+
new PatchMutation(
349+
DocumentKey.fromPathString("some/object/for/test"),
350+
ObjectValue.fromMap(fields),
351+
mask,
352+
Precondition.NONE,
353+
fieldTransforms));
354+
}
355+
} catch (Throwable e) {
356+
error.set(e);
357+
}
358+
},
359+
/* name */ "test",
360+
/* stackSize */ 1024 * 1024);
361+
try {
362+
thread.start();
363+
thread.join();
364+
} catch (InterruptedException e) {
365+
throw new AssertionError(e);
366+
}
367+
assertThat(error.get()).isNull();
368+
}
293369
}

0 commit comments

Comments
 (0)