GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — bug: SessionContext.close() / DataFrame.close() race with concurrent JNI calls (use-after-free) · Issue #40 · apache/datafusion-java

bug: SessionContext.close() / DataFrame.close() race with concurrent JNI calls (use-after-free) #40

New issue
Copy link
New issue
Copy link
Open
#115
Open
bug: SessionContext.close() / DataFrame.close() race with concurrent JNI calls (use-after-free)#40
#115
Copy link
Assignees
andygrove
Labels
bugSomething isn't workingSomething isn't working

Description

@andygrove
andygrove
opened on May 13, 2026
Issue body actions

Describe the bug

SessionContext and DataFrame hold their native pointer in a plain
long nativeHandle. Every public method follows the pattern:

if (nativeHandle == 0) throw new IllegalStateException(...);
someNativeCall(nativeHandle, ...);

…and close() does:

if (nativeHandle != 0) {
    closeSessionContext(nativeHandle);
    nativeHandle = 0;
}

If thread A is mid-method on a context and thread B calls close() on
the same context, the read in A and the write+free in B race:

  1. A reads nativeHandle (non-zero), passes it to JNI.
  2. B sets nativeHandle = 0 and the Rust side drops the Box.
  3. A's JNI call dereferences a freed *const SessionContext → UAF.

Even the nativeHandle == 0 guard is not safe — it's a TOCTOU. The
same shape applies to DataFrame (each method reads nativeHandle,
then calls JNI).

To Reproduce

No reproducer exists yet. A two-thread test running a tight loop of
ctx.sql(...).count() against ctx.close() on an ASan-instrumented
native build would surface it deterministically; can write one if the
maintainers want to see it first.

Expected behavior

One of:

  1. Document the UB explicitly. Today the Javadoc says contexts are
    "not thread-safe" and warns about concurrent sql / register* /
    close, but the consequence ("can produce a use-after-free") is
    already spelled out — so it's arguably already expected, and this
    issue is just a tracking marker so a future maintainer doesn't get
    surprised.
  2. Atomic handle + reference count. Use AtomicLong for the
    handle and reference-count on the Rust side so close() defers until
    in-flight calls drain. Closer to what JNA-style bindings do.
  3. Per-instance lock. Wrap every JNI call in a synchronized
    block. Simplest, but kills any potential concurrency on independent
    read-only operations.

Additional context

Not a regression — the documented contract already excludes concurrent
use. Filing for visibility ahead of the first multi-threaded user (a
server, a Flink/Spark integration, etc.) hitting it in production
rather than dev. Cross-references SessionContext.java and
DataFrame.java; same shape will need attention any time a new
long-lived handle is added.

Activity

  1. andygrove
    added
    bugSomething isn't workingSomething isn't working
    on May 13, 2026
  2. djanand commented on May 28, 2026

    @djanand
    djanand
    on May 28, 2026
    More actions

    @andygrove - Was doing some digging and not sure if its worth a thought, but what if we use readwritelock instead of AtomicLong (Option 2)

    ReadWriteLock:

    • Java tracks in-flight calls (read lock holders)
    • close() blocks - it acquires the write lock, which waits until every in-flight call releases its read lock, then frees immediately
    • Check and use happen under the same lock with no TOCTOU gap

    AtomicLong + Rust Arc:

    • Rust tracks in-flight calls (Arc refcount)
    • close() returns immediately, it just zeros the AtomicLong and drops its Arc reference. If in-flight calls hold Arc clones, the actual free happens later when the last clone drops inside Rust
    • There IS still a TOCTOU gap on the Java side between reading the AtomicLong and Rust cloning the Arc, close() could free the Box<Arc>.
  3. andygrove
    self-assigned this
    on Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

  • @andygrove
    andygrove

Labels

bugSomething isn't workingSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    You are visitor 01402503 — GeezerVison does not store the pages you look up.
    Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.