Skip to content

Using Memory#close can throw NPE if cleanable not initialized #1446

@dbwiddis

Description

@dbwiddis
  1. Version of JNA and related jars
    5.12.0

  2. Version and vendor of the java virtual machine
    OpenJDK 64-Bit Server VM Homebrew (build 18.0.1.1+0, mixed mode, sharing)

  3. Operating system
    macOS 12.4 (Monterey)

  4. System architecture (CPU type, bitness of the JVM)
    64-bit Intel Core i9

  5. Complete description of the problem
    In Remove use of finalizers in JNA and improve concurrency for Memory, CallbackReference and NativeLibrary #1402, a close() method was added to Memory, which also implemented Closeable (which extends AutoCloseable on newer JDKs). The implementation assumes the private instance variable cleanable still exists:

    /** Free the native memory and set peer to zero */
    public void close() {
    peer = 0;
    cleanable.clean();
    }

However, cleanable is of type CleanerRef which extends PhantomReference and thus calling it in code does not imply reachability. When the Cleaner does its work, the reference is removed from the queue and the instance variable becomes null.

Executing close() in this situation can result in an NPE.

  1. Steps to reproduce

I updated my project to use try-with-resources blocks on all of my Memory allocations, and added AutoCloseable wrapper classes for (nearly all of) my uses of classes which extend ByReference and Structure. One such example:

class CloseableLUID extends LUID implements AutoCloseable {
    @Override
    public void close() {
        Util.freeMemory(getPointer());
    }
}

and the Util class:

public static void freeMemory(Pointer p) {
    if (p instanceof Memory) {
        ((Memory) p).close();
    }
}

Then, in a method:

try (CloseableLUID luid = new CloseableLUID()) {
    // ...
} // exception occurs here when try-with-resources calls close()

Hundreds of similar implementations like this worked well, but this one fails.

Possibly/probably related, it's in an initializer of one of the first classes to be instantiated, and may very well have been the very first reference in the cleaner queue, giving it a significant advantage in a race condition.

=> java.lang.ExceptionInInitializerError
   oshi.SystemInfo.createOperatingSystem(SystemInfo.java:104)
   oshi.util.Memoizer$1.get(Memoizer.java:87)
   oshi.SystemInfo.getOperatingSystem(SystemInfo.java:98)
   oshi.SystemInfoTest.main(SystemInfoTest.java:102)
   oshi.SystemInfoTest.testPlatformEnum(SystemInfoTest.java:87)
   [...]
 Caused by: java.lang.NullPointerException
   com.sun.jna.Memory.close(Memory.java:185)
   oshi.util.Util.freeMemory(Util.java:112)
   oshi.jna.Struct$CloseableLUID.close(Struct.java:193)
   oshi.software.os.windows.WindowsOperatingSystem.enableDebugPrivilege(WindowsOperatingSystem.java:485)
   oshi.software.os.windows.WindowsOperatingSystem.<clinit>(WindowsOperatingSystem.java:124)

My tests seem to be running fine by removing this one mapping, so this is likely an exceedingly rare race condition.

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