Skip to content

JNI code safety issues in LibrustzcashJNIImpl and LibsodiumJNIImpl #16

@Federico2014

Description

@Federico2014

Description

During a comprehensive code review of the JNI native layer, multiple critical code quality and safety issues were identified in LibrustzcashJNIImpl.cpp and LibsodiumJNIImpl.cpp. These issues could lead to:

  • Memory leaks - Improper JNI array release on error paths
  • Undefined behavior - Null pointer dereference due to incorrect check ordering
  • Build failures - Unused headers causing compilation errors on macOS
  • Poor maintainability - C-style casts and inconsistent code patterns

Environment

Network: N/A (native library code)

Software Versions

OS: macOS 24.6.0, Linux
JVM: JDK 8+
Git Commit: 834f0da
Version: 1.0.0
Code: zksnark-java-sdk

Configuration

  • CMake 3.10.2+
  • Rust (cargo) 1.50.0+
  • Native library: libzksnarkjni.jnilib / libzksnarkjni.so

Expected Behavior

  1. JNI native code should properly release array elements on all error paths
  2. Null pointer checks should execute BEFORE function calls that dereference pointers
  3. C++ code should use modern C++ style (nullptr, reinterpret_cast, etc.)
  4. Build should succeed on all supported platforms (macOS x86_64/aarch64, Linux)

Actual Behavior

  1. Memory leaks on error paths - When GetByteArrayElements returns null for only some arrays, previously acquired arrays are not released
  2. Null check after use - In librustzcashToScalar, the null check executes after the function call, so it never protects against null dereference
  3. Build failures on macOS - Unused #include <iostream> causes 'iostream' file not found error
  4. Compilation errors with modern JDKs - Missing const_cast when releasing const jbyte* arrays

Frequency

  • Always (100%) - Code defects exist in all executions
  • Frequently (>50%)
  • Sometimes (10-50%)
  • Rarely (<10%)

Steps to Reproduce

Issue 1: Null Check After Function Call (librustzcashToScalar)

// Line 837-845 (original code)
const unsigned char * i = (const unsigned char *) env->GetByteArrayElements(input, nullptr);
unsigned char * r = (unsigned char *) env->GetByteArrayElements(result, nullptr);
librustzcash_to_scalar(i, r);           // Called FIRST
if (r == NULL || i == NULL) {           // Check AFTER - never executes!
    return;
}

Reproduction: Trigger GetByteArrayElements to return null (e.g., invalid array), the function will crash.

Issue 2: Memory Leak on Error Path

// Multiple functions (original code)
const unsigned char * s = reinterpret_cast<const unsigned char *>(env->GetByteArrayElements(seed, nullptr));
unsigned char * x = reinterpret_cast<unsigned char *>(env->GetByteArrayElements(xsk_master, nullptr));
if (s == NULL || x == NULL) {
    return;  // Leaks s if x is null, leaks x if s is null!
}

Reproduction: Pass one invalid array and one valid array - the valid array's native reference leaks.

Issue 3: Build Failure on macOS

cd cpp && mkdir build && cd build
cmake ..
make -j4
# Error: 'iostream' file not found

Additional Context

Possible Solution

The fix involves:

  1. Replace unused headers: <iostream><cstring> and <new>
  2. Fix null check ordering: Move null checks BEFORE function calls
  3. Add proper error cleanup: Release acquired arrays on all error paths with JNI_ABORT
  4. Use modern C++ casts: Replace C-style casts with reinterpret_cast, static_cast, const_cast
  5. Use nullptr instead of NULL
  6. Add const_cast for JNI array release: Required when releasing const jbyte* with JNI_ABORT
  7. Use new (std::nothrow): Prevent unhandled C++ exceptions in JNI code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions