From 4bcf78c9a5814919ed89de4c5a691b75c901eba9 Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Thu, 3 Sep 2026 07:24:22 -0400 Subject: [PATCH 1/6] Run the SFTP regression tests on Windows under MSYS2 tests/regress.c holds the only coverage for the Windows SFTP open path, including TestSftpWindowsOpenFlagMatrix, which walks the RecvOpen CREAT/EXCL/TRUNC/APPEND matrix against the CreateFile() disposition table. That test is guarded by USE_WINDOWS_API and ran in no CI job. The MSVC solution in ide/winvs has no regress project, and regress.c does not build with cl because it includes arpa/inet.h and unistd.h, so the disposition fix it locks down could regress unnoticed. Add an MSYS2 MinGW64 job to the Windows workflow. MinGW defines _WIN32, so wolfssh/settings.h turns on USE_WINDOWS_API and the Windows-only branches compile and run. The job builds wolfSSL static, configures wolfSSH with --enable-sftp, and runs tests/regress.test and tests/unit.test. wolfsshd is left out: its autotools path is not MinGW-clean and the MSVC solution already covers it. --- .github/workflows/windows-check.yml | 68 +++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/.github/workflows/windows-check.yml b/.github/workflows/windows-check.yml index aed2420da..35ba8644a 100644 --- a/.github/workflows/windows-check.yml +++ b/.github/workflows/windows-check.yml @@ -220,3 +220,71 @@ jobs: if ($LASTEXITCODE -ne 0) { throw "$t failed under ASAN (exit $LASTEXITCODE)" } } + # Build and run the autotools regression tests under MSYS2 MinGW64. MinGW + # defines _WIN32, so wolfssh/settings.h turns on USE_WINDOWS_API and the + # Windows-only coverage in tests/regress.c (TestSftpWindowsOpenFlagMatrix, + # which walks the RecvOpen CREAT/EXCL/TRUNC/APPEND matrix against the + # CreateFile() disposition table) actually compiles and runs. The MSVC + # solution has no regress project and regress.c does not build with cl (it + # uses arpa/inet.h and unistd.h), so without this job that matrix runs in no + # CI at all. wolfsshd is left out of the build: its autotools path is not + # MinGW-clean and the MSVC solution already covers it. + mingw-regress: + name: MSYS2 MinGW64 regression tests + runs-on: windows-latest + timeout-minutes: 40 + + defaults: + run: + shell: msys2 {0} + + steps: + - name: Set up MSYS2 MinGW64 + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: false + install: >- + base-devel + autotools + git + mingw-w64-x86_64-gcc + mingw-w64-x86_64-pkgconf + + - name: Checkout wolfssl + uses: actions/checkout@v4 + with: + repository: wolfssl/wolfssl + path: wolfssl + + - name: Build and install wolfssl + working-directory: wolfssl + run: | + ./autogen.sh + # --enable-all pulls in --enable-crl-monitor, which wolfSSL's configure + # rejects on MinGW (it is limited to linux, OS X, and freebsd). Turn it + # back off explicitly; wolfSSH does not use the CRL monitor. + ./configure --enable-all --disable-crl-monitor \ + --enable-static --disable-shared \ + --prefix="$HOME/wolfssl-install" + make -j$(nproc) + make install + + - name: Checkout wolfssh + uses: actions/checkout@v4 + with: + path: wolfssh + + - name: Build and run the regression tests + working-directory: wolfssh + run: | + # Run autoreconf directly instead of ./autogen.sh: for a git checkout + # autogen.sh exports WARNINGS="all,error", turning autotools warnings + # into errors that the MSYS2 automake can trip on. + autoreconf -ivf + ./configure --enable-sftp \ + CPPFLAGS="-I$HOME/wolfssl-install/include" \ + LDFLAGS="-L$HOME/wolfssl-install/lib" + make -j$(nproc) tests/regress.test tests/unit.test + ./tests/regress.test + ./tests/unit.test From e2fb60b4a98c7d42fc45698b32eed36214fac73f Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Thu, 3 Sep 2026 07:33:56 -0400 Subject: [PATCH 2/6] wolfsshd: Cover -D option parsing on Windows in CI The Windows StartSSHD() path rebuilds argv from GetCommandLineW(). A regression there left -D foreground mode walking the raw wide command line, so -f and -p were ignored and the daemon used its built-in defaults. Nothing in CI caught that. Add sshd_dash_d_test.ps1: it starts wolfsshd with -D and a config file at a non-default path whose Port line differs from the -p value, then checks the listener binds the -p port and not the config port. That holds only when -D mode parsed both -f and -p. Run it from the Windows build job next to the existing LoginGraceTime check. --- .github/workflows/windows-check.yml | 6 ++ apps/wolfsshd/test/sshd_dash_d_test.ps1 | 115 ++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 apps/wolfsshd/test/sshd_dash_d_test.ps1 diff --git a/.github/workflows/windows-check.yml b/.github/workflows/windows-check.yml index 35ba8644a..11292e22a 100644 --- a/.github/workflows/windows-check.yml +++ b/.github/workflows/windows-check.yml @@ -88,6 +88,12 @@ jobs: timeout-minutes: 2 run: .\sshd_login_grace_test.ps1 -SshdExe "$env:SSHD_PATH" + - name: Test wolfsshd -D option parsing on Windows + working-directory: ${{ github.workspace }}\wolfssh\apps\wolfsshd\test + shell: pwsh + timeout-minutes: 2 + run: .\sshd_dash_d_test.ps1 -SshdExe "$env:SSHD_PATH" + # Build and run the self-contained unit tests with the MSVC AddressSanitizer. # This is the only job that executes wolfSSH tests under a sanitizer on # Windows, where the USE_WINDOWS_API console code (e.g. wolfSSH_DoOSC) is diff --git a/apps/wolfsshd/test/sshd_dash_d_test.ps1 b/apps/wolfsshd/test/sshd_dash_d_test.ps1 new file mode 100644 index 000000000..9be75fe4c --- /dev/null +++ b/apps/wolfsshd/test/sshd_dash_d_test.ps1 @@ -0,0 +1,115 @@ +#!/usr/bin/env pwsh +# +# Windows regression test for wolfsshd -D (foreground) option parsing. +# +# On Windows, StartSSHD() rebuilds argv from GetCommandLineW(); a bug in that +# path left -D (foreground) mode parsing the raw wide command line, so -f and +# -p were silently ignored and the daemon fell back to its compiled-in +# defaults. This test starts wolfsshd with -D and a config file at a +# non-default path whose Port line differs from the -p value, then checks that +# the listener comes up on the -p port. That only happens if -D mode parsed +# both -f (to find the config) and -p (to override the config's Port). +# +# No Windows user account or authorized key is required: the check is that the +# daemon binds the requested port, not that a session authenticates. +# +# Usage: +# pwsh sshd_dash_d_test.ps1 -SshdExe [-Port N] [-ConfPort N] +# (SshdExe also accepts the SSHD_PATH environment variable.) + +param( + [string]$SshdExe = $env:SSHD_PATH, + [int]$Port = 22335, + [int]$ConfPort = 22336 +) + +$ErrorActionPreference = "Stop" +$exitCode = 1 + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = (Resolve-Path (Join-Path $scriptDir "..\..\..")).Path +$keyPath = (Resolve-Path (Join-Path $repoRoot "keys\server-key.pem")).Path +$confFile = Join-Path $scriptDir "sshd_config_test_dash_d" +$authFile = Join-Path $scriptDir "authorized_keys_test_dash_d" + +if (-not $SshdExe -or -not (Test-Path $SshdExe)) { + Write-Host "ERROR: wolfsshd.exe not found (pass -SshdExe or set SSHD_PATH)" + exit 1 +} + +if ($Port -eq $ConfPort) { + Write-Host "ERROR: -Port and -ConfPort must differ so the test can tell them apart" + exit 1 +} + +# The config's Port is deliberately not the port we probe. If -p is parsed it +# wins (wolfsshd only reads the config Port when none was given on the command +# line), so a listener on $Port proves the -p override took effect. +@" +Port $ConfPort +Protocol 2 +PermitRootLogin yes +PasswordAuthentication yes +UseDNS no +HostKey $keyPath +AuthorizedKeysFile $authFile +"@ | Out-File -FilePath $confFile -Encoding ASCII + +"" | Out-File -FilePath $authFile -Encoding ASCII + +# -D selects the non-service (foreground) path on Windows. +$sshd = Start-Process -FilePath $SshdExe ` + -ArgumentList "-D", "-f", "`"$confFile`"", "-p", "$Port" ` + -NoNewWindow -PassThru + +try { + $up = $false + for ($i = 0; $i -lt 20; $i++) { + if ($sshd.HasExited) { + throw "wolfsshd exited early (code $($sshd.ExitCode)); -D option parsing likely failed" + } + try { + $probe = New-Object System.Net.Sockets.TcpClient + $probe.Connect("127.0.0.1", $Port) + $probe.Close() + $up = $true + break + } + catch { + Start-Sleep -Milliseconds 500 + } + } + if (-not $up) { + throw "wolfsshd did not listen on the -p port $Port; -D did not honor -f/-p" + } + + # The config Port must not have been used: nothing should answer there. + $confBound = $false + try { + $probe = New-Object System.Net.Sockets.TcpClient + $probe.Connect("127.0.0.1", $ConfPort) + $probe.Close() + $confBound = $true + } + catch { + # expected: no listener on the config Port + } + if ($confBound) { + throw "wolfsshd listened on the config Port $ConfPort; -p override was not applied" + } + + Write-Host "PASS: -D mode parsed -f and -p (listening on $Port, not $ConfPort)" + $exitCode = 0 +} +catch { + Write-Host "FAIL: $_" + $exitCode = 1 +} +finally { + if ($sshd -and -not $sshd.HasExited) { + Stop-Process -Id $sshd.Id -Force -ErrorAction SilentlyContinue + } + Remove-Item -Path $confFile, $authFile -Force -ErrorAction SilentlyContinue +} + +exit $exitCode From 84098bfbc938e1a43babf998ebd3d343568d0523 Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Fri, 4 Sep 2026 12:50:16 -0400 Subject: [PATCH 3/6] windows-check: Print config.log on mingw-regress failure The mingw-regress job's wolfssh configure step fails with "libwolfssl is required for wolfssh" even though libwolfssl.a is installed at the expected path. AC_CHECK_LIB only reports pass/fail; dump config.log on failure to see the actual link error. --- .github/workflows/windows-check.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/windows-check.yml b/.github/workflows/windows-check.yml index 11292e22a..8199988ac 100644 --- a/.github/workflows/windows-check.yml +++ b/.github/workflows/windows-check.yml @@ -294,3 +294,8 @@ jobs: make -j$(nproc) tests/regress.test tests/unit.test ./tests/regress.test ./tests/unit.test + + - name: Show config.log on failure + if: failure() + working-directory: wolfssh + run: cat config.log From 0d5c7a5494cbacaa4380c0adf64078531e2356e8 Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Fri, 4 Sep 2026 13:05:51 -0400 Subject: [PATCH 4/6] windows-check: Link ws2_32 and crypt32 for the mingw-regress wolfssh build config.log from a failing run showed the wolfCrypt_Init AC_CHECK_LIB probe pulling in ssl.c/internal.c/wolfio.c from the static libwolfssl.a, leaving Winsock (socket, send, recv, inet_pton, ...) and cert store (CertOpenSystemStoreA, ...) symbols unresolved. A shared build would defer that resolution to the DLL; the static archive here needs ws2_32 and crypt32 passed explicitly via LIBS. --- .github/workflows/windows-check.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/windows-check.yml b/.github/workflows/windows-check.yml index 8199988ac..341cf14cb 100644 --- a/.github/workflows/windows-check.yml +++ b/.github/workflows/windows-check.yml @@ -288,9 +288,13 @@ jobs: # autogen.sh exports WARNINGS="all,error", turning autotools warnings # into errors that the MSYS2 automake can trip on. autoreconf -ivf + # wolfssl is a static archive here, so its Windows socket (ws2_32) and + # certificate store (crypt32) references are only resolved when this + # configure's own AC_CHECK_LIB and later link steps pull them in too. ./configure --enable-sftp \ CPPFLAGS="-I$HOME/wolfssl-install/include" \ - LDFLAGS="-L$HOME/wolfssl-install/lib" + LDFLAGS="-L$HOME/wolfssl-install/lib" \ + LIBS="-lws2_32 -lcrypt32" make -j$(nproc) tests/regress.test tests/unit.test ./tests/regress.test ./tests/unit.test From de4c26119f9b8b831c8560843a528b0b62afbea4 Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Fri, 4 Sep 2026 13:29:15 -0400 Subject: [PATCH 5/6] windows-check: Build the .exe-suffixed regress/unit test targets on MinGW The new run showed the ws2_32/crypt32 link fix worked (configure passed) but make then failed: "No rule to make target 'tests/regress.test'." Reproducing the autotools build locally confirmed why: MinGW's EXEEXT is ".exe", so automake's check_PROGRAMS rule names the binaries tests/regress.test.exe and tests/unit.test.exe, not the extension-less names this job was asking make to build and run. --- .github/workflows/windows-check.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/windows-check.yml b/.github/workflows/windows-check.yml index 341cf14cb..89ebe096d 100644 --- a/.github/workflows/windows-check.yml +++ b/.github/workflows/windows-check.yml @@ -295,9 +295,12 @@ jobs: CPPFLAGS="-I$HOME/wolfssl-install/include" \ LDFLAGS="-L$HOME/wolfssl-install/lib" \ LIBS="-lws2_32 -lcrypt32" - make -j$(nproc) tests/regress.test tests/unit.test - ./tests/regress.test - ./tests/unit.test + # MinGW's EXEEXT is ".exe", so the check_PROGRAMS targets automake + # generates are tests/regress.test.exe and tests/unit.test.exe, not + # the extension-less names make would use on a POSIX host. + make -j$(nproc) tests/regress.test.exe tests/unit.test.exe + ./tests/regress.test.exe + ./tests/unit.test.exe - name: Show config.log on failure if: failure() From f903261ec662b69c9151f40368961493ef407018 Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Fri, 4 Sep 2026 13:51:25 -0400 Subject: [PATCH 6/6] Fix three MinGW -Werror build failures in the regress/unit test build MinGW-w64 does not ship arpa/inet.h; guard tests/regress.c's include the same way apps/wolfsshd/auth.c already does, since htonl/ntohl end up declared via the winsock2.h wolfSSL's headers pull in later in the same translation unit. wolfssh/test.h guarded its MSVC #pragma warning(disable:4996) with USE_WINDOWS_API alone, which is also true for MinGW's gcc; gcc treats the unrecognized pragma as an error under -Werror. Require _MSC_VER too, matching the existing ALIGN16 pragma guard in wolfssh/internal.h. wolfSSH_CleanPath's Windows/Nucleus drive-letter cleanup re-declared `i` in a nested scope, shadowing the function's own `i` used by every other loop in the function. Hoist `j` to the function's declarations (guarded by the same #if so non-Windows/Nucleus builds don't get an unused-variable warning) and drop the now-unnecessary block so the loop reuses the outer `i`. Verified locally: autoreconf + configure + make tests/regress.test tests/unit.test builds clean and both binaries pass on Linux. --- src/internal.c | 26 +++++++++++++------------- tests/regress.c | 4 +++- wolfssh/test.h | 4 ++-- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/internal.c b/src/internal.c index 10769aef4..c63d54165 100644 --- a/src/internal.c +++ b/src/internal.c @@ -22616,6 +22616,9 @@ int wolfSSH_CleanPath(WOLFSSH* ssh, char* in, int inSz) byte found; char *path; void *heap = NULL; +#if defined(WOLFSSL_NUCLEUS) || defined(USE_WINDOWS_API) + int j; +#endif if (in == NULL || inSz <= 0) { return WS_BAD_ARGUMENT; @@ -22715,19 +22718,16 @@ int wolfSSH_CleanPath(WOLFSSH* ssh, char* in, int inSz) } /* clean up any multiple drive listed i.e. A:/A: */ - { - int i,j; - sz = (long)WSTRLEN(path); - for (i = 0, j = 0; i < sz; i++) { - if (path[i] == ':') { - if (j == 0) j = i; - else { - /* @TODO only checking once */ - WMEMMOVE(path, path + i - WS_DRIVE_SIZE, - sz - i + WS_DRIVE_SIZE); - path[sz - i + WS_DRIVE_SIZE] = '\0'; - break; - } + sz = (long)WSTRLEN(path); + for (i = 0, j = 0; i < sz; i++) { + if (path[i] == ':') { + if (j == 0) j = i; + else { + /* @TODO only checking once */ + WMEMMOVE(path, path + i - WS_DRIVE_SIZE, + sz - i + WS_DRIVE_SIZE); + path[sz - i + WS_DRIVE_SIZE] = '\0'; + break; } } } diff --git a/tests/regress.c b/tests/regress.c index eca4fd729..e0947c47f 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -32,7 +32,9 @@ #include #include -#include +#ifndef _WIN32 + #include +#endif #include #include #include diff --git a/wolfssh/test.h b/wolfssh/test.h index a03f43686..a258075cc 100644 --- a/wolfssh/test.h +++ b/wolfssh/test.h @@ -401,7 +401,7 @@ static INLINE int mygetopt(int argc, char** argv, const char* optstring) } -#ifdef USE_WINDOWS_API +#if defined(USE_WINDOWS_API) && defined(_MSC_VER) #pragma warning(push) #pragma warning(disable:4996) /* For Windows builds, disable compiler warnings for: @@ -563,7 +563,7 @@ static INLINE void build_addr(SOCKADDR_IN_T* addr, const char* peer, } #endif /* WOLFSSH_NUCLEUS */ -#ifdef USE_WINDOWS_API +#if defined(USE_WINDOWS_API) && defined(_MSC_VER) #pragma warning(pop) #endif