Skip to content

Commit 0f07929

Browse files
committed
Fix Windows attach failure when standalone JRE is on PATH
When the system PATH contains a standalone JRE (no attach.dll) and JAVA_HOME points to a JDK, URLClassLoader loads tools.jar but System.loadLibrary("attach") fails because the JRE bin/ directory has no attach.dll. AttachHelper.loadAttachLibrary() catches the UnsatisfiedLinkError and falls back to System.load() using the absolute path JAVA_HOME/jre/bin/attach.dll. Add workflow windows-split-install to verify the fix on demand: builds with JDK 11, tests with a standalone Temurin JRE 8 (no attach.dll) against a JDK 8 JAVA_HOME. Fixes #7.
1 parent 4500fe5 commit 0f07929

2 files changed

Lines changed: 144 additions & 0 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Windows split brain - standalone JRE on PATH with JDK JAVA_HOME
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
verify-fix:
8+
runs-on: windows-latest
9+
10+
steps:
11+
- uses: actions/checkout@v4
12+
13+
- name: Set up Temurin JDK 11 (build)
14+
uses: actions/setup-java@v4
15+
with:
16+
java-version: '11'
17+
distribution: 'temurin'
18+
19+
- name: Set up Temurin JDK 8 (JAVA_HOME will point here for the test)
20+
uses: actions/setup-java@v4
21+
with:
22+
java-version: '8'
23+
distribution: 'temurin'
24+
25+
- name: Build jar
26+
env:
27+
JAVA_HOME: ${{ env.JAVA_HOME_11_X64 }}
28+
run: mvn package -DskipTests -q
29+
30+
- name: Download Temurin JRE-only archive (simulates Oracle standalone public JRE)
31+
shell: pwsh
32+
run: |
33+
$url = "https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u442-b06/OpenJDK8U-jre_x64_windows_hotspot_8u442b06.zip"
34+
Invoke-WebRequest -Uri $url -OutFile jre8.zip
35+
Expand-Archive jre8.zip -DestinationPath jre8
36+
$jreHome = (Get-ChildItem jre8 -Directory)[0].FullName
37+
echo "STANDALONE_JRE=$jreHome" >> $env:GITHUB_ENV
38+
39+
- name: Confirm preconditions
40+
shell: pwsh
41+
run: |
42+
Write-Host "Standalone JRE (simulates java.exe on PATH):"
43+
Write-Host " path : $env:STANDALONE_JRE"
44+
Write-Host " attach.dll : $(Test-Path "$env:STANDALONE_JRE\bin\attach.dll")"
45+
Write-Host ""
46+
Write-Host "JDK (JAVA_HOME):"
47+
Write-Host " path : $env:JAVA_HOME"
48+
Write-Host " tools.jar : $(Test-Path "$env:JAVA_HOME\lib\tools.jar")"
49+
Write-Host " attach.dll : $(Test-Path "$env:JAVA_HOME\jre\bin\attach.dll")"
50+
51+
- name: Verify fix
52+
shell: pwsh
53+
# Simulates: Oracle standalone JRE on PATH, JAVA_HOME pointing to JDK.
54+
# java.exe is from the standalone JRE → sun.boot.library.path = standalone JRE bin\
55+
# Standalone JRE has no attach.dll → System.loadLibrary("attach") fails
56+
# Fix: loadAttachLibrary() catches the error and loads from JAVA_HOME\jre\bin\attach.dll
57+
run: |
58+
# Strip JDK directories from PATH so java.library.path also has no attach.dll fallback
59+
$env:PATH = ($env:PATH -split ';' |
60+
Where-Object { $_ -notmatch 'jdk|jre|java|hostedtoolcache' }) -join ';'
61+
62+
Write-Host "PATH : $env:PATH"
63+
64+
$java = "$env:STANDALONE_JRE\bin\java.exe"
65+
$jar = (Get-ChildItem target\extract-tls-secrets-*.jar)[0].FullName
66+
67+
Write-Host "java.exe : $java (standalone JRE — no attach.dll)"
68+
Write-Host "JAVA_HOME: $env:JAVA_HOME (JDK — has tools.jar + jre\bin\attach.dll)"
69+
Write-Host ""
70+
& $java -jar $jar list

