load native libraries directly from application image
What changed, and why it matters
This commit changes how Sparrow Wallet bundles and loads native libraries (small pieces of platform-specific code used for things like USB hardware wallets and camera scanning). Instead of extracting these libraries from inside JAR files at runtime, it now copies them directly into the application's lib/ folder during the build and tells the app to load them from there. This is a build and deployment refactor. It is not obviously a security fix, but it touches sensitive areas: native library loading paths, file permissions, and JNA/JNI configuration. There is no direct evidence in the commit that this fixes a known vulnerability.
Treat this as a build/deployment refactor with security-adjacent implications. Review that `extractNativeLibraries` preserves correct file permissions and does not introduce path traversal or library injection risks. Verify that the runtime path override cannot be influenced by environment variables or untrusted input. If this commit is suspected to address a vulnerability, look for an accompanying advisory or follow-up commit; the current diff alone does not establish security relevance.
Security signals we found
Native library loading path changed from JAR extraction to filesystem directory
Build task sets unix permissions on extracted native libraries
java.library.path and JNA library paths are overridden at runtime based on java.home
Bwt JNI library loading now prefers System.load with absolute path before fallback extraction
No explicit security rationale, CVE reference, or vulnerability description in commit message or diff
Evidence from the diff
The patch removes a Gradle processResources step that deleted non-matching native resources, adds --exclude-resources rules to jlink to strip bundled native libraries from the runtime image, and introduces an extractNativeLibraries build task that copies project-owned, JavaFX, and third-party native libraries (JNA, argon2-jvm, hid4java, openpnp-capture-java, jSerialComm, usb4java, jzbar) into build/image/lib. At runtime, SparrowWallet.main sets jna.boot.library.path, jna.library.path, jSerialComm.library.path, org.usb4java.LibraryName, and java.library.path to java.home/lib when running from a jpackage image. Bwt.initialize now prefers System.load(java.home/lib/<libbwt_jni.*>) and falls back to NativeUtils.loadLibraryFromJar. AppController now references SparrowWallet.JPACKAGE_APP_PATH instead of a local constant.
Changed components
build.gradle (jlink options, extractNativeLibraries task)SparrowWallet.java (runtime native library path configuration)Bwt.java (bwt_jni native library loading)AppController.java (jpackage app path constant relocation)Inspect captured patch +148 / −26
diff --git a/build.gradle b/build.gradle
index b5e0c78..83b0ba4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -124,14 +124,6 @@ compileJava {
}
}
-processResources {
- doLast {
- delete fileTree("$buildDir/resources/main/native").matching {
- exclude "${osName}/${osArch}/**"
- }
- }
-}
-
test {
useJUnitPlatform()
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--enable-native-access=ALL-UNNAMED"]
@@ -188,7 +180,34 @@ jlink {
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
}
- options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
+ options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6',
+ '--no-header-files', '--no-man-pages', '--ignore-signing-information',
+ '--exclude-files', '**.png',
+ '--exclude-resources',
+ 'glob:/com.sparrowwallet.merged.module/META-INF/*,' +
+ 'glob:/javafx.graphics/*.dylib,' +
+ 'glob:/javafx.graphics/*.so,' +
+ 'glob:/javafx.graphics/*.dll,' +
+ 'glob:/com.sparrowwallet.drongo/native/**,' +
+ 'glob:/com.sparrowwallet.sparrow/native/**,' +
+ 'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.so,' +
+ 'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.dylib,' +
+ 'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.jnilib,' +
+ 'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.dll,' +
+ 'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.a,' +
+ 'glob:/com.sparrowwallet.merged.module/darwin-*/**,' +
+ 'glob:/com.sparrowwallet.merged.module/linux-*/**,' +
+ 'glob:/com.sparrowwallet.merged.module/win32-*/**,' +
+ 'glob:/org.usb4java/org/usb4java/darwin-*/**,' +
+ 'glob:/org.usb4java/org/usb4java/linux-*/**,' +
+ 'glob:/org.usb4java/org/usb4java/win32-*/**,' +
+ 'glob:/org.hid4java/darwin-*/**,' +
+ 'glob:/org.hid4java/linux-*/**,' +
+ 'glob:/org.hid4java/win32-*/**,' +
+ 'glob:/openpnp.capture.java/darwin-*/**,' +
+ 'glob:/openpnp.capture.java/linux-*/**,' +
+ 'glob:/openpnp.capture.java/win32-*/**,' +
+ 'glob:/io.github.doblon8.jzbar/native/**']
launcher {
name = 'sparrow'
jvmArgs = ["--enable-native-access=com.sparrowwallet.drongo",
@@ -277,13 +296,13 @@ jlink {
}
if(os.linux) {
- tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules')
+ tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules', 'extractNativeLibraries')
tasks.jpackageImage.finalizedBy('prepareResourceDir')
if(!headless) {
tasks.jpackage.dependsOn('copyMimeInfo')
}
} else {
- tasks.jlink.finalizedBy('addUserWritePermission')
+ tasks.jlink.finalizedBy('addUserWritePermission', 'extractNativeLibraries')
}
tasks.register('addUserWritePermission', Exec) {
@@ -362,6 +381,74 @@ tasks.register('packageTarDistribution', Tar) {
}
}
+def jnaPlatform
+if(os.macOsX) {
+ jnaPlatform = "darwin-${osArch == 'aarch64' ? 'aarch64' : 'x86-64'}"
+} else if(os.windows) {
+ jnaPlatform = "win32-x86-64"
+} else {
+ jnaPlatform = "linux-${osArch == 'aarch64' ? 'aarch64' : 'x86-64'}"
+}
+
+def serialOs = os.macOsX ? "OSX" : (os.windows ? "Windows" : "Linux")
+def serialArch = osArch == "aarch64" ? "aarch64" : "x86_64"
+
+// Map of JAR name prefix to the include glob for platform-specific natives inside the JAR.
+def nativeLibJars = [
+ 'jna-' : "com/sun/jna/${jnaPlatform}/*",
+ 'argon2-jvm-2' : "${jnaPlatform}/*",
+ 'hid4java-' : "${jnaPlatform}/*",
+ 'openpnp-capture-java': "${jnaPlatform}/*",
+ 'jSerialComm-' : "${serialOs}/${serialArch}/*",
+ 'usb4java-' : "org/usb4java/${jnaPlatform}/*",
+ 'jzbar-' : "native/${osName}/${osArch}/*",
+]
+
+tasks.register('extractNativeLibraries') {
+ dependsOn 'jlink'
+ doLast {
+ def imageLib = file("$buildDir/image/lib")
+
+ // Project-owned natives
+ copy {
+ from "${project(':drongo').projectDir}/src/main/resources/native/${osName}/${osArch}", "src/main/resources/native/${osName}/${osArch}"
+ into imageLib
+ eachFile { it.permissions { unix('rw-r--r--') } }
+ }
+
+ // JavaFX natives
+ def javafxClassifier = ""
+ if(os.macOsX) {
+ javafxClassifier = osArch == "aarch64" ? "mac-aarch64" : "mac"
+ } else if(os.windows) {
+ javafxClassifier = "win"
+ } else {
+ javafxClassifier = osArch == "aarch64" ? "linux-aarch64" : "linux"
+ }
+ def javafxJar = configurations.runtimeClasspath.find { it.name == "javafx-graphics-${javafx.version}-${javafxClassifier}.jar" }
+ if(javafxJar) {
+ copy {
+ from(zipTree(javafxJar)) { include "*.dylib", "*.so", "*.dll" }
+ into imageLib
+ eachFile { it.permissions { unix('rw-r--r--') } }
+ }
+ }
+
+ // Third-party natives
+ nativeLibJars.each { prefix, includePattern ->
+ def jar = configurations.runtimeClasspath.find { it.name.startsWith(prefix) }
+ if(jar) {
+ copy {
+ from(zipTree(jar)) { include includePattern }
+ into imageLib
+ eachFile { it.path = it.name; it.permissions { unix('rw-r--r--') } }
+ includeEmptyDirs = false
+ }
+ }
+ }
+ }
+}
+
extraJavaModuleInfo {
module('no.tornado:tornadofx-controls', 'tornadofx.controls') {
exports('tornadofx.control')
diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java
index 191ce5b..345467d 100644
--- a/src/main/java/com/sparrowwallet/sparrow/AppController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java
@@ -88,7 +88,6 @@ public class AppController implements Initializable {
public static final String LOADING_TRANSACTIONS_MESSAGE = "Loading wallet, select Transactions tab to view...";
public static final String CONNECTION_FAILED_PREFIX = "Connection failed: ";
public static final String TRYING_ANOTHER_SERVER_MESSAGE = "trying another server...";
- public static final String JPACKAGE_APP_PATH = "jpackage.app-path";
@FXML
private VBox rootBox;
@@ -420,7 +419,7 @@ public class AppController implements Initializable {
networkItem.setOnAction(event -> restart(event, network));
restart.getItems().add(networkItem);
}
- restart.setVisible(System.getProperty(JPACKAGE_APP_PATH) != null);
+ restart.setVisible(System.getProperty(SparrowWallet.JPACKAGE_APP_PATH) != null);
saveTransaction.setDisable(true);
showTransaction.visibleProperty().bind(Bindings.and(saveTransaction.visibleProperty(), saveTransaction.disableProperty().not()));
@@ -597,7 +596,7 @@ public class AppController implements Initializable {
sudo groupadd -f -r plugdev
sudo usermod -aG plugdev `whoami`
""";
- String home = System.getProperty(JPACKAGE_APP_PATH);
+ String home = System.getProperty(SparrowWallet.JPACKAGE_APP_PATH);
if(home != null && !home.startsWith("/opt/sparrowwallet") && home.endsWith("bin/Sparrow")) {
home = home.replace("bin/Sparrow", "");
commands = commands.replace("/opt/sparrowwallet/", home);
@@ -1045,8 +1044,8 @@ public class AppController implements Initializable {
}
public void restart(ActionEvent event, Network network) {
- if(System.getProperty(JPACKAGE_APP_PATH) == null) {
- throw new IllegalStateException("Property " + JPACKAGE_APP_PATH + " is not present");
+ if(System.getProperty(SparrowWallet.JPACKAGE_APP_PATH) == null) {
+ throw new IllegalStateException("Property " + SparrowWallet.JPACKAGE_APP_PATH + " is not present");
}
Args args = getRestartArgs();
@@ -1067,7 +1066,7 @@ public class AppController implements Initializable {
private void restart(ActionEvent event, Args args) {
try {
List<String> cmd = new ArrayList<>();
- cmd.add(System.getProperty(JPACKAGE_APP_PATH));
+ cmd.add(System.getProperty(SparrowWallet.JPACKAGE_APP_PATH));
cmd.addAll(args.toParams());
final ProcessBuilder builder = new ProcessBuilder(cmd);
if(OsType.getCurrent() == OsType.UNIX) {
diff --git a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java
index d3af37d..94bebaa 100644
--- a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java
+++ b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java
@@ -22,10 +22,20 @@ public class SparrowWallet {
public static final String APP_VERSION_SUFFIX = "";
public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
+ public static final String JPACKAGE_APP_PATH = "jpackage.app-path";
private static Instance instance;
public static void main(String[] argv) {
+ if(System.getProperty(JPACKAGE_APP_PATH) != null) {
+ String libDir = System.getProperty("java.home") + File.separator + "lib";
+ System.setProperty("jna.boot.library.path", libDir);
+ System.setProperty("jna.library.path", libDir);
+ System.setProperty("jSerialComm.library.path", libDir);
+ System.setProperty("org.usb4java.LibraryName", "usb4java");
+ System.setProperty("java.library.path", libDir);
+ }
+
Args args = new Args();
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase(Locale.ROOT)).acceptUnknownOptions(true).build();
jCommander.parse(argv);
diff --git a/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java b/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java
index 82b8e93..62863c9 100644
--- a/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java
+++ b/src/main/java/com/sparrowwallet/sparrow/net/Bwt.java
@@ -23,6 +23,7 @@ import javafx.concurrent.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.time.Duration;
@@ -42,22 +43,47 @@ public class Bwt {
public synchronized static void initialize() {
if(!initialized) {
+ OsType osType = OsType.getCurrent();
+ String osArch = System.getProperty("os.arch");
+ String libName;
+ if(osType == OsType.MACOS) {
+ libName = "libbwt_jni.dylib";
+ } else if(osType == OsType.WINDOWS) {
+ libName = "bwt_jni.dll";
+ } else {
+ libName = "libbwt_jni.so";
+ }
+
+ // Try loading from the application image lib/ directory
+ String javaHome = System.getProperty("java.home");
+ if(javaHome != null) {
+ File libFile = new File(javaHome, "lib" + java.io.File.separator + libName);
+ if(libFile.exists()) {
+ try {
+ System.load(libFile.getAbsolutePath());
+ initialized = true;
+ return;
+ } catch(UnsatisfiedLinkError e) {
+ log.debug("Could not load bwt from java.home, falling back to JAR extraction", e);
+ }
+ }
+ }
+
+ // Fallback: extract from JAR
try {
- OsType osType = OsType.getCurrent();
- String osArch = System.getProperty("os.arch");
if(osType == OsType.MACOS && osArch.equals("aarch64")) {
- NativeUtils.loadLibraryFromJar("/native/osx/aarch64/libbwt_jni.dylib");
+ NativeUtils.loadLibraryFromJar("/native/osx/aarch64/" + libName);
} else if(osType == OsType.MACOS) {
- NativeUtils.loadLibraryFromJar("/native/osx/x64/libbwt_jni.dylib");
+ NativeUtils.loadLibraryFromJar("/native/osx/x64/" + libName);
} else if(osType == OsType.WINDOWS) {
- NativeUtils.loadLibraryFromJar("/native/windows/x64/bwt_jni.dll");
+ NativeUtils.loadLibraryFromJar("/native/windows/x64/" + libName);
} else if(osArch.equals("aarch64")) {
- NativeUtils.loadLibraryFromJar("/native/linux/aarch64/libbwt_jni.so");
+ NativeUtils.loadLibraryFromJar("/native/linux/aarch64/" + libName);
} else {
- NativeUtils.loadLibraryFromJar("/native/linux/x64/libbwt_jni.so");
+ NativeUtils.loadLibraryFromJar("/native/linux/x64/" + libName);
}
initialized = true;
- } catch(IOException e) {
+ } catch(UnsatisfiedLinkError | IOException e) {
log.error("Error loading bwt library", e);
}
}
Why this scored 31/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.