From 40a657f5856ee61e5663937f9eee8d3b23ed1956 Mon Sep 17 00:00:00 2001 From: Jeremy Norris Date: Fri, 20 Feb 2026 08:15:41 -0600 Subject: [PATCH 01/12] Don't try to close an INVALID_HANDLE_VALUE. --- src/main/java/com/jcraft/jsch/PageantConnector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/jcraft/jsch/PageantConnector.java b/src/main/java/com/jcraft/jsch/PageantConnector.java index 7b7cc31e..3fcd458a 100644 --- a/src/main/java/com/jcraft/jsch/PageantConnector.java +++ b/src/main/java/com/jcraft/jsch/PageantConnector.java @@ -129,7 +129,7 @@ public void query(Buffer buffer) throws AgentProxyException { } finally { if (sharedMemory != null) kernel32.UnmapViewOfFile(sharedMemory); - if (sharedFile != null) + if (sharedFile != null && sharedFile != WinBase.INVALID_HANDLE_VALUE) kernel32.CloseHandle(sharedFile); } } From e580ff7d9cdfc84a67d86c2b4564964ed91208ca Mon Sep 17 00:00:00 2001 From: Jeremy Norris Date: Fri, 20 Feb 2026 08:15:47 -0600 Subject: [PATCH 02/12] Correctly use FILE_MAP flags instead of SECTION_MAP flags when calling MapViewOfFile(). --- src/main/java/com/jcraft/jsch/PageantConnector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/jcraft/jsch/PageantConnector.java b/src/main/java/com/jcraft/jsch/PageantConnector.java index 3fcd458a..0eee096c 100644 --- a/src/main/java/com/jcraft/jsch/PageantConnector.java +++ b/src/main/java/com/jcraft/jsch/PageantConnector.java @@ -100,7 +100,7 @@ public void query(Buffer buffer) throws AgentProxyException { throw new AgentProxyException("Unable to create shared file mapping."); } - sharedMemory = kernel32.MapViewOfFile(sharedFile, WinNT.SECTION_MAP_WRITE, 0, 0, 0); + sharedMemory = kernel32.MapViewOfFile(sharedFile, WinBase.FILE_MAP_WRITE, 0, 0, 0); if (sharedMemory == null) { throw new AgentProxyException("Unable to create shared file mapping."); } From 8df28c9b8740db45cb9a9a43f0c0fac80c411498 Mon Sep 17 00:00:00 2001 From: Jeremy Norris Date: Fri, 20 Feb 2026 08:15:52 -0600 Subject: [PATCH 03/12] Better differentiate errors & also include GetLastError() value. --- src/main/java/com/jcraft/jsch/PageantConnector.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/jcraft/jsch/PageantConnector.java b/src/main/java/com/jcraft/jsch/PageantConnector.java index 0eee096c..2ba939ba 100644 --- a/src/main/java/com/jcraft/jsch/PageantConnector.java +++ b/src/main/java/com/jcraft/jsch/PageantConnector.java @@ -97,12 +97,14 @@ public void query(Buffer buffer) throws AgentProxyException { sharedFile = kernel32.CreateFileMapping(WinBase.INVALID_HANDLE_VALUE, psa, WinNT.PAGE_READWRITE, 0, AGENT_MAX_MSGLEN, mapname); if (sharedFile == null || sharedFile == WinBase.INVALID_HANDLE_VALUE) { - throw new AgentProxyException("Unable to create shared file mapping."); + throw new AgentProxyException( + "Unable to create shared file mapping: GetLastError() = " + kernel32.GetLastError()); } sharedMemory = kernel32.MapViewOfFile(sharedFile, WinBase.FILE_MAP_WRITE, 0, 0, 0); if (sharedMemory == null) { - throw new AgentProxyException("Unable to create shared file mapping."); + throw new AgentProxyException( + "Unable to create shared memory mapping: GetLastError() = " + kernel32.GetLastError()); } sharedMemory.write(0, buffer.buffer, 0, buffer.getLength()); @@ -124,7 +126,7 @@ public void query(Buffer buffer) throws AgentProxyException { sharedMemory.read(4, buffer.buffer, 0, i); } else { throw new AgentProxyException( - "User32.SendMessage() returned 0 with cds.dwData: " + Long.toHexString(foo)); + "SendMessage() returned 0 with cds.dwData: " + Long.toHexString(foo)); } } finally { if (sharedMemory != null) From 904fcd7ed1da8201661c5d485a20bb0d1b21d6a7 Mon Sep 17 00:00:00 2001 From: Jeremy Norris Date: Fri, 20 Feb 2026 08:15:56 -0600 Subject: [PATCH 04/12] Update Pageant shared mapping name to use a combination of GetCurrentProcessId() and the Java thread id. This is needed to support virtual threads correctly, since GetCurrentThreadId() could change if the virtual thread is parked and resumed on a different carrier thread. --- pom.xml | 20 ++++++++++++++++++- src/assembly/sources.xml | 7 +++++++ .../java/com/jcraft/jsch/JavaThreadId.java | 11 ++++++++++ .../com/jcraft/jsch/PageantConnector.java | 4 ++-- .../java19/com/jcraft/jsch/JavaThreadId.java | 8 ++++++++ 5 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/jcraft/jsch/JavaThreadId.java create mode 100644 src/main/java19/com/jcraft/jsch/JavaThreadId.java diff --git a/pom.xml b/pom.xml index 1ad7096c..16d90af5 100644 --- a/pom.xml +++ b/pom.xml @@ -418,6 +418,19 @@ 16 + + default-compile-19 + + compile + + + + ${project.basedir}/src/main/java19 + + true + 19 + + default-compile-24 @@ -560,7 +573,7 @@ none com.jcraft.jsch com.jcraft.jsch.* - ${project.build.sourceDirectory}:${project.build.directory}/generated-sources/java-templates:${project.basedir}/src/main/java9:${project.basedir}/src/main/java10:${project.basedir}/src/main/java11:${project.basedir}/src/main/java15:${project.basedir}/src/main/java16:${project.basedir}/src/main/java24 + ${project.build.sourceDirectory}:${project.build.directory}/generated-sources/java-templates:${project.basedir}/src/main/java9:${project.basedir}/src/main/java10:${project.basedir}/src/main/java11:${project.basedir}/src/main/java15:${project.basedir}/src/main/java16:${project.basedir}/src/main/java19:${project.basedir}/src/main/java24 @@ -658,6 +671,7 @@ ${project.basedir}/src/main/java11 ${project.basedir}/src/main/java15 ${project.basedir}/src/main/java16 + ${project.basedir}/src/main/java19 ${project.basedir}/src/main/java24 ${project.basedir}/src/main/java-templates @@ -679,6 +693,7 @@ ${project.basedir}/src/main/java11 ${project.basedir}/src/main/java15 ${project.basedir}/src/main/java16 + ${project.basedir}/src/main/java19 ${project.basedir}/src/main/java24 ${project.basedir}/src/main/java-templates @@ -699,6 +714,7 @@ ${project.basedir}/src/main/java11 ${project.basedir}/src/main/java15 ${project.basedir}/src/main/java16 + ${project.basedir}/src/main/java19 ${project.basedir}/src/main/java24 ${project.basedir}/src/main/java-templates @@ -710,6 +726,7 @@ 0.8.15 + com/jcraft/jsch/JavaThreadId.class com/jcraft/jsch/JavaVersion.class com/jcraft/jsch/JplLogger.class com/jcraft/jsch/UnixDomainSocketFactory.class @@ -723,6 +740,7 @@ com/jcraft/jsch/jce/XDH.class META-INF/versions/9/com/jcraft/jsch/JavaVersion.class META-INF/versions/10/com/jcraft/jsch/JavaVersion.class + META-INF/versions/19/com/jcraft/jsch/JavaThreadId.class diff --git a/src/assembly/sources.xml b/src/assembly/sources.xml index 51877a64..e71fcb8e 100644 --- a/src/assembly/sources.xml +++ b/src/assembly/sources.xml @@ -62,6 +62,13 @@ **/*.java + + ${project.basedir}/src/main/java19 + META-INF/versions/19 + + **/*.java + + ${project.basedir}/src/main/java24 META-INF/versions/24 diff --git a/src/main/java/com/jcraft/jsch/JavaThreadId.java b/src/main/java/com/jcraft/jsch/JavaThreadId.java new file mode 100644 index 00000000..07aadc75 --- /dev/null +++ b/src/main/java/com/jcraft/jsch/JavaThreadId.java @@ -0,0 +1,11 @@ +package com.jcraft.jsch; + +import com.jcraft.jsch.annotations.SuppressForbiddenApi; + +final class JavaThreadId { + + @SuppressForbiddenApi("jdk-deprecated") + static long get() { + return Thread.currentThread().getId(); + } +} diff --git a/src/main/java/com/jcraft/jsch/PageantConnector.java b/src/main/java/com/jcraft/jsch/PageantConnector.java index 2ba939ba..c65b26d4 100644 --- a/src/main/java/com/jcraft/jsch/PageantConnector.java +++ b/src/main/java/com/jcraft/jsch/PageantConnector.java @@ -85,8 +85,8 @@ public void query(Buffer buffer) throws AgentProxyException { throw new AgentProxyException("Pageant is not runnning."); } - String mapname = - String.format(Locale.ROOT, "PageantRequest%08x", kernel32.GetCurrentThreadId()); + String mapname = String.format(Locale.ROOT, "JSchPageantRequest%08x%08x", + kernel32.GetCurrentProcessId(), JavaThreadId.get()); HANDLE sharedFile = null; Pointer sharedMemory = null; diff --git a/src/main/java19/com/jcraft/jsch/JavaThreadId.java b/src/main/java19/com/jcraft/jsch/JavaThreadId.java new file mode 100644 index 00000000..608652ab --- /dev/null +++ b/src/main/java19/com/jcraft/jsch/JavaThreadId.java @@ -0,0 +1,8 @@ +package com.jcraft.jsch; + +final class JavaThreadId { + + static long get() { + return Thread.currentThread().threadId(); + } +} From a7517d8ce817ce45d5568f284d1213d206234f9d Mon Sep 17 00:00:00 2001 From: Jeremy Norris Date: Fri, 20 Feb 2026 08:15:59 -0600 Subject: [PATCH 05/12] Add FFM version of Pageant AgentConnector. --- .github/workflows/maven.yml | 2 +- pom.xml | 80 +++++++- src/assembly/sources.xml | 14 ++ .../com/jcraft/jsch/PageantFFMConnector.java | 49 +++++ .../com/jcraft/jsch/PageantFFMConnector.java | 172 ++++++++++++++++++ 5 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/jcraft/jsch/PageantFFMConnector.java create mode 100644 src/main/java23/com/jcraft/jsch/PageantFFMConnector.java diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 79071f66..f8f59a36 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -85,7 +85,7 @@ jobs: run: | LOWER_JDK="${{ matrix.java }}" UPPER_JDK=$((LOWER_JDK+1)) - ./mvnw -B -V -e -Pcoverage verify -Dtoolchain.jdk.version="[$LOWER_JDK,$UPPER_JDK)" -Dmaven.resources.skip=true -Dflatten.skip=true -Dmaven.main.skip=true -Dbnd.skip=true -Dassembly.skipAssembly=true -Dmaven.javadoc.skip=true -Dcyclonedx.skip=true -Dspdx.skip=true -Dformatter.skip=true -Dimpsort.skip=true -Dforbiddenapis.skip=true -DskipTests=false -DskipITs=false + ./mvnw -B -V -e -Pcoverage verify -Dtoolchain.jdk.version="[$LOWER_JDK,$UPPER_JDK)" -Dwindowsapi.skip=true -Dmaven.resources.skip=true -Dflatten.skip=true -Dmaven.main.skip=true -Dbnd.skip=true -Dassembly.skipAssembly=true -Dmaven.javadoc.skip=true -Dcyclonedx.skip=true -Dspdx.skip=true -Dformatter.skip=true -Dimpsort.skip=true -Dforbiddenapis.skip=true -DskipTests=false -DskipITs=false - name: Upload test results uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/pom.xml b/pom.xml index 16d90af5..086189b7 100644 --- a/pom.xml +++ b/pom.xml @@ -271,6 +271,36 @@ + + net.codecrete.windows-api + windowsapi-maven-plugin + 0.8.5 + + . + com.jcraft.jsch.windowsapi + false + + CloseHandle + CreateFileMappingA + FindWindowA + GetCurrentProcessId + MapViewOfFile + SendMessageA + UnmapViewOfFile + + + COPYDATASTRUCT + + + FILE_MAP + PAGE_PROTECTION_FLAGS + + + INVALID_HANDLE_VALUE + WM_COPYDATA + + + org.apache.maven.plugins maven-toolchains-plugin @@ -431,6 +461,23 @@ 19 + + default-compile-23 + + compile + + + + ${project.basedir}/src/main/java23 + ${project.build.directory}/generated-sources/windows-api + + true + 23 + + -Xlint:all,-processing,-classfile,-options,-restricted + + + default-compile-24 @@ -572,8 +619,8 @@ true none com.jcraft.jsch - com.jcraft.jsch.* - ${project.build.sourceDirectory}:${project.build.directory}/generated-sources/java-templates:${project.basedir}/src/main/java9:${project.basedir}/src/main/java10:${project.basedir}/src/main/java11:${project.basedir}/src/main/java15:${project.basedir}/src/main/java16:${project.basedir}/src/main/java19:${project.basedir}/src/main/java24 + com.jcraft.jsch.*,com.jcraft.jsch.windowsapi.windows.win32.*,com.jcraft.jsch.windowsapi.windows.win32.system.*,com.jcraft.jsch.windowsapi.windows.win32.ui.* + ${project.build.sourceDirectory}:${project.build.directory}/generated-sources/java-templates:${project.basedir}/src/main/java9:${project.basedir}/src/main/java10:${project.basedir}/src/main/java11:${project.basedir}/src/main/java15:${project.basedir}/src/main/java16:${project.basedir}/src/main/java19:${project.basedir}/src/main/java23:${project.build.directory}/generated-sources/windows-api:${project.basedir}/src/main/java24 @@ -672,6 +719,7 @@ ${project.basedir}/src/main/java15 ${project.basedir}/src/main/java16 ${project.basedir}/src/main/java19 + ${project.basedir}/src/main/java23 ${project.basedir}/src/main/java24 ${project.basedir}/src/main/java-templates @@ -694,6 +742,7 @@ ${project.basedir}/src/main/java15 ${project.basedir}/src/main/java16 ${project.basedir}/src/main/java19 + ${project.basedir}/src/main/java23 ${project.basedir}/src/main/java24 ${project.basedir}/src/main/java-templates @@ -715,6 +764,7 @@ ${project.basedir}/src/main/java15 ${project.basedir}/src/main/java16 ${project.basedir}/src/main/java19 + ${project.basedir}/src/main/java23 ${project.basedir}/src/main/java24 ${project.basedir}/src/main/java-templates @@ -729,6 +779,7 @@ com/jcraft/jsch/JavaThreadId.class com/jcraft/jsch/JavaVersion.class com/jcraft/jsch/JplLogger.class + com/jcraft/jsch/PageantFFMConnector.class com/jcraft/jsch/UnixDomainSocketFactory.class com/jcraft/jsch/jce/KeyPairGenEdDSA.class com/jcraft/jsch/jce/MLKEM.class @@ -1049,5 +1100,30 @@ + + windowsapi + + [21,) + + windowsapi.skip + !true + + + + + + net.codecrete.windows-api + windowsapi-maven-plugin + + + + windows-api + + + + + + + diff --git a/src/assembly/sources.xml b/src/assembly/sources.xml index e71fcb8e..c9db1979 100644 --- a/src/assembly/sources.xml +++ b/src/assembly/sources.xml @@ -69,6 +69,20 @@ **/*.java + + ${project.basedir}/src/main/java23 + META-INF/versions/23 + + **/*.java + + + + ${project.build.directory}/generated-sources/windows-api + META-INF/versions/23 + + **/*.java + + ${project.basedir}/src/main/java24 META-INF/versions/24 diff --git a/src/main/java/com/jcraft/jsch/PageantFFMConnector.java b/src/main/java/com/jcraft/jsch/PageantFFMConnector.java new file mode 100644 index 00000000..ee10f022 --- /dev/null +++ b/src/main/java/com/jcraft/jsch/PageantFFMConnector.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. The names of the authors may not be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL JCRAFT, INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jcraft.jsch; + +public class PageantFFMConnector implements AgentConnector { + + public PageantFFMConnector() throws AgentProxyException { + throw new AgentProxyException("PageantFFMConnector requires Java23+."); + } + + @Override + public String getName() { + throw new UnsupportedOperationException("PageantFFMConnector requires Java23+."); + } + + @Override + public boolean isAvailable() { + throw new UnsupportedOperationException("PageantFFMConnector requires Java23+."); + } + + @Override + public void query(Buffer buffer) throws AgentProxyException { + throw new UnsupportedOperationException("PageantFFMConnector requires Java23+."); + } +} diff --git a/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java b/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java new file mode 100644 index 00000000..82c463a2 --- /dev/null +++ b/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. The names of the authors may not be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL JCRAFT, INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jcraft.jsch; + +import static com.jcraft.jsch.windowsapi.windows.win32.foundation.Apis.CloseHandle; +import static com.jcraft.jsch.windowsapi.windows.win32.foundation.Constants.INVALID_HANDLE_VALUE; +import static com.jcraft.jsch.windowsapi.windows.win32.system.memory.Apis.CreateFileMappingA; +import static com.jcraft.jsch.windowsapi.windows.win32.system.memory.Apis.MapViewOfFile; +import static com.jcraft.jsch.windowsapi.windows.win32.system.memory.Apis.UnmapViewOfFile; +import static com.jcraft.jsch.windowsapi.windows.win32.system.threading.Apis.GetCurrentProcessId; +import static com.jcraft.jsch.windowsapi.windows.win32.ui.windowsandmessaging.Apis.FindWindowA; +import static com.jcraft.jsch.windowsapi.windows.win32.ui.windowsandmessaging.Apis.SendMessageA; +import static com.jcraft.jsch.windowsapi.windows.win32.ui.windowsandmessaging.Constants.WM_COPYDATA; + +import com.jcraft.jsch.windowsapi.windows.win32.system.dataexchange.COPYDATASTRUCT; +import com.jcraft.jsch.windowsapi.windows.win32.system.memory.FILE_MAP; +import com.jcraft.jsch.windowsapi.windows.win32.system.memory.PAGE_PROTECTION_FLAGS; +import java.lang.foreign.Arena; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.StructLayout; +import java.lang.invoke.VarHandle; +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +public class PageantFFMConnector implements AgentConnector { + + private static final int AGENT_MAX_MSGLEN = 262144; + private static final long AGENT_COPYDATA_ID = 0x804e50baL; + + private final StructLayout errorStateLayout; + private final VarHandle getLastErrorVarHandle; + + public PageantFFMConnector() throws AgentProxyException { + if (!Util.getSystemProperty("os.name", "").startsWith("Windows")) { + throw new AgentProxyException("PageantFFMConnector only available on Windows."); + } + + try { + errorStateLayout = Linker.Option.captureStateLayout(); + getLastErrorVarHandle = + errorStateLayout.varHandle(MemoryLayout.PathElement.groupElement("GetLastError")); + + // Force class initialization to catch UnsatisfiedLinkError + Object foo = com.jcraft.jsch.windowsapi.windows.win32.foundation.Apis.CloseHandle$handle(); + foo = com.jcraft.jsch.windowsapi.windows.win32.system.memory.Apis.CreateFileMappingA$handle(); + foo = com.jcraft.jsch.windowsapi.windows.win32.system.threading.Apis + .GetCurrentProcessId$handle(); + foo = + com.jcraft.jsch.windowsapi.windows.win32.ui.windowsandmessaging.Apis.FindWindowA$handle(); + } catch (IllegalArgumentException | LinkageError e) { + throw new AgentProxyException(e.toString(), e); + } + } + + @Override + public String getName() { + return "pageant_ffm"; + } + + @Override + public boolean isAvailable() { + try (Arena arena = Arena.ofConfined()) { + MemorySegment errorState = arena.allocate(errorStateLayout); + MemorySegment pageant = arena.allocateFrom("Pageant", StandardCharsets.US_ASCII); + return !FindWindowA(errorState, pageant, pageant).equals(MemorySegment.NULL); + } + } + + @Override + public void query(Buffer buffer) throws AgentProxyException { + if (buffer.getLength() > AGENT_MAX_MSGLEN) { + throw new AgentProxyException("Query too large."); + } + + try (Arena arena = Arena.ofConfined()) { + MemorySegment sharedFile = MemorySegment.NULL; + MemorySegment sharedMemory = MemorySegment.NULL; + MemorySegment errorState = arena.allocate(errorStateLayout); + MemorySegment pageant = arena.allocateFrom("Pageant", StandardCharsets.US_ASCII); + + MemorySegment hwnd = FindWindowA(errorState, pageant, pageant); + + if (hwnd.equals(MemorySegment.NULL)) { + throw new AgentProxyException("Pageant is not runnning."); + } + + MemorySegment mapname = + arena.allocateFrom(String.format(Locale.ROOT, "JSchPageantRequest%08x%08x", + GetCurrentProcessId(), JavaThreadId.get()), StandardCharsets.US_ASCII); + + try { + // TODO + MemorySegment psa = MemorySegment.NULL; + + sharedFile = CreateFileMappingA(errorState, INVALID_HANDLE_VALUE, psa, + PAGE_PROTECTION_FLAGS.PAGE_READWRITE, 0, AGENT_MAX_MSGLEN, mapname); + if (sharedFile.equals(MemorySegment.NULL) || sharedFile.equals(INVALID_HANDLE_VALUE)) { + throw new AgentProxyException("Unable to create shared file mapping: GetLastError() = " + + (int) getLastErrorVarHandle.get(errorState, 0)); + } + + sharedMemory = MapViewOfFile(errorState, sharedFile, FILE_MAP.WRITE, 0, 0, 0); + if (sharedMemory.equals(MemorySegment.NULL)) { + throw new AgentProxyException("Unable to create shared memory mapping: GetLastError() = " + + (int) getLastErrorVarHandle.get(errorState, 0)); + } + sharedMemory = sharedMemory.reinterpret(AGENT_MAX_MSGLEN); + + MemorySegment buf = MemorySegment.ofArray(buffer.buffer); + MemorySegment.copy(buf, 0, sharedMemory, 0, buffer.getLength()); + + MemorySegment cds = COPYDATASTRUCT.allocate(arena); + COPYDATASTRUCT.dwData(cds, AGENT_COPYDATA_ID); + COPYDATASTRUCT.cbData(cds, (int) mapname.byteSize()); + COPYDATASTRUCT.lpData(cds, mapname); + + long rcode = SendMessageA(errorState, hwnd, WM_COPYDATA, MemorySegment.NULL.address(), + cds.address()); + // Dummy read to make sure COPYDATASTRUCT isn't GC'd early + long foo = COPYDATASTRUCT.dwData(cds); + + buffer.rewind(); + if (rcode != 0) { + MemorySegment.copy(sharedMemory, 0, buf, 0, 4); // length + int i = buffer.getInt(); + if (i <= 0 || i > AGENT_MAX_MSGLEN - 4) { + throw new AgentProxyException("Illegal length: " + i); + } + buffer.rewind(); + buffer.checkFreeSize(i); + // checkFreeSize may have created a new array + buf = MemorySegment.ofArray(buffer.buffer); + MemorySegment.copy(sharedMemory, 4, buf, 0, i); + } else { + throw new AgentProxyException( + "SendMessage() returned 0 with cds.dwData: " + Long.toHexString(foo)); + } + } finally { + if (!sharedMemory.equals(MemorySegment.NULL)) + UnmapViewOfFile(errorState, sharedMemory); + if (!sharedFile.equals(MemorySegment.NULL) && !sharedFile.equals(INVALID_HANDLE_VALUE)) + CloseHandle(errorState, sharedFile); + } + } + } +} From 7bf92c76b79c6164a6cf530f316d12add14877d1 Mon Sep 17 00:00:00 2001 From: Jeremy Norris Date: Fri, 20 Feb 2026 11:47:16 -0600 Subject: [PATCH 06/12] Throw an exception if the shared file mapping already exists. --- src/main/java/com/jcraft/jsch/PageantConnector.java | 7 ++++++- src/main/java23/com/jcraft/jsch/PageantFFMConnector.java | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/jcraft/jsch/PageantConnector.java b/src/main/java/com/jcraft/jsch/PageantConnector.java index c65b26d4..3c51683e 100644 --- a/src/main/java/com/jcraft/jsch/PageantConnector.java +++ b/src/main/java/com/jcraft/jsch/PageantConnector.java @@ -36,6 +36,7 @@ import com.sun.jna.platform.win32.WinDef.HWND; import com.sun.jna.platform.win32.WinDef.LPARAM; import com.sun.jna.platform.win32.WinDef.LRESULT; +import com.sun.jna.platform.win32.WinError; import com.sun.jna.platform.win32.WinNT; import com.sun.jna.platform.win32.WinNT.HANDLE; import com.sun.jna.platform.win32.WinUser; @@ -96,9 +97,13 @@ public void query(Buffer buffer) throws AgentProxyException { sharedFile = kernel32.CreateFileMapping(WinBase.INVALID_HANDLE_VALUE, psa, WinNT.PAGE_READWRITE, 0, AGENT_MAX_MSGLEN, mapname); + int lastError = kernel32.GetLastError(); if (sharedFile == null || sharedFile == WinBase.INVALID_HANDLE_VALUE) { throw new AgentProxyException( - "Unable to create shared file mapping: GetLastError() = " + kernel32.GetLastError()); + "Unable to create shared file mapping: GetLastError() = " + lastError); + } + if (lastError == WinError.ERROR_ALREADY_EXISTS) { + throw new AgentProxyException("Shared file mapping already exists"); } sharedMemory = kernel32.MapViewOfFile(sharedFile, WinBase.FILE_MAP_WRITE, 0, 0, 0); diff --git a/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java b/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java index 82c463a2..86d80e1f 100644 --- a/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java +++ b/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java @@ -36,6 +36,7 @@ import static com.jcraft.jsch.windowsapi.windows.win32.ui.windowsandmessaging.Apis.SendMessageA; import static com.jcraft.jsch.windowsapi.windows.win32.ui.windowsandmessaging.Constants.WM_COPYDATA; +import com.jcraft.jsch.windowsapi.windows.win32.foundation.WIN32_ERROR; import com.jcraft.jsch.windowsapi.windows.win32.system.dataexchange.COPYDATASTRUCT; import com.jcraft.jsch.windowsapi.windows.win32.system.memory.FILE_MAP; import com.jcraft.jsch.windowsapi.windows.win32.system.memory.PAGE_PROTECTION_FLAGS; @@ -120,9 +121,13 @@ public void query(Buffer buffer) throws AgentProxyException { sharedFile = CreateFileMappingA(errorState, INVALID_HANDLE_VALUE, psa, PAGE_PROTECTION_FLAGS.PAGE_READWRITE, 0, AGENT_MAX_MSGLEN, mapname); + int lastError = (int) getLastErrorVarHandle.get(errorState, 0); if (sharedFile.equals(MemorySegment.NULL) || sharedFile.equals(INVALID_HANDLE_VALUE)) { - throw new AgentProxyException("Unable to create shared file mapping: GetLastError() = " - + (int) getLastErrorVarHandle.get(errorState, 0)); + throw new AgentProxyException( + "Unable to create shared file mapping: GetLastError() = " + lastError); + } + if (lastError == WIN32_ERROR.ERROR_ALREADY_EXISTS) { + throw new AgentProxyException("Shared file mapping already exists"); } sharedMemory = MapViewOfFile(errorState, sharedFile, FILE_MAP.WRITE, 0, 0, 0); From 6923d66484da13426ab27612f63a762e935f1b2b Mon Sep 17 00:00:00 2001 From: Jeremy Norris Date: Fri, 20 Feb 2026 12:32:43 -0600 Subject: [PATCH 07/12] Only check for NULL when checking for failure from CreateFileMapping() per official Windows API docs. --- src/main/java/com/jcraft/jsch/PageantConnector.java | 4 ++-- src/main/java23/com/jcraft/jsch/PageantFFMConnector.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/jcraft/jsch/PageantConnector.java b/src/main/java/com/jcraft/jsch/PageantConnector.java index 3c51683e..d235f0a8 100644 --- a/src/main/java/com/jcraft/jsch/PageantConnector.java +++ b/src/main/java/com/jcraft/jsch/PageantConnector.java @@ -98,7 +98,7 @@ public void query(Buffer buffer) throws AgentProxyException { sharedFile = kernel32.CreateFileMapping(WinBase.INVALID_HANDLE_VALUE, psa, WinNT.PAGE_READWRITE, 0, AGENT_MAX_MSGLEN, mapname); int lastError = kernel32.GetLastError(); - if (sharedFile == null || sharedFile == WinBase.INVALID_HANDLE_VALUE) { + if (sharedFile == null) { throw new AgentProxyException( "Unable to create shared file mapping: GetLastError() = " + lastError); } @@ -136,7 +136,7 @@ public void query(Buffer buffer) throws AgentProxyException { } finally { if (sharedMemory != null) kernel32.UnmapViewOfFile(sharedMemory); - if (sharedFile != null && sharedFile != WinBase.INVALID_HANDLE_VALUE) + if (sharedFile != null) kernel32.CloseHandle(sharedFile); } } diff --git a/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java b/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java index 86d80e1f..efe01721 100644 --- a/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java +++ b/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java @@ -122,7 +122,7 @@ public void query(Buffer buffer) throws AgentProxyException { sharedFile = CreateFileMappingA(errorState, INVALID_HANDLE_VALUE, psa, PAGE_PROTECTION_FLAGS.PAGE_READWRITE, 0, AGENT_MAX_MSGLEN, mapname); int lastError = (int) getLastErrorVarHandle.get(errorState, 0); - if (sharedFile.equals(MemorySegment.NULL) || sharedFile.equals(INVALID_HANDLE_VALUE)) { + if (sharedFile.equals(MemorySegment.NULL)) { throw new AgentProxyException( "Unable to create shared file mapping: GetLastError() = " + lastError); } @@ -169,7 +169,7 @@ public void query(Buffer buffer) throws AgentProxyException { } finally { if (!sharedMemory.equals(MemorySegment.NULL)) UnmapViewOfFile(errorState, sharedMemory); - if (!sharedFile.equals(MemorySegment.NULL) && !sharedFile.equals(INVALID_HANDLE_VALUE)) + if (!sharedFile.equals(MemorySegment.NULL)) CloseHandle(errorState, sharedFile); } } From 053b1fcf2ade7479f470f3877a3c427eecdcf0d4 Mon Sep 17 00:00:00 2001 From: Jeremy Norris Date: Fri, 20 Feb 2026 14:01:37 -0600 Subject: [PATCH 08/12] Update Pageant shared mapping name to only use the GetCurrentProcessId() and the Java thread id for Java 19+. Thread.getId() isn't final and thus could be overriden, leaving the possibility that it isn't unique. Since virtual threads were only introduced in Java 19, using GetCurrentThreadId() should work for earlier Java releases. --- pom.xml | 1 + src/main/java/com/jcraft/jsch/PageantConnector.java | 6 ++++-- src/main/java23/com/jcraft/jsch/PageantFFMConnector.java | 7 +++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 086189b7..fdedd2ad 100644 --- a/pom.xml +++ b/pom.xml @@ -284,6 +284,7 @@ CreateFileMappingA FindWindowA GetCurrentProcessId + GetCurrentThreadId MapViewOfFile SendMessageA UnmapViewOfFile diff --git a/src/main/java/com/jcraft/jsch/PageantConnector.java b/src/main/java/com/jcraft/jsch/PageantConnector.java index d235f0a8..b6e7e0b9 100644 --- a/src/main/java/com/jcraft/jsch/PageantConnector.java +++ b/src/main/java/com/jcraft/jsch/PageantConnector.java @@ -86,8 +86,10 @@ public void query(Buffer buffer) throws AgentProxyException { throw new AgentProxyException("Pageant is not runnning."); } - String mapname = String.format(Locale.ROOT, "JSchPageantRequest%08x%08x", - kernel32.GetCurrentProcessId(), JavaThreadId.get()); + String threadId = JavaVersion.getVersion() >= 19 + ? String.format(Locale.ROOT, "%08x%08x", kernel32.GetCurrentProcessId(), JavaThreadId.get()) + : String.format(Locale.ROOT, "%08x", kernel32.GetCurrentThreadId()); + String mapname = "JSchPageantRequest" + threadId; HANDLE sharedFile = null; Pointer sharedMemory = null; diff --git a/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java b/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java index efe01721..7c3190f5 100644 --- a/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java +++ b/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java @@ -32,6 +32,7 @@ import static com.jcraft.jsch.windowsapi.windows.win32.system.memory.Apis.MapViewOfFile; import static com.jcraft.jsch.windowsapi.windows.win32.system.memory.Apis.UnmapViewOfFile; import static com.jcraft.jsch.windowsapi.windows.win32.system.threading.Apis.GetCurrentProcessId; +import static com.jcraft.jsch.windowsapi.windows.win32.system.threading.Apis.GetCurrentThreadId; import static com.jcraft.jsch.windowsapi.windows.win32.ui.windowsandmessaging.Apis.FindWindowA; import static com.jcraft.jsch.windowsapi.windows.win32.ui.windowsandmessaging.Apis.SendMessageA; import static com.jcraft.jsch.windowsapi.windows.win32.ui.windowsandmessaging.Constants.WM_COPYDATA; @@ -111,9 +112,11 @@ public void query(Buffer buffer) throws AgentProxyException { throw new AgentProxyException("Pageant is not runnning."); } + String threadId = JavaVersion.getVersion() >= 19 + ? String.format(Locale.ROOT, "%08x%08x", GetCurrentProcessId(), JavaThreadId.get()) + : String.format(Locale.ROOT, "%08x", GetCurrentThreadId()); MemorySegment mapname = - arena.allocateFrom(String.format(Locale.ROOT, "JSchPageantRequest%08x%08x", - GetCurrentProcessId(), JavaThreadId.get()), StandardCharsets.US_ASCII); + arena.allocateFrom("JSchPageantRequest" + threadId, StandardCharsets.US_ASCII); try { // TODO From 04bbf694c8a23afc6c576d938dc2de451fabdabe Mon Sep 17 00:00:00 2001 From: Jeremy Norris Date: Sat, 21 Feb 2026 16:45:40 -0600 Subject: [PATCH 09/12] Make file mapping owned by the user SID, matching PuTTY's behavior. This allows PageantFFMConnector to connect to an unprivileged Pageant when JSch is executed via run as Administrator. --- pom.xml | 17 ++- .../com/jcraft/jsch/PageantFFMConnector.java | 112 ++++++++++++++++-- 2 files changed, 121 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index fdedd2ad..698d74a7 100644 --- a/pom.xml +++ b/pom.xml @@ -281,23 +281,38 @@ false CloseHandle + CopySid CreateFileMappingA FindWindowA GetCurrentProcessId - GetCurrentThreadId + GetLengthSid + GetTokenInformation + InitializeSecurityDescriptor + IsValidSid MapViewOfFile + OpenProcess + OpenProcessToken SendMessageA + SetSecurityDescriptorOwner UnmapViewOfFile COPYDATASTRUCT + SECURITY_ATTRIBUTES + SECURITY_DESCRIPTOR + SID_AND_ATTRIBUTES + TOKEN_USER FILE_MAP PAGE_PROTECTION_FLAGS + TOKEN_ACCESS_MASK + TOKEN_INFORMATION_CLASS INVALID_HANDLE_VALUE + MAXIMUM_ALLOWED + SECURITY_DESCRIPTOR_REVISION WM_COPYDATA diff --git a/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java b/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java index 7c3190f5..c82faa7f 100644 --- a/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java +++ b/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java @@ -28,16 +28,31 @@ import static com.jcraft.jsch.windowsapi.windows.win32.foundation.Apis.CloseHandle; import static com.jcraft.jsch.windowsapi.windows.win32.foundation.Constants.INVALID_HANDLE_VALUE; +import static com.jcraft.jsch.windowsapi.windows.win32.security.Apis.CopySid; +import static com.jcraft.jsch.windowsapi.windows.win32.security.Apis.GetLengthSid; +import static com.jcraft.jsch.windowsapi.windows.win32.security.Apis.GetTokenInformation; +import static com.jcraft.jsch.windowsapi.windows.win32.security.Apis.InitializeSecurityDescriptor; +import static com.jcraft.jsch.windowsapi.windows.win32.security.Apis.IsValidSid; +import static com.jcraft.jsch.windowsapi.windows.win32.security.Apis.SetSecurityDescriptorOwner; import static com.jcraft.jsch.windowsapi.windows.win32.system.memory.Apis.CreateFileMappingA; import static com.jcraft.jsch.windowsapi.windows.win32.system.memory.Apis.MapViewOfFile; import static com.jcraft.jsch.windowsapi.windows.win32.system.memory.Apis.UnmapViewOfFile; +import static com.jcraft.jsch.windowsapi.windows.win32.system.systemservices.Constants.MAXIMUM_ALLOWED; +import static com.jcraft.jsch.windowsapi.windows.win32.system.systemservices.Constants.SECURITY_DESCRIPTOR_REVISION; import static com.jcraft.jsch.windowsapi.windows.win32.system.threading.Apis.GetCurrentProcessId; -import static com.jcraft.jsch.windowsapi.windows.win32.system.threading.Apis.GetCurrentThreadId; +import static com.jcraft.jsch.windowsapi.windows.win32.system.threading.Apis.OpenProcess; +import static com.jcraft.jsch.windowsapi.windows.win32.system.threading.Apis.OpenProcessToken; import static com.jcraft.jsch.windowsapi.windows.win32.ui.windowsandmessaging.Apis.FindWindowA; import static com.jcraft.jsch.windowsapi.windows.win32.ui.windowsandmessaging.Apis.SendMessageA; import static com.jcraft.jsch.windowsapi.windows.win32.ui.windowsandmessaging.Constants.WM_COPYDATA; import com.jcraft.jsch.windowsapi.windows.win32.foundation.WIN32_ERROR; +import com.jcraft.jsch.windowsapi.windows.win32.security.SECURITY_ATTRIBUTES; +import com.jcraft.jsch.windowsapi.windows.win32.security.SECURITY_DESCRIPTOR; +import com.jcraft.jsch.windowsapi.windows.win32.security.SID_AND_ATTRIBUTES; +import com.jcraft.jsch.windowsapi.windows.win32.security.TOKEN_ACCESS_MASK; +import com.jcraft.jsch.windowsapi.windows.win32.security.TOKEN_INFORMATION_CLASS; +import com.jcraft.jsch.windowsapi.windows.win32.security.TOKEN_USER; import com.jcraft.jsch.windowsapi.windows.win32.system.dataexchange.COPYDATASTRUCT; import com.jcraft.jsch.windowsapi.windows.win32.system.memory.FILE_MAP; import com.jcraft.jsch.windowsapi.windows.win32.system.memory.PAGE_PROTECTION_FLAGS; @@ -46,6 +61,7 @@ import java.lang.foreign.MemoryLayout; import java.lang.foreign.MemorySegment; import java.lang.foreign.StructLayout; +import java.lang.foreign.ValueLayout; import java.lang.invoke.VarHandle; import java.nio.charset.StandardCharsets; import java.util.Locale; @@ -70,6 +86,7 @@ public PageantFFMConnector() throws AgentProxyException { // Force class initialization to catch UnsatisfiedLinkError Object foo = com.jcraft.jsch.windowsapi.windows.win32.foundation.Apis.CloseHandle$handle(); + foo = com.jcraft.jsch.windowsapi.windows.win32.security.Apis.CopySid$handle(); foo = com.jcraft.jsch.windowsapi.windows.win32.system.memory.Apis.CreateFileMappingA$handle(); foo = com.jcraft.jsch.windowsapi.windows.win32.system.threading.Apis .GetCurrentProcessId$handle(); @@ -112,16 +129,28 @@ public void query(Buffer buffer) throws AgentProxyException { throw new AgentProxyException("Pageant is not runnning."); } - String threadId = JavaVersion.getVersion() >= 19 - ? String.format(Locale.ROOT, "%08x%08x", GetCurrentProcessId(), JavaThreadId.get()) - : String.format(Locale.ROOT, "%08x", GetCurrentThreadId()); + MemorySegment usersid = getUserSid(arena, errorState); + MemorySegment psd = SECURITY_DESCRIPTOR.allocate(arena); + if (InitializeSecurityDescriptor(errorState, psd, SECURITY_DESCRIPTOR_REVISION) == 0) { + throw new AgentProxyException("Unable to InitializeSecurityDescriptor(): GetLastError() = " + + (int) getLastErrorVarHandle.get(errorState, 0)); + } + if (SetSecurityDescriptorOwner(errorState, psd, usersid, 0) == 0) { + throw new AgentProxyException("Unable to SetSecurityDescriptorOwner(): GetLastError() = " + + (int) getLastErrorVarHandle.get(errorState, 0)); + } + + MemorySegment psa = SECURITY_ATTRIBUTES.allocate(arena); + SECURITY_ATTRIBUTES.nLength(psa, (int) SECURITY_ATTRIBUTES.sizeof()); + SECURITY_ATTRIBUTES.bInheritHandle(psa, 1); + SECURITY_ATTRIBUTES.lpSecurityDescriptor(psa, psd); + + String threadId = String.format(Locale.ROOT, "%08x%08x", GetCurrentProcessId(), + Thread.currentThread().threadId()); MemorySegment mapname = arena.allocateFrom("JSchPageantRequest" + threadId, StandardCharsets.US_ASCII); try { - // TODO - MemorySegment psa = MemorySegment.NULL; - sharedFile = CreateFileMappingA(errorState, INVALID_HANDLE_VALUE, psa, PAGE_PROTECTION_FLAGS.PAGE_READWRITE, 0, AGENT_MAX_MSGLEN, mapname); int lastError = (int) getLastErrorVarHandle.get(errorState, 0); @@ -177,4 +206,73 @@ public void query(Buffer buffer) throws AgentProxyException { } } } + + private MemorySegment getUserSid(Arena arena, MemorySegment errorState) + throws AgentProxyException { + MemorySegment proc = MemorySegment.NULL; + MemorySegment tok = MemorySegment.NULL; + + try { + proc = OpenProcess(errorState, MAXIMUM_ALLOWED, 0, GetCurrentProcessId()); + if (proc.equals(MemorySegment.NULL)) { + throw new AgentProxyException("Unable to OpenProcess(): GetLastError() = " + + (int) getLastErrorVarHandle.get(errorState, 0)); + } + + MemorySegment ptok = arena.allocate(ValueLayout.ADDRESS); + if (OpenProcessToken(errorState, proc, TOKEN_ACCESS_MASK.TOKEN_QUERY, ptok) == 0) { + throw new AgentProxyException("Unable to OpenProcessToken(): GetLastError() = " + + (int) getLastErrorVarHandle.get(errorState, 0)); + } + + tok = ptok.get(ValueLayout.ADDRESS, 0); + if (tok.equals(MemorySegment.NULL)) { + throw new AgentProxyException("ProcessToken is NULL"); + } + + MemorySegment ptoklen = arena.allocate(ValueLayout.JAVA_INT); + if (GetTokenInformation(errorState, tok, TOKEN_INFORMATION_CLASS.TokenUser, + MemorySegment.NULL, 0, ptoklen) == 0) { + int lastError = (int) getLastErrorVarHandle.get(errorState, 0); + if (lastError != WIN32_ERROR.ERROR_INSUFFICIENT_BUFFER) { + throw new AgentProxyException( + "Unable to GetTokenInformation() toklen: GetLastError() = " + lastError); + } + } + + long toklen = ptoklen.get(ValueLayout.JAVA_INT, 0) & 0xffffffffL; + if (toklen < TOKEN_USER.sizeof()) { + throw new AgentProxyException(String.format(Locale.ROOT, + "toklen (%d) < sizeof(TOKEN_USER) (%d)", toklen, TOKEN_USER.sizeof())); + } + + MemorySegment user = arena.allocate(toklen, ValueLayout.ADDRESS.byteAlignment()); + if (GetTokenInformation(errorState, tok, TOKEN_INFORMATION_CLASS.TokenUser, user, + (int) toklen, ptoklen) == 0) { + throw new AgentProxyException("Unable to GetTokenInformation() user: GetLastError() = " + + (int) getLastErrorVarHandle.get(errorState, 0)); + } + + MemorySegment psid = SID_AND_ATTRIBUTES.Sid(TOKEN_USER.User(user)); + if (IsValidSid(psid) == 0) { + throw new AgentProxyException("IsValidSid() failed"); + } + + long sidlen = GetLengthSid(psid) & 0xffffffffL; + MemorySegment usersid = arena.allocate(sidlen, ValueLayout.ADDRESS.byteAlignment()); + if (CopySid(errorState, (int) sidlen, usersid, psid) == 0) { + throw new AgentProxyException("Unable to CopySid(): GetLastError() = " + + (int) getLastErrorVarHandle.get(errorState, 0)); + } + + return usersid; + } finally { + if (!tok.equals(MemorySegment.NULL)) { + CloseHandle(errorState, tok); + } + if (!proc.equals(MemorySegment.NULL)) { + CloseHandle(errorState, proc); + } + } + } } From 7a53813883c049c14eb8ffa5f07ba5e4b0cee600 Mon Sep 17 00:00:00 2001 From: Jeremy Norris Date: Sat, 21 Feb 2026 18:46:41 -0600 Subject: [PATCH 10/12] Normalize Exception messages. --- src/main/java/com/jcraft/jsch/PageantConnector.java | 4 ++-- src/main/java23/com/jcraft/jsch/PageantFFMConnector.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/jcraft/jsch/PageantConnector.java b/src/main/java/com/jcraft/jsch/PageantConnector.java index b6e7e0b9..326d0bdf 100644 --- a/src/main/java/com/jcraft/jsch/PageantConnector.java +++ b/src/main/java/com/jcraft/jsch/PageantConnector.java @@ -102,7 +102,7 @@ public void query(Buffer buffer) throws AgentProxyException { int lastError = kernel32.GetLastError(); if (sharedFile == null) { throw new AgentProxyException( - "Unable to create shared file mapping: GetLastError() = " + lastError); + "Unable to CreateFileMapping(): GetLastError() = " + lastError); } if (lastError == WinError.ERROR_ALREADY_EXISTS) { throw new AgentProxyException("Shared file mapping already exists"); @@ -111,7 +111,7 @@ public void query(Buffer buffer) throws AgentProxyException { sharedMemory = kernel32.MapViewOfFile(sharedFile, WinBase.FILE_MAP_WRITE, 0, 0, 0); if (sharedMemory == null) { throw new AgentProxyException( - "Unable to create shared memory mapping: GetLastError() = " + kernel32.GetLastError()); + "Unable to MapViewOfFile(): GetLastError() = " + kernel32.GetLastError()); } sharedMemory.write(0, buffer.buffer, 0, buffer.getLength()); diff --git a/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java b/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java index c82faa7f..fcfbc00b 100644 --- a/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java +++ b/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java @@ -156,7 +156,7 @@ public void query(Buffer buffer) throws AgentProxyException { int lastError = (int) getLastErrorVarHandle.get(errorState, 0); if (sharedFile.equals(MemorySegment.NULL)) { throw new AgentProxyException( - "Unable to create shared file mapping: GetLastError() = " + lastError); + "Unable to CreateFileMapping(): GetLastError() = " + lastError); } if (lastError == WIN32_ERROR.ERROR_ALREADY_EXISTS) { throw new AgentProxyException("Shared file mapping already exists"); @@ -164,7 +164,7 @@ public void query(Buffer buffer) throws AgentProxyException { sharedMemory = MapViewOfFile(errorState, sharedFile, FILE_MAP.WRITE, 0, 0, 0); if (sharedMemory.equals(MemorySegment.NULL)) { - throw new AgentProxyException("Unable to create shared memory mapping: GetLastError() = " + throw new AgentProxyException("Unable to MapViewOfFile(): GetLastError() = " + (int) getLastErrorVarHandle.get(errorState, 0)); } sharedMemory = sharedMemory.reinterpret(AGENT_MAX_MSGLEN); From f7947cba55b1f1494ac16adb4457bea8889df969 Mon Sep 17 00:00:00 2001 From: Jeremy Norris Date: Sun, 22 Feb 2026 11:58:39 -0600 Subject: [PATCH 11/12] Add GetLastError() wrapper method. --- .../com/jcraft/jsch/PageantFFMConnector.java | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java b/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java index fcfbc00b..09974af4 100644 --- a/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java +++ b/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java @@ -133,11 +133,11 @@ public void query(Buffer buffer) throws AgentProxyException { MemorySegment psd = SECURITY_DESCRIPTOR.allocate(arena); if (InitializeSecurityDescriptor(errorState, psd, SECURITY_DESCRIPTOR_REVISION) == 0) { throw new AgentProxyException("Unable to InitializeSecurityDescriptor(): GetLastError() = " - + (int) getLastErrorVarHandle.get(errorState, 0)); + + GetLastError(errorState)); } if (SetSecurityDescriptorOwner(errorState, psd, usersid, 0) == 0) { - throw new AgentProxyException("Unable to SetSecurityDescriptorOwner(): GetLastError() = " - + (int) getLastErrorVarHandle.get(errorState, 0)); + throw new AgentProxyException( + "Unable to SetSecurityDescriptorOwner(): GetLastError() = " + GetLastError(errorState)); } MemorySegment psa = SECURITY_ATTRIBUTES.allocate(arena); @@ -153,7 +153,7 @@ public void query(Buffer buffer) throws AgentProxyException { try { sharedFile = CreateFileMappingA(errorState, INVALID_HANDLE_VALUE, psa, PAGE_PROTECTION_FLAGS.PAGE_READWRITE, 0, AGENT_MAX_MSGLEN, mapname); - int lastError = (int) getLastErrorVarHandle.get(errorState, 0); + int lastError = GetLastError(errorState); if (sharedFile.equals(MemorySegment.NULL)) { throw new AgentProxyException( "Unable to CreateFileMapping(): GetLastError() = " + lastError); @@ -164,8 +164,8 @@ public void query(Buffer buffer) throws AgentProxyException { sharedMemory = MapViewOfFile(errorState, sharedFile, FILE_MAP.WRITE, 0, 0, 0); if (sharedMemory.equals(MemorySegment.NULL)) { - throw new AgentProxyException("Unable to MapViewOfFile(): GetLastError() = " - + (int) getLastErrorVarHandle.get(errorState, 0)); + throw new AgentProxyException( + "Unable to MapViewOfFile(): GetLastError() = " + GetLastError(errorState)); } sharedMemory = sharedMemory.reinterpret(AGENT_MAX_MSGLEN); @@ -215,14 +215,14 @@ private MemorySegment getUserSid(Arena arena, MemorySegment errorState) try { proc = OpenProcess(errorState, MAXIMUM_ALLOWED, 0, GetCurrentProcessId()); if (proc.equals(MemorySegment.NULL)) { - throw new AgentProxyException("Unable to OpenProcess(): GetLastError() = " - + (int) getLastErrorVarHandle.get(errorState, 0)); + throw new AgentProxyException( + "Unable to OpenProcess(): GetLastError() = " + GetLastError(errorState)); } MemorySegment ptok = arena.allocate(ValueLayout.ADDRESS); if (OpenProcessToken(errorState, proc, TOKEN_ACCESS_MASK.TOKEN_QUERY, ptok) == 0) { - throw new AgentProxyException("Unable to OpenProcessToken(): GetLastError() = " - + (int) getLastErrorVarHandle.get(errorState, 0)); + throw new AgentProxyException( + "Unable to OpenProcessToken(): GetLastError() = " + GetLastError(errorState)); } tok = ptok.get(ValueLayout.ADDRESS, 0); @@ -233,7 +233,7 @@ private MemorySegment getUserSid(Arena arena, MemorySegment errorState) MemorySegment ptoklen = arena.allocate(ValueLayout.JAVA_INT); if (GetTokenInformation(errorState, tok, TOKEN_INFORMATION_CLASS.TokenUser, MemorySegment.NULL, 0, ptoklen) == 0) { - int lastError = (int) getLastErrorVarHandle.get(errorState, 0); + int lastError = GetLastError(errorState); if (lastError != WIN32_ERROR.ERROR_INSUFFICIENT_BUFFER) { throw new AgentProxyException( "Unable to GetTokenInformation() toklen: GetLastError() = " + lastError); @@ -249,8 +249,8 @@ private MemorySegment getUserSid(Arena arena, MemorySegment errorState) MemorySegment user = arena.allocate(toklen, ValueLayout.ADDRESS.byteAlignment()); if (GetTokenInformation(errorState, tok, TOKEN_INFORMATION_CLASS.TokenUser, user, (int) toklen, ptoklen) == 0) { - throw new AgentProxyException("Unable to GetTokenInformation() user: GetLastError() = " - + (int) getLastErrorVarHandle.get(errorState, 0)); + throw new AgentProxyException( + "Unable to GetTokenInformation() user: GetLastError() = " + GetLastError(errorState)); } MemorySegment psid = SID_AND_ATTRIBUTES.Sid(TOKEN_USER.User(user)); @@ -261,8 +261,8 @@ private MemorySegment getUserSid(Arena arena, MemorySegment errorState) long sidlen = GetLengthSid(psid) & 0xffffffffL; MemorySegment usersid = arena.allocate(sidlen, ValueLayout.ADDRESS.byteAlignment()); if (CopySid(errorState, (int) sidlen, usersid, psid) == 0) { - throw new AgentProxyException("Unable to CopySid(): GetLastError() = " - + (int) getLastErrorVarHandle.get(errorState, 0)); + throw new AgentProxyException( + "Unable to CopySid(): GetLastError() = " + GetLastError(errorState)); } return usersid; @@ -275,4 +275,8 @@ private MemorySegment getUserSid(Arena arena, MemorySegment errorState) } } } + + private int GetLastError(MemorySegment errorState) { + return (int) getLastErrorVarHandle.get(errorState, 0); + } } From 6f5c7186e9c3bc3c939f23e6d14cc296b015303c Mon Sep 17 00:00:00 2001 From: Jeremy Norris Date: Mon, 23 Feb 2026 06:13:59 -0600 Subject: [PATCH 12/12] Use try/finally when cleaning up multiple resources in case one cleanup fails and throws an exception, the second resource cleanup will still execute. --- .../com/jcraft/jsch/PageantConnector.java | 13 +++++++--- .../com/jcraft/jsch/PageantFFMConnector.java | 26 ++++++++++++------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/jcraft/jsch/PageantConnector.java b/src/main/java/com/jcraft/jsch/PageantConnector.java index 326d0bdf..fd697ab8 100644 --- a/src/main/java/com/jcraft/jsch/PageantConnector.java +++ b/src/main/java/com/jcraft/jsch/PageantConnector.java @@ -136,10 +136,15 @@ public void query(Buffer buffer) throws AgentProxyException { "SendMessage() returned 0 with cds.dwData: " + Long.toHexString(foo)); } } finally { - if (sharedMemory != null) - kernel32.UnmapViewOfFile(sharedMemory); - if (sharedFile != null) - kernel32.CloseHandle(sharedFile); + try { + if (sharedMemory != null) { + kernel32.UnmapViewOfFile(sharedMemory); + } + } finally { + if (sharedFile != null) { + kernel32.CloseHandle(sharedFile); + } + } } } diff --git a/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java b/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java index 09974af4..93e01085 100644 --- a/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java +++ b/src/main/java23/com/jcraft/jsch/PageantFFMConnector.java @@ -199,10 +199,15 @@ public void query(Buffer buffer) throws AgentProxyException { "SendMessage() returned 0 with cds.dwData: " + Long.toHexString(foo)); } } finally { - if (!sharedMemory.equals(MemorySegment.NULL)) - UnmapViewOfFile(errorState, sharedMemory); - if (!sharedFile.equals(MemorySegment.NULL)) - CloseHandle(errorState, sharedFile); + try { + if (!sharedMemory.equals(MemorySegment.NULL)) { + UnmapViewOfFile(errorState, sharedMemory); + } + } finally { + if (!sharedFile.equals(MemorySegment.NULL)) { + CloseHandle(errorState, sharedFile); + } + } } } } @@ -267,11 +272,14 @@ private MemorySegment getUserSid(Arena arena, MemorySegment errorState) return usersid; } finally { - if (!tok.equals(MemorySegment.NULL)) { - CloseHandle(errorState, tok); - } - if (!proc.equals(MemorySegment.NULL)) { - CloseHandle(errorState, proc); + try { + if (!tok.equals(MemorySegment.NULL)) { + CloseHandle(errorState, tok); + } + } finally { + if (!proc.equals(MemorySegment.NULL)) { + CloseHandle(errorState, proc); + } } } }