src/main/java/name/neykov/secrets/cli/AttachHelper.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.sun.tools.attach.AttachNotSupportedException;
66
import com.sun.tools.attach.VirtualMachine;
77
import com.sun.tools.attach.VirtualMachineDescriptor;
8+
import java.io.File;
89
import java.io.IOException;
910

1011
/**
@@ -21,6 +22,9 @@
2122
public class AttachHelper {
2223
public static void handle(String jarPath, String pid, String attachOptions)
2324
throws FailureMessageException {
25+
if (isWindows()) {
26+
loadAttachLibrary();
27+
}
2428
if (pid.equals("list")) {
2529
System.out.print(AttachHelper.list());
2630
} else {
@@ -83,4 +87,74 @@ private static boolean pidExists(String pid) {
8387
}
8488
return false;
8589
}
90+
91+
private static boolean isWindows() {
92+
return System.getProperty("os.name").startsWith("Windows");
93+
}
94+
95+
96+
// The Windows library-loading fix lives here rather than in AgentAttach because native
97+
// library ownership is classloader-scoped. The JVM keeps a global set of loaded library
98+
// names; if a library is already owned by one classloader, any other classloader that
99+
// attempts to load it by name receives UnsatisfiedLinkError("already loaded in another
100+
// classloader") — unconditionally, with no exception for parent/child relationships.
101+
// See: NativeLibraries.loadLibrary() in OpenJDK
102+
// src/java.base/share/classes/jdk/internal/loader/NativeLibraries.java
103+
// (Holder.loadedLibraryNames global set, checked before every load attempt)
104+
// and the JNI spec: https://docs.oracle.com/en/java/javase/21/docs/specs/jni/design.html
105+
// ("the VM internally maintains a list of loaded native libraries for each class loader")
106+
//
107+
// AttachHelper is loaded inside URLClassLoader(cp, null). Moving System.load() to
108+
// AgentAttach (app classloader) would register "attach" there; when
109+
// WindowsAttachProvider's static initializer calls System.loadLibrary("attach") from
110+
// within the URLClassLoader it would hit the global name check and fail. Keeping
111+
// loadAttachLibrary() in AttachHelper ensures both the pre-load and the SPI-triggered
112+
// load share the same classloader.
113+
private static void loadAttachLibrary() throws FailureMessageException {
114+
try {
115+
System.loadLibrary("attach");
116+
// All good — system is set up properly.
117+
} catch (UnsatisfiedLinkError e) {
118+
// attach.dll not on the default search path; try well-known fallback locations.
119+
// This can happen when running with a standalone JRE whose bin/ has no attach.dll
120+
// while JAVA_HOME points to a JDK that does have it under jre/bin/.
121+
if (!tryLoadLibrary("jre/bin/attach.dll")) {
122+
throw new FailureMessageException(
123+
"Failed loading attach provider."
124+
+ " Make sure you are running with a JDK java executable."
125+
+ " Alternatively locate 'attach.dll' on your system,"
126+
+ " typically found in '<jdk home>/jre/bin' for Oracle JDK,"
127+
+ " and pass the path at startup as:",
128+
" java -Djava.library.path=\"<jdk home>/jre/bin\""
129+
+ " -jar extract-tls-secrets.jar");
130+
}
131+
}
132+
}
133+
134+
// Duplicated from AgentAttach — cannot be shared across the ClassLoader boundary.
135+
private static File getJavaHome() throws FailureMessageException {
136+
String javaHomeEnv = System.getenv("JAVA_HOME");
137+
if (javaHomeEnv != null) {
138+
return new File(javaHomeEnv);
139+
}
140+
throw new FailureMessageException(
141+
"No JAVA_HOME environment variable found."
142+
+ " Must point to a local JDK installation.");
143+
}
144+
145+
private static boolean tryLoadLibrary(String relativePath) throws FailureMessageException {
146+
File javaHome = getJavaHome();
147+
File lib = new File(javaHome, relativePath);
148+
if (!lib.exists()) {
149+
return false;
150+
}
151+
try {
152+
System.load(lib.getAbsolutePath());
153+
} catch (UnsatisfiedLinkError e) {
154+
return false;
155+
}
156+
// Once loaded by absolute path the library is registered under the name "attach",
157+
// so any subsequent System.loadLibrary("attach") calls will find it already loaded.
158+
return true;
159+
}
86160
}

0 commit comments

Comments
 (0)