From 41b8c88cc4c0f7338cbfc75c59491b707e68ecb0 Mon Sep 17 00:00:00 2001 From: Eduardo Radieske Date: Tue, 27 Oct 2026 21:40:04 -0300 Subject: [PATCH] feat: add libc-specific native resource selection --- CHANGES.md | 4 + src/com/sun/jna/Native.java | 13 +- src/com/sun/jna/Platform.java | 96 +++++++++++++ test/com/sun/jna/LibraryLoadTest.java | 189 ++++++++++++++++++++++++++ test/com/sun/jna/PlatformTest.java | 75 ++++++++++ 5 files changed, 376 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c7c5c2551..135d25343 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,10 @@ Bug Fixes --------- * [#1740](https://github.com/java-native-access/jna/issues/1740): Fix `c.s.j.p.unix.solaris.LibKstat.KstatCtl` mapping `kc_chain` as an inline `Kstat` instead of a pointer, which made every call taking a `KstatCtl` write past the end of the structure `kstat_open()` allocated, and map the `Kstat` function pointers `ks_update` and `ks_snapshot` as `Pointer` instead of `int` - [@dbwiddis](https://github.com/dbwiddis). +Features +-------- +* [#1694](https://github.com/java-native-access/jna/issues/1694): Add support for multiple Linux native library variants by detecting the process libc and selecting the matching `glibc` or `musl` resource, falling back to the legacy resource when the libc is unknown or the matching variant is unavailable - [@EduardoRadieske](https://github.com/EduardoRadieske). + Breaking Changes ---------------- * `c.s.j.p.unix.solaris.LibKstat.KstatCtl#kc_chain` is now a `Pointer` instead of a `Kstat`. Use `KstatCtl#chain()` to get the head of the kstat chain as a `Kstat`. The previous mapping never returned usable data: the inline `Kstat` decoded the native chain pointer as its `ks_crtime` timestamp, and every following field from bytes past the end of the native structure, so code that read this field could not have relied on it. diff --git a/src/com/sun/jna/Native.java b/src/com/sun/jna/Native.java index ab45f84fc..53d9de98d 100644 --- a/src/com/sun/jna/Native.java +++ b/src/com/sun/jna/Native.java @@ -1158,6 +1158,10 @@ public static File extractFromResourcePath(String name) throws IOException { * @throws IOException if resource not found */ public static File extractFromResourcePath(String name, ClassLoader loader) throws IOException { + return extractFromResourcePath(name, loader, Platform.LIBC); + } + + public static File extractFromResourcePath(String name, ClassLoader loader, Platform.LibC libc) throws IOException { final Level DEBUG = (DEBUG_LOAD || (DEBUG_JNA_LOAD && name.contains("jnidispatch"))) ? Level.INFO : Level.FINE; @@ -1174,7 +1178,14 @@ public static File extractFromResourcePath(String name, ClassLoader loader) thro if (resourcePath.startsWith("/")) { resourcePath = resourcePath.substring(1); } - URL url = loader.getResource(resourcePath); + URL url = null; + if (!name.startsWith("/") && Platform.isLinux() && libc != Platform.LibC.UNKNOWN) { + String variant = Platform.RESOURCE_PREFIX + "-" + libc.name().toLowerCase(); + url = loader.getResource(variant + "/" + libname); + } + if (url == null) { + url = loader.getResource(resourcePath); + } if (url == null) { if (resourcePath.startsWith(Platform.RESOURCE_PREFIX)) { // Fallback for legacy darwin behaviour: darwin was in the past diff --git a/src/com/sun/jna/Platform.java b/src/com/sun/jna/Platform.java index ba5d8ee11..c2b23ac7e 100644 --- a/src/com/sun/jna/Platform.java +++ b/src/com/sun/jna/Platform.java @@ -22,13 +22,24 @@ */ package com.sun.jna; +import java.io.BufferedReader; import java.io.File; +import java.io.FileReader; import java.io.IOException; +import java.io.Reader; +import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; /** Provide simplified platform information. */ public final class Platform { + + public enum LibC { + GLIBC, + MUSL, + UNKNOWN + } + public static final int UNSPECIFIED = -1; public static final int MAC = 0; public static final int LINUX = 1; @@ -68,6 +79,9 @@ public final class Platform { private static final int osType; /** Current platform architecture. */ public static final String ARCH; + + /** The C library implementation used by the current platform. */ + public static final LibC LIBC; static { String osName = System.getProperty("os.name"); @@ -137,6 +151,7 @@ else if (osName.equalsIgnoreCase("dragonflybsd")) { // Windows aarch64 callbacks disabled via ASMFN_OFF (no mingw support) HAS_DLL_CALLBACKS = osType == WINDOWS && !ARCH.startsWith("aarch"); RESOURCE_PREFIX = getNativeLibraryResourcePrefix(); + LIBC = getLibC(); } private Platform() { } public static final int getOSType() { @@ -297,6 +312,87 @@ static boolean isSoftFloat() { return false; } + static LibC getLibC() { + if (!isLinux()) { + return LibC.UNKNOWN; + } + + try { + return getLibC(new FileReader("/proc/self/maps")); + } catch (IOException ex) { + return LibC.UNKNOWN; + } catch (SecurityException ex) { + return LibC.UNKNOWN; + } + } + + /** + * Determines the libc implementation from process memory mappings. The supplied + * reader is consumed and closed. + */ + static LibC getLibC(Reader mapsReader) { + boolean glibc = false; + boolean musl = false; + + try (BufferedReader reader = new BufferedReader(mapsReader)) { + String line; + + while ((line = reader.readLine()) != null) { + String filename = getMappedFileName(line); + + if (filename == null) { + continue; + } + + String lower = filename.toLowerCase(Locale.ROOT); + + if (lower.startsWith("libc.musl-") || lower.startsWith("ld-musl-")) { + musl = true; + } + + if ("libc.so.6".equals(lower) || lower.startsWith("ld-linux")) { + glibc = true; + } + + if (glibc && musl) { + return LibC.UNKNOWN; + } + } + } catch (IOException ex) { + return LibC.UNKNOWN; + } catch (SecurityException ex) { + return LibC.UNKNOWN; + } + + if (glibc == musl) { + return LibC.UNKNOWN; + } + + return musl ? LibC.MUSL : LibC.GLIBC; + } + + private static String getMappedFileName(String line) { + // /proc/self/maps: + // address perms offset dev inode pathname + String[] fields = line.split("\\s+", 6); + + if (fields.length < 6) { + return null; + } + + String pathname = fields[5]; + String deletedSuffix = " (deleted)"; + + if (pathname.endsWith(deletedSuffix)) { + pathname = pathname.substring(0, pathname.length() - deletedSuffix.length()); + } + + int lastSlash = pathname.lastIndexOf('/'); + String filename = pathname.substring(lastSlash + 1); + + return filename.isEmpty() ? null : filename; + } + /** Generate a canonical String prefix based on the current OS type/arch/name. */ diff --git a/test/com/sun/jna/LibraryLoadTest.java b/test/com/sun/jna/LibraryLoadTest.java index 676e72a0b..78f15c276 100644 --- a/test/com/sun/jna/LibraryLoadTest.java +++ b/test/com/sun/jna/LibraryLoadTest.java @@ -34,6 +34,7 @@ import java.net.URL; import java.net.URLClassLoader; import java.util.Collections; +import java.util.UUID; import junit.framework.TestCase; @@ -80,6 +81,194 @@ public void testExtractFromResourcePathWithNullClassLoader() throws Exception { assertNotNull(Native.extractFromResourcePath("/com/sun/jna/LibraryLoadTest.class", null)); } + private static final String LIBC_SELECTION_LIBRARY = "jna-libc-selection-test"; + private static final String LIBC_GLIBC_MARKER = "glibc variant"; + private static final String LIBC_MUSL_MARKER = "musl variant"; + private static final String LIBC_LEGACY_MARKER = "legacy variant"; + + private File createLibcSelectionClasspath() throws IOException { + File root = new File(Native.getTempDir(), "jna-libc-selection-" + UUID.randomUUID()); + + assertTrue("Could not create test classpath directory", root.mkdirs()); + + return root; + } + + private void writeLibcSelectionResource(File root, String prefix, String marker) throws IOException { + String libraryName = NativeLibrary.mapSharedLibraryName(LIBC_SELECTION_LIBRARY); + File resource = new File(new File(root, prefix), libraryName); + + assertTrue("Could not create test resource directory", + resource.getParentFile().exists() || resource.getParentFile().mkdirs()); + + FileOutputStream output = new FileOutputStream(resource); + + try { + output.write(marker.getBytes("UTF-8")); + } finally { + output.close(); + } + } + + private String readLibcSelectionMarker(File resource) throws IOException { + FileInputStream input = new FileInputStream(resource); + try { + byte[] buffer = new byte[64]; + int length = input.read(buffer); + return length < 0 ? "" : new String(buffer, 0, length, "UTF-8"); + } finally { + input.close(); + } + } + + private void deleteRecursively(File file) { + if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + deleteRecursively(child); + } + } + } + file.delete(); + } + + private URLClassLoader createLibcSelectionLoader(File root) throws MalformedURLException { + return new URLClassLoader(new URL[] { root.toURI().toURL() }, new CloverLoader()); + } + + private void assertLibcSelection(File root, Platform.LibC libc, String expectedMarker) throws Exception { + URLClassLoader loader = createLibcSelectionLoader(root); + + try { + File extracted = Native.extractFromResourcePath(LIBC_SELECTION_LIBRARY, loader, libc); + + assertEquals(expectedMarker, readLibcSelectionMarker(extracted)); + } finally { + loader.close(); + } + } + + private void assertLibcSelectionFails(File root, Platform.LibC libc) throws Exception { + URLClassLoader loader = createLibcSelectionLoader(root); + + try { + try { + Native.extractFromResourcePath(LIBC_SELECTION_LIBRARY, loader, libc); + + fail("Expected IOException when no compatible resource exists for libc " + libc); + } catch (IOException expected) { + // No compatible resource is available. + } + } finally { + loader.close(); + } + } + + public void testExtractPrefersMuslVariant() throws Exception { + File root = createLibcSelectionClasspath(); + try { + writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-glibc", LIBC_GLIBC_MARKER); + writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-musl", LIBC_MUSL_MARKER); + + assertLibcSelection(root, Platform.LibC.MUSL, LIBC_MUSL_MARKER); + } finally { + deleteRecursively(root); + } + } + + public void testExtractPrefersGlibcVariant() throws Exception { + File root = createLibcSelectionClasspath(); + + try { + writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-glibc", LIBC_GLIBC_MARKER); + writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-musl", LIBC_MUSL_MARKER); + + assertLibcSelection(root, Platform.LibC.GLIBC, LIBC_GLIBC_MARKER); + } finally { + deleteRecursively(root); + } + } + + public void testExtractFallsBackToLegacyResource() throws Exception { + File root = createLibcSelectionClasspath(); + + try { + writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX, LIBC_LEGACY_MARKER); + + assertLibcSelection(root, Platform.LibC.MUSL, LIBC_LEGACY_MARKER); + } finally { + deleteRecursively(root); + } + } + + public void testExtractSpecificVariantPrecedesLegacyResource() throws Exception { + File root = createLibcSelectionClasspath(); + + try { + writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-musl", LIBC_MUSL_MARKER); + writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX, LIBC_LEGACY_MARKER); + + assertLibcSelection(root, Platform.LibC.MUSL, LIBC_MUSL_MARKER); + } finally { + deleteRecursively(root); + } + } + + public void testExtractFallsBackToLegacyWhenRequestedVariantIsMissing() throws Exception { + File root = createLibcSelectionClasspath(); + + try { + // A GLIBC variant must not be selected for a MUSL request. + writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-glibc", LIBC_GLIBC_MARKER); + writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX, LIBC_LEGACY_MARKER); + + assertLibcSelection(root, Platform.LibC.MUSL, LIBC_LEGACY_MARKER); + } finally { + deleteRecursively(root); + } + } + + public void testExtractUnknownLibcFallsBackToLegacyResource() throws Exception { + File root = createLibcSelectionClasspath(); + + try { + writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-glibc", LIBC_GLIBC_MARKER); + writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-musl", LIBC_MUSL_MARKER); + writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX, LIBC_LEGACY_MARKER); + + assertLibcSelection(root, Platform.LibC.UNKNOWN, LIBC_LEGACY_MARKER); + } finally { + deleteRecursively(root); + } + } + + public void testExtractUnknownLibcDoesNotSelectVariant() throws Exception { + File root = createLibcSelectionClasspath(); + + try { + writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-musl", LIBC_MUSL_MARKER); + writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-glibc", LIBC_GLIBC_MARKER); + + assertLibcSelectionFails(root, Platform.LibC.UNKNOWN); + } finally { + deleteRecursively(root); + } + } + + public void testExtractDoesNotSelectOtherLibcVariant() throws Exception { + File root = createLibcSelectionClasspath(); + + try { + // Only GLIBC is available; MUSL must not select it. + writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-glibc", LIBC_GLIBC_MARKER); + + assertLibcSelectionFails(root, Platform.LibC.MUSL); + } finally { + deleteRecursively(root); + } + } + public void testLoadFromJNALibraryPath() { // Tests are already configured to load from this path NativeLibrary.getInstance("testlib"); diff --git a/test/com/sun/jna/PlatformTest.java b/test/com/sun/jna/PlatformTest.java index 5a4a9e0ab..3d4e03a94 100644 --- a/test/com/sun/jna/PlatformTest.java +++ b/test/com/sun/jna/PlatformTest.java @@ -23,6 +23,10 @@ */ package com.sun.jna; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; + import junit.framework.TestCase; //@SuppressWarnings("unused") @@ -116,6 +120,77 @@ public void testSystemProperty() { System.clearProperty("jna.prefix"); } + + private static String mapLine(String pathname) { + return "7f000000-7f001000 r-xp 00000000 08:01 123 " + pathname + "\n"; + } + + private static Platform.LibC detectLibC(String maps) { + return Platform.getLibC(new StringReader(maps)); + } + + public void testGetLibCDetectsGlibcFromLibcMapping() { + String maps = mapLine("/lib/x86_64-linux-gnu/libc.so.6"); + + assertEquals(Platform.LibC.GLIBC, detectLibC(maps)); + } + + public void testGetLibCDetectsGlibcFromDynamicLinker() { + String maps = mapLine("/lib64/ld-linux-x86-64.so.2"); + + assertEquals(Platform.LibC.GLIBC, detectLibC(maps)); + } + + public void testGetLibCDetectsMuslFromLibcMapping() { + String maps = mapLine("/lib/libc.musl-x86_64.so.1"); + + assertEquals(Platform.LibC.MUSL, detectLibC(maps)); + } + + public void testGetLibCDetectsMuslFromDynamicLinker() { + String maps = mapLine("/lib/ld-musl-x86_64.so.1"); + + assertEquals(Platform.LibC.MUSL, detectLibC(maps)); + } + + public void testGetLibCReturnsUnknownWhenNoLibcIsDetected() { + String maps = mapLine("/usr/lib/libpthread.so.0") + mapLine("/usr/lib/libjvm.so") + mapLine("[heap]"); + + assertEquals(Platform.LibC.UNKNOWN, detectLibC(maps)); + } + + public void testGetLibCReturnsUnknownWhenBothLibcsAreDetected() { + String maps = mapLine("/lib/libc.so.6") + mapLine("/lib/ld-musl-x86_64.so.1"); + + assertEquals(Platform.LibC.UNKNOWN, detectLibC(maps)); + } + + public void testGetLibCIgnoresLibcNamesInParentDirectories() { + String maps = mapLine("/opt/libc.so.6/my-application.jar"); + + assertEquals(Platform.LibC.UNKNOWN, detectLibC(maps)); + } + + public void testGetLibCHandlesDeletedMappings() { + String maps = mapLine("/lib/ld-musl-x86_64.so.1 (deleted)"); + + assertEquals(Platform.LibC.MUSL, detectLibC(maps)); + } + + public void testGetLibCReturnsUnknownWhenReadingFails() { + Reader failingReader = new Reader() { + @Override + public int read(char[] buffer, int offset, int length) throws IOException { + throw new IOException("Simulated read failure"); + } + + @Override + public void close() { + } + }; + + assertEquals(Platform.LibC.UNKNOWN, Platform.getLibC(failingReader)); + } public static void main(String[] args) { junit.textui.TestRunner.run(PlatformTest.class);