Skip to content

Jp2kDecoderAsync JSON-parses evaluateJavaScriptAsync results without empty-string guard  #133

@jim-daf

Description

@jim-daf

Description

I noticed that Jp2kDecoderAsync calls evaluateJavaScriptAsync in five places but only the WASM-init path (L166) guards against unexpected results via a sentinel check. The other four call sites - precache, executeGetSize, executeDecodeImage, and getMemoryUsage - immediately pass the raw result into JSONObject(), which throws an opaque JSONException ("End of input at character 0") if the engine returns an empty string "".

The Jetpack JavaScriptEngine silently coerces non-string JS expression results (numbers, booleans, objects, undefined) to "". Since this is the async/callback variant (as opposed to Jp2kDecoder.kt), the confusing JSONException surfaces via callback.onError, making it especially hard to diagnose.

Evidence

1. WASM init correctly guards with a sentinel

In Jp2kDecoderAsync.kt (L162-L167), loadWasm checks against INTERNAL_RESULT_SUCCESS:

val resultFuture = isolate.evaluateJavaScriptAsync(script) // L162
try {
    val result = resultFuture.get()                         // L165
    if (result != INTERNAL_RESULT_SUCCESS) {                // L166
        throw IllegalStateException("WASM instantiation failed.")
    }

An empty string is correctly caught here because "" != "1".

2. precache parses JSON on a potentially empty result

In Jp2kDecoderAsync.kt (L215-L219):

val resultFuture = isolate.evaluateJavaScriptAsync(script)   // L215
val result = resultFuture.get()                               // L216

if (result != INTERNAL_RESULT_SUCCESS) {                      // L218
    val root = JSONObject(result)  // JSONException if ""     // L219

If the engine returns "", it enters the != INTERNAL_RESULT_SUCCESS branch and JSONObject("") throws JSONException.

3. executeGetSize parses JSON directly

In Jp2kDecoderAsync.kt (L303-L307):

val resultFuture = isolate.evaluateJavaScriptAsync(script)                    // L303
val jsonResult =
    resultFuture.get() ?: throw IllegalStateException("Result Future is null") // L305
val root = JSONObject(jsonResult)  // JSONException if ""                     // L307

The null check on L305 does not catch "" - only null.

4. executeDecodeImage parses JSON directly

In Jp2kDecoderAsync.kt (L766-L772):

val resultFuture = isolate.evaluateJavaScriptAsync(script)                    // L766
val jsonResult =
    resultFuture.get() ?: throw IllegalStateException("Result Future is null") // L770
val root = JSONObject(jsonResult)  // JSONException if ""                     // L772

Same pattern: null check present, empty-string check absent.

5. getMemoryUsage parses JSON directly

In Jp2kDecoderAsync.kt (L912-L916):

val resultFuture = isolate.evaluateJavaScriptAsync("globalThis.getMemoryUsage()") // L912
val jsonResult =
    resultFuture.get() ?: throw IllegalStateException("Result Future is null")     // L915
val root = JSONObject(jsonResult)  // JSONException if ""                          // L916

Why this may matter

  • Four of five evaluateJavaScriptAsync call sites would surface JSONException with "End of input at character 0" rather than a descriptive error, making diagnosis difficult.
  • Since this is the async variant, errors arrive via callback.onError where the caller has less context about which stage failed.
  • The WASM-init path (L166) already demonstrates the correct pattern of checking against a known sentinel before attempting JSON parsing.

Suggested hardening

Add an empty-string check before JSON parsing, consistent with the sentinel pattern already used in loadWasm:

val jsonResult = resultFuture.get()
if (jsonResult.isNullOrEmpty()) {
    throw IllegalStateException("JS engine returned empty result - expression may have evaluated to a non-string type")
}
val root = JSONObject(jsonResult)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions