From fc9a784012d75b0b53a8b412a28cf5fb9cd11bef Mon Sep 17 00:00:00 2001 From: eran shalom Date: Mon, 20 Apr 2026 11:57:05 +0300 Subject: [PATCH 1/7] Harden Windows build setup - Prefer VS 2017/2019/2022 over newer installs (e.g. VS 2026) when vswhere reports both, so a host with a newer VS installed alongside VS 2022 still builds with v143. Without this the downstream Select-String version checks miss and the VS 2015 fallback dereferences a null VS140COMNTOOLS. - Pin vcpkg's CMake with VCPKG_VISUAL_STUDIO_PATH and VCPKG_PLATFORM_TOOLSET=v143 so it uses the same toolset as the .vcxproj files, avoiding MSB8040 (Spectre-libs missing for v145) when vcpkg would otherwise pick up a newer VS. - Fall back to the repo root for OpenSSH-build.ps1 -destination when \$env:WORKSPACE is unset. CI still has its WORKSPACE value, and no longer invokes this script directly anyway. - Document Windows build prerequisites in README.txt: VS 2022 Desktop C++ workload, v143 Spectre-mitigated libs, Git, the one-time vcpkg bootstrap/integrate step, and the need to run from an elevated PowerShell. --- contrib/win32/openssh/OpenSSH-build.ps1 | 2 +- contrib/win32/openssh/OpenSSHBuildHelper.psm1 | 10 +++- contrib/win32/openssh/README.txt | 46 ++++++++++++++++++- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/contrib/win32/openssh/OpenSSH-build.ps1 b/contrib/win32/openssh/OpenSSH-build.ps1 index 22c6794a84de..190387a45408 100644 --- a/contrib/win32/openssh/OpenSSH-build.ps1 +++ b/contrib/win32/openssh/OpenSSH-build.ps1 @@ -2,7 +2,7 @@ # PowerShell Script to clone, build and package PowerShell from specified fork and branch param ( [string] $repolocation = "$PSScriptRoot\..\..\..", - [string] $destination = "$env:WORKSPACE", + [string] $destination = $(if ($env:WORKSPACE) { $env:WORKSPACE } else { "$PSScriptRoot\..\..\.." }), [ValidateSet('x86', 'x64', 'arm64', 'arm')] [String]$NativeHostArch = 'x64', [ValidateSet('Debug', 'Release')] diff --git a/contrib/win32/openssh/OpenSSHBuildHelper.psm1 b/contrib/win32/openssh/OpenSSHBuildHelper.psm1 index 61a7c4502e13..9958df352104 100644 --- a/contrib/win32/openssh/OpenSSHBuildHelper.psm1 +++ b/contrib/win32/openssh/OpenSSHBuildHelper.psm1 @@ -559,6 +559,11 @@ function Start-OpenSSHBuild $VisualStudioPath = Get-VisualStudioPath -NativeHostArch $NativeHostArch if ($null -ne $VisualStudioPath) { $msbuildCmd = Get-MSBuildPath -VSInstallPath $VisualStudioPath + # Pin vcpkg's CMake to the same VS install / toolset (v143) as the + # OpenSSH vcxproj files, so manifest-mode auto-install doesn't pick a + # newer VS (e.g. VS 2026) whose v14x toolset is unsupported here. + $env:VCPKG_VISUAL_STUDIO_PATH = $VisualStudioPath + $env:VCPKG_PLATFORM_TOOLSET = "v143" } else { $msbuildCmd = Get-VS2015BuildToolPath @@ -614,7 +619,10 @@ function Get-VisualStudioPath { $VSPaths = (& $vsWherePath -products * -requires $requiredVCtools -property installationPath) # for some reason, VSWhere does not seem to find MSBuild so check manually if ($null -ne $VSPaths) { - foreach ($VSPath in $VSPaths) { + # Prefer supported versions (2022/2019/2017) over newer unsupported ones (e.g., 2026) + $preferred = @($VSPaths | Where-Object { $_ -match '\\(2022|2019|2017)\\' }) + $ordered = $preferred + @($VSPaths | Where-Object { $_ -notmatch '\\(2022|2019|2017)\\' }) + foreach ($VSPath in $ordered) { if (Get-MSBuildPath -VSInstallPath $VSPath) { return $VSPath } diff --git a/contrib/win32/openssh/README.txt b/contrib/win32/openssh/README.txt index 14b250eb6d64..18eb6bed0649 100644 --- a/contrib/win32/openssh/README.txt +++ b/contrib/win32/openssh/README.txt @@ -1,4 +1,4 @@ -Custom paths for the visual studio projects are defined in paths.targets. +Custom paths for the visual studio projects are defined in paths.targets. All projects import this targets file, and it should be in the same directory as the project. @@ -10,12 +10,54 @@ OpenSSH-Lib-Path = The directory path of the location to which libra LibreSSL-x86-Path = The directory path of LibreSSL statically compiled for x86 platform. LibreSSL-x64-Path = The directory path of LibreSSL statically compiled for x64 platform. +Prerequisites +------------- + +Before building OpenSSH for Windows, install the following: + +1. Visual Studio 2022 (Community, Professional, or Build Tools). + Required components (Visual Studio Installer -> Modify): + - Workload: "Desktop development with C++" + This installs MSBuild, the v143 toolset, and the Windows 10/11 SDK. + - Individual component: "MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)" + Required because the vcpkg x64-custom triplet compiles + dependencies (LibreSSL, libfido2, zlib) with /Qspectre, which + demands matching Spectre-mitigated runtime libraries. + - For ARM64 builds, also install "MSVC v143 - VS 2022 C++ ARM64 Spectre-mitigated libs". + + Note: If a newer Visual Studio (e.g. VS 2026) is also installed, + OpenSSH-build.ps1 prefers VS 2022 and pins vcpkg's CMake to the + same install / v143 toolset automatically. + +2. Git for Windows. + The build script expects git.exe to be on PATH (it will add + "%ProgramFiles%\Git\cmd" to the machine PATH if missing). + +3. vcpkg (one-time bootstrap). + Dependencies (LibreSSL, libfido2, zlib, libcbor) are managed via a + vcpkg manifest (vcpkg.json). MSBuild auto-installs them at build + time, but vcpkg must be cloned, bootstrapped, and integrated first: + + git clone https://github.com/microsoft/vcpkg + cd vcpkg + .\bootstrap-vcpkg.bat + .\vcpkg.exe integrate install + + "vcpkg integrate install" registers vcpkg's MSBuild props user-wide; + after that, every OpenSSH-build.ps1 run picks up the manifest + automatically. No need to run "vcpkg install" manually. + +4. Administrator PowerShell. + The build script updates the machine PATH (to add Git / Chocolatey) + and may install the Windows SDK via Chocolatey if missing. Run the + build from an elevated PowerShell session. + Notes on FIDO2 support ---------------------- * How to build: - - Open Windows PowerShell. + - Open Windows PowerShell as Administrator. - Build OpenSSH for Windows: From c55ef2349c6b56c87d23f6e327f500f4b56fbd71 Mon Sep 17 00:00:00 2001 From: eran shalom Date: Tue, 21 Apr 2026 08:56:17 +0300 Subject: [PATCH 2/7] Prefer VS 2022 specifically, not 2017/2019/2022 as a group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The .vcxproj files pin PlatformToolset v143, which ships with VS 2022. VS 2017 (v141) and VS 2019 (v142) would need v143 build tools sideloaded to build this, which is a non-default setup — so they shouldn't be treated as equals to VS 2022 in the preference order. --- contrib/win32/openssh/OpenSSHBuildHelper.psm1 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/contrib/win32/openssh/OpenSSHBuildHelper.psm1 b/contrib/win32/openssh/OpenSSHBuildHelper.psm1 index 9958df352104..88ca67f720b9 100644 --- a/contrib/win32/openssh/OpenSSHBuildHelper.psm1 +++ b/contrib/win32/openssh/OpenSSHBuildHelper.psm1 @@ -619,9 +619,11 @@ function Get-VisualStudioPath { $VSPaths = (& $vsWherePath -products * -requires $requiredVCtools -property installationPath) # for some reason, VSWhere does not seem to find MSBuild so check manually if ($null -ne $VSPaths) { - # Prefer supported versions (2022/2019/2017) over newer unsupported ones (e.g., 2026) - $preferred = @($VSPaths | Where-Object { $_ -match '\\(2022|2019|2017)\\' }) - $ordered = $preferred + @($VSPaths | Where-Object { $_ -notmatch '\\(2022|2019|2017)\\' }) + # Prefer VS 2022 — the .vcxproj files pin v143, + # which ships with VS 2022. Older VS (2017=v141, 2019=v142) would need v143 build tools + # sideloaded; newer VS (e.g. 2026) defaults to v145 which isn't supported here. + $preferred = @($VSPaths | Where-Object { $_ -match '\\2022\\' }) + $ordered = $preferred + @($VSPaths | Where-Object { $_ -notmatch '\\2022\\' }) foreach ($VSPath in $ordered) { if (Get-MSBuildPath -VSInstallPath $VSPath) { return $VSPath From f7e73fc438f52b613067b06bb1a2bdbe9097dbf7 Mon Sep 17 00:00:00 2001 From: eran shalom Date: Mon, 20 Apr 2026 12:27:16 +0300 Subject: [PATCH 3/7] Fix format-string mismatch in load_user_profile debug log The debug3 on LoadUserProfileW failure had format "%s %S %d" (three specifiers) but only two arguments. %S consumed GetLastError()'s DWORD as a wide-string pointer and wcsnlen dereferenced it, crashing sshd-session post-auth. Only visible where LoadUserProfileW actually fails (e.g. WinPE, which has no user-profile service), so regular Windows was unaffected. --- contrib/win32/win32compat/win32_usertoken_utils.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/win32/win32compat/win32_usertoken_utils.c b/contrib/win32/win32compat/win32_usertoken_utils.c index 2b8ede0d43c8..7d932c97603e 100644 --- a/contrib/win32/win32compat/win32_usertoken_utils.c +++ b/contrib/win32/win32compat/win32_usertoken_utils.c @@ -432,7 +432,7 @@ load_user_profile(HANDLE user_token, char* user) EnablePrivilege("SeBackupPrivilege", 1); EnablePrivilege("SeRestorePrivilege", 1); if (LoadUserProfileW(user_token, &profileInfo) == FALSE) { - debug3("%s: LoadUserProfileW() failed for user %S with error %d.", __FUNCTION__, GetLastError()); + debug3("%s: LoadUserProfileW() failed for user %S with error %d.", __FUNCTION__, user_name, GetLastError()); } EnablePrivilege("SeBackupPrivilege", 0); EnablePrivilege("SeRestorePrivilege", 0); From a650ee0091e8fbbd9f2ded2f50e7f8518e85a902 Mon Sep 17 00:00:00 2001 From: eran shalom Date: Mon, 20 Apr 2026 11:42:44 +0300 Subject: [PATCH 4/7] Delay-load user32 in sshd-auth to survive restricted window stations sshd-auth runs as a privsep helper under a different user with no desktop/window-station access. user32's DllMain binds to the process window station and fails with STATUS_DLL_INIT_FAILED in restricted environments like WinPE, crashing the helper before auth can run. Add delayimp.lib and /DELAYLOAD:user32.dll so user32 is only loaded when one of its APIs is actually called. sshd-auth's only transitive user32 references come from console.c's ConRestoreViewRect_NoPtyHack (ShowWindow / GetWindowPlacement), which sshd-auth never executes, so in practice user32 is never loaded at all. The ItemDefinitionGroup carrying /DELAYLOAD is placed after Microsoft.Cpp.props, where the Link item type becomes defined. --- contrib/win32/openssh/sshd-auth.vcxproj | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/contrib/win32/openssh/sshd-auth.vcxproj b/contrib/win32/openssh/sshd-auth.vcxproj index fd3a28e06214..695afc0fb9dc 100644 --- a/contrib/win32/openssh/sshd-auth.vcxproj +++ b/contrib/win32/openssh/sshd-auth.vcxproj @@ -1,6 +1,20 @@ + + + bcrypt.lib;Userenv.lib;Crypt32.lib;Ws2_32.lib;Secur32.lib;Shlwapi.lib;kernel32.lib;user32.lib;delayimp.lib;advapi32.lib;Netapi32.lib;Rpcrt4.lib;ntdll.lib + Debug @@ -112,6 +126,11 @@ true + + + /DELAYLOAD:user32.dll %(AdditionalOptions) + + From 80c0154cd45d2956dab65f937478e03f55fc576c Mon Sep 17 00:00:00 2001 From: eran shalom Date: Wed, 29 Apr 2026 15:51:46 +0300 Subject: [PATCH 5/7] Route s4u logon to MSV1_0 on non-domain-joined hosts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KERB_S4U_LOGON requires a KDC, so it cannot succeed on a workgroup machine. Prefixed names ("DOMAIN\user") were being routed to the Kerberos package regardless of host join state — failing on WinPE (whose SAM hive retains a "minwinpc" domain that diverges from the runtime computer name) and on any non-joined host where a client happens to send a qualified username. Gate Kerberos selection on NetGetJoinInformation. When the host is not domain-joined, route through MSV1_0 and skip past any leading backslash so the bare SAM name reaches LsaLogonUser. The auth package and message struct are now selected by the same predicate, so they cannot disagree. Also add a debug3 in get_passwd surfacing the domain/computer name pair, to make future SAM-vs-GetComputerName divergences easier to diagnose. --- .../win32/win32compat/win32_usertoken_utils.c | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/contrib/win32/win32compat/win32_usertoken_utils.c b/contrib/win32/win32compat/win32_usertoken_utils.c index 7d932c97603e..656bb972e3fb 100644 --- a/contrib/win32/win32compat/win32_usertoken_utils.c +++ b/contrib/win32/win32compat/win32_usertoken_utils.c @@ -94,6 +94,25 @@ EnablePrivilege(const char *privName, int enabled) return; } +BOOL +is_domain_joined_machine(void){ + BOOL result = FALSE; + + LPWSTR name = NULL; + NETSETUP_JOIN_STATUS s = NetSetupUnknownStatus; + DWORD api_res = NetGetJoinInformation(NULL, &name, &s); + if (api_res == NERR_Success) { + result = s == NetSetupDomainName; + NetApiBufferFree(name); + name = NULL; + } + else{ + debug("%s: NetGetJoinInformation() failed. Error %d.", __FUNCTION__, api_res); + } + debug("%s: NetGetJoinInformation(). Join result %d", __FUNCTION__, result); + return result; +} + HANDLE generate_s4u_user_token(wchar_t* user_cpn, int impersonation) { HANDLE lsa_handle = NULL, token = NULL; @@ -112,7 +131,8 @@ generate_s4u_user_token(wchar_t* user_cpn, int impersonation) { * so only the only two formats are a NetBiosDomain\SamAccountName which is * a domain account or just SamAccountName in which is a local account */ BOOL domain_user = wcschr(user_cpn, L'\\') != NULL; - + BOOL use_kerberos = domain_user && is_domain_joined_machine(); + /* initialize connection to local security provider */ if (impersonation) { @@ -132,11 +152,11 @@ generate_s4u_user_token(wchar_t* user_cpn, int impersonation) { goto done; } - InitLsaString(&auth_package_name, (domain_user) ? MICROSOFT_KERBEROS_NAME_A : MSV1_0_PACKAGE_NAME); + InitLsaString(&auth_package_name, use_kerberos ? MICROSOFT_KERBEROS_NAME_A : MSV1_0_PACKAGE_NAME); if (ret = LsaLookupAuthenticationPackage(lsa_handle, &auth_package_name, &auth_package_id) != STATUS_SUCCESS) goto done; - if (domain_user) { + if (use_kerberos) { /* lookup the user principal name for the account */ WCHAR domain_upn[MAX_UPN_LEN + 1]; @@ -168,6 +188,11 @@ generate_s4u_user_token(wchar_t* user_cpn, int impersonation) { } else { + /* if a domain prefix slipped through (non-domain-joined host. e.g. winpe is backed with such domain), + point past it without mutating the caller's buffer */ + wchar_t *backslash = wcschr(user_cpn, L'\\'); + if (backslash != NULL) + user_cpn = backslash + 1; MSV1_0_S4U_LOGON *s4u_logon; logon_info_size = sizeof(MSV1_0_S4U_LOGON); From 8a7d6b4e6a449d02c2b0a5615afffa7a944aae79 Mon Sep 17 00:00:00 2001 From: eran shalom Date: Wed, 29 Apr 2026 16:08:41 +0300 Subject: [PATCH 6/7] Address PR review: static helper, drop bogus README PATH claim - Make is_domain_joined_machine static (TU-local) and reformat to match the file's brace/indent conventions. - Remove the README claim that the build script adds Git's cmd directory to the machine PATH; no such logic exists in the build scripts. --- contrib/win32/openssh/README.txt | 3 +-- contrib/win32/win32compat/win32_usertoken_utils.c | 11 +++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/contrib/win32/openssh/README.txt b/contrib/win32/openssh/README.txt index 18eb6bed0649..675ede9a0b7f 100644 --- a/contrib/win32/openssh/README.txt +++ b/contrib/win32/openssh/README.txt @@ -30,8 +30,7 @@ Before building OpenSSH for Windows, install the following: same install / v143 toolset automatically. 2. Git for Windows. - The build script expects git.exe to be on PATH (it will add - "%ProgramFiles%\Git\cmd" to the machine PATH if missing). + The build script expects git.exe to be on PATH. 3. vcpkg (one-time bootstrap). Dependencies (LibreSSL, libfido2, zlib, libcbor) are managed via a diff --git a/contrib/win32/win32compat/win32_usertoken_utils.c b/contrib/win32/win32compat/win32_usertoken_utils.c index 656bb972e3fb..57e517f6e665 100644 --- a/contrib/win32/win32compat/win32_usertoken_utils.c +++ b/contrib/win32/win32compat/win32_usertoken_utils.c @@ -94,10 +94,10 @@ EnablePrivilege(const char *privName, int enabled) return; } -BOOL -is_domain_joined_machine(void){ - BOOL result = FALSE; - +static BOOL +is_domain_joined_machine(void) +{ + BOOL result = FALSE; LPWSTR name = NULL; NETSETUP_JOIN_STATUS s = NetSetupUnknownStatus; DWORD api_res = NetGetJoinInformation(NULL, &name, &s); @@ -105,8 +105,7 @@ is_domain_joined_machine(void){ result = s == NetSetupDomainName; NetApiBufferFree(name); name = NULL; - } - else{ + } else { debug("%s: NetGetJoinInformation() failed. Error %d.", __FUNCTION__, api_res); } debug("%s: NetGetJoinInformation(). Join result %d", __FUNCTION__, result); From 0e055f42ceb5664c13cbb2f72ad4c8ad9621bb36 Mon Sep 17 00:00:00 2001 From: eran shalom Date: Wed, 29 Apr 2026 16:34:44 +0300 Subject: [PATCH 7/7] Extend inherited dependent libs in sshd-auth instead of overriding Per PR review: replacing $(AdditionalDependentLibs) with a hand-written list made sshd-auth's link inputs drift from the rest of the solution (and dropped libs supplied by paths.targets). Append delayimp.lib to the inherited value instead. user32.lib is already in the inherited list; the /DELAYLOAD:user32.dll directive is unchanged, so delay-load behavior is preserved. --- contrib/win32/openssh/sshd-auth.vcxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/win32/openssh/sshd-auth.vcxproj b/contrib/win32/openssh/sshd-auth.vcxproj index 695afc0fb9dc..1d9e4a6c0be3 100644 --- a/contrib/win32/openssh/sshd-auth.vcxproj +++ b/contrib/win32/openssh/sshd-auth.vcxproj @@ -13,7 +13,7 @@ never loaded. The ItemDefinitionGroup with /DELAYLOAD is placed after Microsoft.Cpp.props (where the Link item type becomes defined). --> - bcrypt.lib;Userenv.lib;Crypt32.lib;Ws2_32.lib;Secur32.lib;Shlwapi.lib;kernel32.lib;user32.lib;delayimp.lib;advapi32.lib;Netapi32.lib;Rpcrt4.lib;ntdll.lib + $(AdditionalDependentLibs);delayimp.lib