Skip to content

[java-firestore] UserDataConverter throws IndexOutOfBoundsException on an empty __binary__ value #14634

Open

Description

@laughingman7743

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:

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

  1. Aayush10016 commented on Oct 19, 2026

    @Aayush10016

    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.

  2. added
    type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.
    priority: p2Moderately-important priority. Fix may not be included in next release.
    on Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

priority: p2Moderately-important priority. Fix may not be included in next release.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions