Skip to content

Add ability for static search locations #41

Open
@tresf

Description

@tresf

Summary

The native-lib-loader should allow specifying a library in a static location for edge-case scenarios, usually imposed by security restrictions, constraints or sandboxing.

Edit: Static locations may also offer slight performance benefits as well, eliminating the need for unzipping (CPU/disk) activity, see discussion here.

Details

Technically, native-lib-loader allows for static search locations, but it requires implementing the JniExtractor class or overriding the behavior of the DefaultJniExractor class, as -- by design -- they both intend to perform an extract operation prior to loading a native library. This extraction operation doesn't work in all environments.

I'll quote a sister project -- JNA -- documentation, I think it words this problem well and explains as to why this can be needed...

  • When [...] classes are loaded, the native shared library [...] is loaded as well. An attempt is made to load it from the any paths defined in jna.boot.library.path (if defined), then the system library path using System.loadLibrary(java.lang.String), unless jna.nosys=true.
  • If not found, the appropriate library will be extracted from the class path (into a temporary directory if found within a jar file) and loaded from there, unless jna.noclasspath=true.
  • If your system has additional security constraints regarding execution or load of files (SELinux, for example), you should probably install the native library in an accessible location and configure your system accordingly, rather than relying on JNA to extract the library from its own jar file.

The last bullet is the point I'd like to focus on since this same use-case exists for native-lib-loader. For example, if a native library is intended to be distributed with a Java application that's distributed from the Apple AppStore, sandboxing is a hard-requirement, and like the aforementioned SELinux use-case, sandboxing can prevent loading a library from arbitrary locations (such as $TEMP), quoting an Apple employee on the Apple Developer forums:

I think you’ll run into problems there. Specifically, modern versions of the system prevent an app from referencing libraries outside of the app (other than system libraries). See Gatekeeper Changes in OS X v10.10.4 and Later in Technote 2206 OS X Code Signing In Depth

Furthermore, the JNA portion "unless jna.nosys=true", I find to be increasingly important as client environments may have identically named libraries in search paths that the client has little or no control over. This is a separate issue, but may be tackled as part of the same enhancement.

Workaround

Providing a stub extractor can handle this issue:

Click to see DefaultJniExtractorStub.java
/**
 * License: https://opensource.org/licenses/BSD-3-Clause
 */
package jssc;

import org.scijava.nativelib.DefaultJniExtractor;
import org.scijava.nativelib.NativeLibraryUtil;

import java.io.File;
import java.io.IOException;

/**
 * @author A. Tres Finocchiaro
 *
 * Stub <code>DefaultJniExtractor</code> class to allow native-lib-loader to conditionally
 * use a statically defined native search path <code>bootPath</code> when provided.
 */
public class DefaultJniExtractorStub extends DefaultJniExtractor {
    private File bootPath;
    private boolean useStub;

    /**
     * Default constructor
     */
    public DefaultJniExtractorStub(Class libraryJarClass) throws IOException {
        super(libraryJarClass);
        useStub = false;
    }

    /**
     * Force native-lib-loader to first look in the location defined as <code>bootPath</code>
     * prior to extracting a native library, useful for sandboxed environments.
     *  <code>
     *  NativeLoader.setJniExtractor(new DefaultJniExtractorStub(null, "/opt/nativelibs")));
     *  NativeLoader.loadLibrary("mylibrary");
     *  </code>
     */
    public DefaultJniExtractorStub(Class libraryJarClass, String bootPath) throws IOException {
        this(libraryJarClass);
        this.bootPath = new File(bootPath);

        if(bootPath != null) {
            File bootTest = new File(bootPath);
            if(bootTest.exists()) {
                // assume a static, existing directory will contain the native libs
                this.useStub = true;
            } else {
                System.err.println("WARNING " + DefaultJniExtractorStub.class.getCanonicalName() + ": Boot path " + bootPath + " not found, falling back to default extraction behavior.");
            }
        }
    }

    /**
     * If a <code>bootPath</code> was provided to the constructor and exists,
     * calculate the <code>File</code> path without any extraction logic.
     *
     * If a <code>bootPath</code> was NOT provided or does NOT exist, fallback on
     * the default extraction behavior.
     */
    @Override
    public File extractJni(String libPath, String libName) throws IOException {
        // Lie and pretend it's already extracted at the bootPath location
        if(useStub) {
            return new File(bootPath, NativeLibraryUtil.getPlatformLibraryName(libName));
        }
        // Fallback on default behavior
        return super.extractJni(libPath, libName);
    }

    @Override
    public void extractRegistered() throws IOException {
        if(useStub) {
            return; // no-op
        }
        super.extractRegistered();
    }
}

Usage:

+ NativeLoader.setJniExtractor(new DefaultJniExtractorStub(null, "/opt/libs"));
  NativeLoader.loadLibrary("mylibrary");

Caveats

Due to the library loading order in native-lib-loader, any System locations will be always be preferred, which can cause compatibility issues if the system was modified (probably warrants a separate bug report).

try {
// try to load library from classpath
System.loadLibrary(libName);
}
catch (final UnsatisfiedLinkError e) {
if (NativeLibraryUtil.loadNativeLibrary(jniExtractor, libName,
searchPaths)) return;
throw new IOException("Couldn't load library library " + libName, e);
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions