Repository navigation
[java-firestore] UserDataConverter throws IndexOutOfBoundsException on an empty __binary__ value #14634
Description
Issue Details
UserDataConverter.decodeBsonBinary reads the subtype byte with bytes.byteAt(0) without checking that the __binary__ bytes are non-empty, so an empty __binary__ value throws ArrayIndexOutOfBoundsException (an IndexOutOfBoundsException) from DocumentSnapshot.get(...) instead of a decode error naming the field:
Lines 414 to 425 in 9de611f
| static Blob decodeBsonBinary(MapValue mapValue) { | |
| ByteString bytes = | |
| mapValue.getFieldsMap().get(MapType.RESERVED_BSON_BINARY_KEY).getBytesValue(); | |
| // Note: A byte is interpreted as a signed 8-bit value. Since values larger than 127 have a | |
| // leading '1' bit, simply casting them to integer results in sign-extension and lead to a | |
| // negative integer value. For example, the byte `0x80` casted to `int` results in `-128`, | |
| // rather than `128`, and the byte `0xFF` casted to `int` will be `-1` rather than `255`. | |
| // Since we want the `subtype` to be an unsigned byte, we need to perform 0-extension (rather | |
| // than sign-extension) to convert it to an int. | |
| int subtype = bytes.byteAt(0) & 0xFF; | |
| return Blob.createBsonBinary(subtype, bytes.substring(1)); | |
| } |
Reachability. Writing these forms through the Firestore API is refused with INVALID_ARGUMENT by the emulator (587.0.0-emulators) and by both a Standard-edition and an Enterprise-edition database, so a document written through that API should never carry them (measured 2026-10-03). Writes through the Enterprise edition's MongoDB-compatible API were not tried. Even if the input is unreachable today, the decoder assumes something it does not check, and the consequence is an unchecked runtime exception from a getter, where a FirestoreException naming the malformed value would be expected. A length check before byteAt(0) would fix it.
Found while building an Apache Flink source on google-cloud-firestore (flink-gcp/flink-connector-gcp).
Environment
- OS Type and Version: macOS 26 (arm64)
- Java Version and JDK Vendor: Eclipse Temurin 17.0.20
The reproducer is offline: it builds the snapshot from a Document proto with com.google.cloud.firestore.Internal, the way the client does for a server response, and sends nothing.
Dependencies
- Libraries-Bom: 26.90.0
- google-cloud-firestore 3.49.0
- Gax: 2.87.0
- Auth: 1.54.0
The affected code is identical in 3.49.0 and on main.
Reproducer
package repro;
import com.google.cloud.NoCredentials;
import com.google.cloud.Timestamp;
import com.google.cloud.firestore.DocumentSnapshot;
import com.google.cloud.firestore.Firestore;
import com.google.cloud.firestore.FirestoreOptions;
import com.google.cloud.firestore.Internal;
import com.google.cloud.firestore.Query;
import com.google.cloud.firestore.spi.v1.GrpcFirestoreRpc;
import com.google.firestore.v1.Document;
import com.google.firestore.v1.MapValue;
import com.google.firestore.v1.Value;
import com.google.protobuf.ByteString;
/** Offline: builds snapshots from protos as the client would receive them; nothing is sent. */
public class DecodeDefects {
public static void main(String[] args) throws Exception {
FirestoreOptions options =
FirestoreOptions.newBuilder()
.setProjectId("demo")
.setCredentials(NoCredentials.getInstance())
.build();
Firestore db = options.getService();
// 1. An __int__ beyond 32 bits is narrowed silently.
long stored = (1L << 40) + 5;
DocumentSnapshot wide = snapshot(options, reserved("__int__", Value.newBuilder().setIntegerValue(stored).build()));
System.out.println("stored __int__: " + stored);
System.out.println("decoded value: " + wide.get("k"));
Query query = db.collection("c").orderBy("k").startAfter(wide);
System.out.println("cursor value: " + query.toProto().getStructuredQuery().getStartAt().getValues(0).getMapValue());
// 2. An empty __binary__ throws instead of failing with a decode error.
DocumentSnapshot empty = snapshot(options, reserved("__binary__", Value.newBuilder().setBytesValue(ByteString.EMPTY).build()));
try {
System.out.println("decoded value: " + empty.get("k"));
} catch (RuntimeException e) {
System.out.println("empty __binary__ threw: " + e);
}
System.exit(0);
}
static Value reserved(String key, Value value) {
return Value.newBuilder().setMapValue(MapValue.newBuilder().putFields(key, value)).build();
}
static DocumentSnapshot snapshot(FirestoreOptions options, Value value) throws Exception {
Timestamp now = Timestamp.now();
Document document =
Document.newBuilder()
.setName("projects/demo/databases/(default)/documents/c/x")
.setCreateTime(now.toProto())
.setUpdateTime(now.toProto())
.putFields("k", value)
.build();
try (GrpcFirestoreRpc rpc = new GrpcFirestoreRpc(options)) {
return new Internal(options, rpc).snapshotFromProto(now, document);
}
}
}Only the second case concerns this issue; the first is reported in #14633.
Logs and Stack Trace
stored __int__: 1099511627781
decoded value: Int32Value{value=5}
cursor value: fields {
key: "__int__"
value {
integer_value: 5
}
}
empty __binary__ threw: java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
Behavior
Deterministic: any __binary__ whose bytes are empty.
Activity
Aayush10016 commentedon Oct 19, 2026
I traced the empty binary path to decodeBsonBinary and found an offline test seam in ExtendedTypesTest. For a malformed proto that Firestore itself rejects on write, is FirestoreException.forInvalidArgument with a message naming binary the error contract you would want from the SDK? I wanted to confirm that behavior before preparing a focused guard and regression test.