Skip to content

fix: reject non-Value params in Connection.execute instead of crashing the JVM - #12

Merged
adsharma merged 2 commits into
mainfrom
fix/reject-non-value-params
Aug 1, 2026
Merged

fix: reject non-Value params in Connection.execute instead of crashing the JVM#12
adsharma merged 2 commits into
mainfrom
fix/reject-non-value-params

Conversation

@adsharma

Copy link
Copy Markdown
Contributor

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

The problem

Connection.execute is declared execute(PreparedStatement, Map<String, Value>) 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 — hands ordinary Java objects to a reinterpret_cast, and the process dies with SIGSEGV.

The fix

Implements both options proposed in the issue:

Option A — Accept boxed Java values. The type-conversion ladder from lbugValueCreateValue (which already handles Boolean, Byte, Short, Integer, Long, BigInteger, Float, Double, BigDecimal, String, InternalID, UUID, LocalDate, Instant, Duration) is reused in bindJavaParamsToPreparedStatement so Map.of(\"p_id\", uuid) behaves the way a JVM caller expects.

Option B — Guard and throw. If the value is neither a Value nor a known boxed type, an IllegalArgumentException is thrown naming the parameter and the offending class, instead of crashing the JVM.

Additional changes

  • Widened Connection.execute signature from Map<String, Value> to Map<String, ?> so non-erased callers can pass raw types directly
  • Added early-return guard in lbugConnectionExecute after param binding to safely propagate the pending Java exception
  • Added 7 tests: raw String/Long/Double/Boolean, mixed raw+Value, unsupported type throws IllegalArgumentException, Value-wrapped regression

Fixes: #11

adsharma added 2 commits July 30, 2026 10:42
…ert boxed Java types

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

Option A: Accept boxed Java values (String, Long, UUID, etc.) by reusing the
type-conversion ladder from lbugValueCreateValue in bindJavaParamsToPreparedStatement.

Option B: Guard unrecognized types with IllegalArgumentException instead of
reinterpret_cast crash.

- Moves the JNI type-conversion ladder into a reusable javaObjectToValue helper
- Adds throwIllegalArgumentException and getClassName JNI helpers
- Widens Connection.execute signature from Map<String, Value> to Map<String, ?>
- Adds early-return guard in lbugConnectionExecute after param binding
- Adds 7 new tests: raw String/Long/Double/Boolean, mixed raw+Value,
  unsupported type throws, Value-wrapped regression

Fixes: #11
Move the boxed-Java-type-to-Value coercion from the JNI binding into
Connection.execute on the Java side. The new coerceParams helper builds
a LinkedHashMap<String, Value> from the user-supplied Map<String, ?>,
preserving order and dispatching each entry through a single
pattern-matching switch (JEP 441, GA in Java 21). Already-wrapped
Value instances pass through; boxed types are auto-converted via
Value(T); unsupported types and null surface as IllegalArgumentException
with clear messages.

This lets the native binding stay strictly typed (no
@SuppressWarnings("unchecked") or raw-type cast in Connection.execute)
and removes ~135 lines of dead code from lbug_java.cpp:
  - javaObjectToValue (the conversion ladder)
  - getClassName (only used to format the dead-branch error)
  - throwIllegalArgumentException (only called from the dead branch)
  - the else branch in bindJavaParamsToPreparedStatement

bindJavaParamsToPreparedStatement collapses to a direct lbug_value_clone
with a defensive IsInstanceOf check as a contract guard against any
future API bypass.

Bumps Java compile target from 1.8/11 to 21 in CMakeLists.txt,
build.gradle, and the published POM. Adds kotlinOptions.jvmTarget = 21
so the Kotlin plugin does not refuse the build with "inconsistent JVM
target" between compileTestJava and compileTestKotlin. CI matrix is
already on 21 (java-workflow.yml, build-and-deploy.yml) so no CI churn.
Downstream consumers will need Java 21+ at runtime.

Adds two tests:
  - executeWithNullParamThrows: locks in the key-naming error
  - executeWithValueCreateNullParam: verifies the happy SQL-NULL path
@adsharma

Copy link
Copy Markdown
Contributor Author

@alvorithm could you please test?

@alvorithm

Copy link
Copy Markdown

Tested. It fixes the crash, including the original Clojure caller, and I found nothing broken. Details below.

Build: JAVA_HOME=/usr/lib/jvm/java-25-openjdk-amd64 make java NUM_THREADS=14 at ladybug 3abdb0653 with tools/java_api at c0ea430, producing tools/java_api/build/lbug_java.jar and liblbug_java_native.so_linux_amd64. Debian 13, OpenJDK 25.0.3, x86-64. (make java needs JAVA_HOME pointing at a JDK; with only /usr/bin/java on PATH the CMake find_package(JNI) fails with missing: JAVA_INCLUDE_PATH JAVA_INCLUDE_PATH2 AWT. Not a problem with this PR, just a note for anyone reproducing.)

Before and after, same class, same source

Repro.java from the issue, unchanged, compiled and run against each binding:

binding result
released com.ladybugdb:lbug 0.19.0 (pins a0e7728) SIGSEGV, exit=134, "The crash happened outside the Java Virtual Machine in native code"
this branch (c0ea430) returned, isSuccess=true / SURVIVED - no crash, exit=0

Caller shapes from #11

case result detail
issue #11 reproducer, unchecked cast, UUID + String ok no crash, SET applied
Map.of("p_id", uuid, "p_name", str), no cast ok compiles against Map<String, ?>, SET applied
single bare String parameter ok no crash
regression: Value-wrapped params ok still works
mixed raw and Value in one map ok SET applied
unsupported type (ArrayList) raises ok IllegalArgumentException: Parameter 'p_name' has unsupported type java.util.ArrayList. Accepted types: ...
null value raises ok IllegalArgumentException: Parameter 'p_name' is null; use Value.createNull() to bind SQL NULL.
connection usable after a rejected bind ok the next execute on the same PreparedStatement succeeds

That last one is the case I most wanted to check, since the ExceptionCheck early return in lbugConnectionExecute leaves a PreparedStatement that was partially bound. It recovers cleanly.

The original caller

The report came from Clojure, where maps are java.util.Map and generics are not enforced, so nothing stopped the bad call. Against this branch:

case result detail
Clojure map, java.util.UUID + String ok isSuccess=true, SET applied
Clojure keyword value IllegalArgumentException unsupported type clojure.lang.Keyword
Clojure vector value IllegalArgumentException unsupported type clojure.lang.PersistentVector

Worth noting the error message names the offending class, which is what makes this actionable from a non-Java caller: clojure.lang.Keyword tells you immediately what to wrap.

Your test suite

make javatest on the branch: 136 tests, 0 failures, 0 errors, 0 skipped. All 12 PreparedStatementTest cases pass, including the 7 new ones.

One thing worth a deliberate decision

The PR raises the Java baseline from 8/11 to 21 (CMAKE_JAVA_COMPILE_FLAGS, sourceCompatibility/targetCompatibility, the Kotlin jvmTarget, and the published POM java.version / maven.compiler.*), because coerceParam uses a pattern-matching switch. That is a consumer-visible change to the published artifact and a larger decision than the crash fix riding with it: anyone on a Java 11 or 17 toolchain loses the ability to consume com.ladybugdb:lbug at all.

The same coercion as an if/instanceof chain would hold the old baseline, at the cost of the O(1) dispatch the comment argues for. For a method that binds a handful of parameters per call, I would guess the dispatch difference is not measurable next to the JNI transition and the query itself, but that is your call and I have not measured it. Flagging it only because it is easy to merge without noticing.

Not blocking on my side. Happy to re-test if you change anything.

@adsharma
adsharma merged commit 0111e0a into main Aug 1, 2026
3 checks passed
@adsharma
adsharma deleted the fix/reject-non-value-params branch August 1, 2026 01:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants