Skip to content

Reject non-Value parameters in Connection.execute instead of crashing the JVM #11

Description

@alvorithm

I hit this while writing an Arrow-based bulk loader for a JVM application: my long-running dev backend died, with no exception and no stack unwinding. Verified against com.ladybugdb/lbug 0.19.0 (current release on Maven Central) on Linux x86-64 / OpenJDK 25.0.3. The code below is ladybug-java @ a0e7728 — the tip of main, and the commit that ladybug v0.19.0 pins at tools/java_api.

The problem

Connection.execute is declared execute(PreparedStatement, Map<String, Value>) (src/main/java/com/lbugdb/Connection.java:116), and the JNI layer trusts that declaration absolutely. Any caller that reaches it through erasure — a raw or unchecked Map, reflection, or a JVM language that does not enforce Java generics (my case: Clojure) — hands ordinary Java objects to a reinterpret_cast, and the process dies.

Map<String, Object> raw = new HashMap<>();
raw.put("p_id", UUID.fromString("11111111-1111-1111-1111-111111111111"));
raw.put("p_name", "hello");

@SuppressWarnings("unchecked")
Map<String, Value> params = (Map<String, Value>) (Map<?, ?>) raw;

try (PreparedStatement ps = conn.prepare("MATCH (n:T {id: $p_id}) SET n.name = $p_name;")) {
    conn.execute(ps, params);   // process dies here
}
# A fatal error has been detected by the Java Runtime Environment:
#  SIGSEGV (0xb) at pc=0x00007fc0f4ba89b3
# Problematic frame:
# C  [liblbug_java_native....so+0x29a9b3]  lbug_value_clone+0x23
#
siginfo: si_signo: 11 (SIGSEGV), si_code: 128 (SI_KERNEL), si_addr: 0x0000000000000000

j  com.ladybugdb.Native.lbugConnectionExecute(Lcom/ladybugdb/Connection;Lcom/ladybugdb/PreparedStatement;Ljava/util/Map;)Lcom/ladybugdb/QueryResult;+0
j  com.ladybugdb.Connection.execute(Lcom/ladybugdb/PreparedStatement;Ljava/util/Map;)Lcom/ladybugdb/QueryResult;+7

One unwrapped entry suffices — a bare String for a single parameter crashes identically. Wrapping the same values works correctly:

Map.of("p_id", new Value(uuid), "p_name", new Value("hello"))

Diagnostic

bindJavaParamsToPreparedStatement passes each map value straight to getValue (src/jni/lbug_java.cpp:516):

auto* clonedValue = lbug_value_clone(getValue(env, value));

but getValue (src/jni/lbug_java.cpp:381) reads a field that only exists on Value, with no type check:

lbug_value* getValue(JNIEnv* env, jobject thisValue) {
    jlong fieldValue = env->GetLongField(thisValue, J_C_Value_F_v_ref);
    uint64_t address = static_cast<uint64_t>(fieldValue);
    return reinterpret_cast<lbug_value*>(address);
}

GetLongField against something that is not a Value returns whatever occupies that field offset, and lbug_value_clone then dereferences it.

Line 516 looks like the only getValue call site fed by an unconstrained user object. The other 38 either take the receiver of a Value instance method, or an element of a Value[] (Java_com_ladybugdb_Native_lbugCreateList___3Lcom_ladybugdb_Value_2, src/jni/lbug_java.cpp:1623), where the JVM's array-store check already guarantees the type.

Proposed fix

Option A — accept boxed Java values. Java_com_ladybugdb_Native_lbugValueCreateValue (src/jni/lbug_java.cpp:1485) already converts Boolean, Byte, Short, Integer, Long, BigInteger, Float, Double, BigDecimal, String, InternalID, UUID, LocalDate, Instant and Duration through an IsInstanceOf ladder. Reusing that ladder in bindJavaParamsToPreparedStatement for values that are not already a Value makes Map.of("p_id", uuid) behave the way a JVM caller expects, and leaves the wrapped form working exactly as before. To be reachable from Java rather than only through erasure, this wants the signature widened to Map<String, ?> (or an overload) — happy to follow whichever you prefer.

Option B — guard and throw. env->IsInstanceOf(value, J_C_Value) before getValue, raising IllegalArgumentException naming the parameter and the offending class. Keeps the current contract, turns a JVM kill into a catchable error.

Value itself already validates correctly — new Value(new ArrayList<String>()) raises java.lang.RuntimeException: Type of value is not supported in value_create_value (src/jni/lbug_java.cpp:1562) — so only the unwrapped path is problematic.

Tests

  1. src/test/java/com/lbugdb/PreparedStatementTest.java: bind a raw UUID, String and Long through an unchecked map and assert the documented behaviour (converted under option A, IllegalArgumentException under option B).
  2. A regression case asserting the wrapped Value path is unchanged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions