From 4bcf78c9a5814919ed89de4c5a691b75c901eba9 Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Thu, 3 Sep 2026 07:24:22 -0400 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] 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 05/11] 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 06/11] 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 From d56b1dc300e78fcc605b057b2facda9cb70ed3f9 Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Fri, 4 Sep 2026 14:39:07 -0400 Subject: [PATCH 07/11] Fix remaining MinGW build errors in the regress/unit test build apps/wolfssh/common.c declared a CONSOLE_SCREEN_BUFFER_INFO local in ClientSetEcho that nothing ever read; the Windows echo toggling never grew the code that would have used it. Drop the unused declaration. wolfssh/port.h defined WSTRSEP as strsep(), a BSD extension MSVCRT and MinGW do not provide. Add a portable wstrsep() in src/port.c, matching the wstrnstr/wstrncat/wstrdup pattern already used for other missing string functions, and route WSTRSEP through it under USE_WINDOWS_API. tests/regress.c called the two argument POSIX mkdir(path, mode) and setenv()/unsetenv() directly in TestKnownHostsLastEntry. Use the existing WMKDIR macro for the directory creation, and add small TEST_SETENV/TEST_UNSETENV macros backed by _putenv_s() on Windows so the HOME juggling this test does still works there. src/wolfsftp.c had three separate issues in code paths that had never been compiled before this job existed. wolfSSH_SFTP_RecvOpen declared a flagsAndAttrs DWORD that nothing read, since WS_CreateFileA is called with a hardcoded FILE_ATTRIBUTE_NORMAL instead. wolfSSH_SFTP_RecvOpenDir compared a signed loop counter against a sizeof expression while building ssh->driveList, so make the counter word32. wolfSSH_SFTP_Put passed &state->rSz, an int, to ReadFile()'s DWORD* output parameter; read into a local DWORD and copy it into state->rSz afterward, since that field is also assigned from WFREAD() on non-Windows builds. src/wolfterm.c's wolfSSH_DoOSC never used its handle parameter. Mark it with WOLFSSH_UNUSED rather than removing it, since the parameter matches the signature its two call sites already pass and future OSC handling such as window titles is a natural use for it. Verified against a real x86_64-w64-mingw32 cross compiler with a config.h edited to match the sizes and header availability the actual Windows CI run reported (SIZEOF_LONG 4, HAVE_SYS_IOCTL_H undefined, and so on): every file this job compiles builds cleanly under the same -Werror flag set. Also reconfirmed a clean, unmodified Linux build still passes both tests/regress.test and tests/unit.test. --- apps/wolfssh/common.c | 1 - src/port.c | 28 ++++++++++++++++++++++++++++ src/wolfsftp.c | 10 +++++++--- src/wolfterm.c | 2 ++ tests/regress.c | 21 ++++++++++++++++----- wolfssh/port.h | 9 ++++++++- 6 files changed, 61 insertions(+), 10 deletions(-) diff --git a/apps/wolfssh/common.c b/apps/wolfssh/common.c index fd5c1d57d..013d86400 100644 --- a/apps/wolfssh/common.c +++ b/apps/wolfssh/common.c @@ -721,7 +721,6 @@ int ClientSetEcho(int type) #else static int echoInit = 0; static DWORD originalTerm; - static CONSOLE_SCREEN_BUFFER_INFO screenOrig; HANDLE stdinHandle = GetStdHandle(STD_INPUT_HANDLE); if (!echoInit) { if (GetConsoleMode(stdinHandle, &originalTerm) == 0) { diff --git a/src/port.c b/src/port.c index 077791f24..6886f77c1 100644 --- a/src/port.c +++ b/src/port.c @@ -925,4 +925,32 @@ char* wstrncat(char* s1, const char* s2, size_t n) return NULL; } + +#ifdef USE_WINDOWS_API +/* strsep() equivalent for platforms whose C library does not provide the + * BSD extension (MSVCRT/MinGW). Splits *s1 on the first character found + * in delim, NUL-terminates the token in place, and advances *s1 past it + * (NULL when no delimiter remains). Returns the start of the token, or + * NULL if *s1 was already NULL. */ +char* wstrsep(char** s1, const char* delim) +{ + char* start = *s1; + char* p; + + if (start == NULL) + return NULL; + + for (p = start; *p != '\0'; p++) { + if (WSTRCHR(delim, *p) != NULL) { + *p = '\0'; + *s1 = p + 1; + return start; + } + } + + *s1 = NULL; + return start; +} +#endif /* USE_WINDOWS_API */ + #endif /* WSTRING_USER */ diff --git a/src/wolfsftp.c b/src/wolfsftp.c index 88cca98f8..3f1d48246 100644 --- a/src/wolfsftp.c +++ b/src/wolfsftp.c @@ -2622,7 +2622,6 @@ int wolfSSH_SFTP_RecvOpen(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) word32 idx = 0; DWORD desiredAccess = 0; DWORD creationDisp = 0; - DWORD flagsAndAttrs = 0; int ret = WS_SUCCESS; int rc; int fileHandleOpened = 0; @@ -3037,7 +3036,7 @@ int wolfSSH_SFTP_RecvOpenDir(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) DWORD drives, mask; UINT driveType; char driveName[] = " :\\"; - int i; + word32 i; WMEMSET(ssh->driveList, 0, sizeof ssh->driveList); ssh->driveListCount = 0; @@ -10401,11 +10400,16 @@ int wolfSSH_SFTP_Put(WOLFSSH* ssh, char* from, char* to, byte resume, break; /* either at end of file or error */ } #else /* USE_WINDOWS_API */ + /* ReadFile() wants a DWORD* out param; state->rSz is + * an int shared with the WFREAD() branch above. */ + DWORD wRSz = 0; + if (ReadFile(state->fileHandle, state->r, - WOLFSSH_MAX_SFTP_RW, &state->rSz, + WOLFSSH_MAX_SFTP_RW, &wRSz, &state->offset) == 0) { break; /* either at end of file or error */ } + state->rSz = (int)wRSz; #endif /* USE_WINDOWS_API */ } sz = wolfSSH_SFTP_SendWritePacket(ssh, diff --git a/src/wolfterm.c b/src/wolfterm.c index 17512d0fe..786cee4d0 100644 --- a/src/wolfterm.c +++ b/src/wolfterm.c @@ -374,6 +374,8 @@ static int wolfSSH_DoOSC(WOLFSSH* ssh, WOLFSSH_HANDLE handle, byte* buf, * not saved to escBuf and escState is never set to WS_ESC_OSC, so there is * no resume path. Returning WS_SUCCESS lets the caller advance past the * sequence and reset escState cleanly. */ + WOLFSSH_UNUSED(handle); + if (*idx >= bufSz) { /* missing the OSC command byte, drop the sequence */ return WS_SUCCESS; diff --git a/tests/regress.c b/tests/regress.c index e0947c47f..2ebc243eb 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -11916,6 +11916,17 @@ static int KnownHostsCheckCapture(const byte* pubKey, word32 pubKeySz, } +/* setenv()/unsetenv() are POSIX and have no MSVCRT equivalent; _putenv_s() + * matches their (name, value) shape and success/failure return closely + * enough for this test's own HOME juggling. */ +#ifdef USE_WINDOWS_API + #define TEST_SETENV(n,v) _putenv_s((n), (v)) + #define TEST_UNSETENV(n) _putenv_s((n), "") +#else + #define TEST_SETENV(n,v) setenv((n), (v), 1) + #define TEST_UNSETENV(n) unsetenv((n)) +#endif + /* known_hosts is a text file and POSIX lets its last line end without a * newline, and a file written on Windows ends its lines with CRLF. Match the * last entry with a trailing newline, without one, and with CRLF line @@ -11980,9 +11991,9 @@ static void TestKnownHostsLastEntry(void) (void)rmdir(homeDir); /* Use a single flag to avoid duplicate errors below. */ - ready = (mkdir(homeDir, 0700) == 0) - && (mkdir(sshDir, 0700) == 0) - && (setenv("HOME", homeDir, 1) == 0); + ready = (WMKDIR(NULL, homeDir, 0700) == 0) + && (WMKDIR(NULL, sshDir, 0700) == 0) + && (TEST_SETENV("HOME", homeDir) == 0); AssertTrue(ready); /* A regression falls through to the "add it to known hosts?" prompt, so @@ -12045,11 +12056,11 @@ static void TestKnownHostsLastEntry(void) } if (savedHome != NULL) { - AssertIntEQ(setenv("HOME", savedHome, 1), 0); + AssertIntEQ(TEST_SETENV("HOME", savedHome), 0); WFREE(savedHome, NULL, 0); } else { - unsetenv("HOME"); + TEST_UNSETENV("HOME"); } (void)remove(hostsPath); diff --git a/wolfssh/port.h b/wolfssh/port.h index 24abb2fba..46f6268db 100644 --- a/wolfssh/port.h +++ b/wolfssh/port.h @@ -628,7 +628,6 @@ extern "C" { #define WSTRNCMP(s1,s2,n) strncmp((s1),(s2),(n)) #define WSTRSPN(s1,s2) strspn((s1),(s2)) #define WSTRCSPN(s1,s2) strcspn((s1),(s2)) - #define WSTRSEP(s,d) strsep((s),(d)) #define WSTRCAT(s1,s2) strcat((s1),(s2)) #define WSTRCPY(s1,s2) strcpy((s1),(s2)) @@ -641,6 +640,14 @@ extern "C" { #define WSTRDUP(s,h,t) wstrdup((s),(h),(t)) #define WSTRCHR(s,c) strchr((s),(c)) + #ifndef USE_WINDOWS_API + #define WSTRSEP(s,d) strsep((s),(d)) + #else + /* strsep() is a BSD extension not provided by MSVCRT/MinGW */ + WOLFSSH_API char* wstrsep(char** s1, const char* delim); + #define WSTRSEP(s,d) wstrsep((s),(d)) + #endif + #ifdef USE_WINDOWS_API #define WSTRNCPY(s1,s2,n) strncpy_s((s1),(n),(s2),(n)) #define WSTRNCASECMP(s1,s2,n) _strnicmp((s1),(s2),(n)) From 13791d17eab680f12692efceb730cef6ae556936 Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Fri, 4 Sep 2026 15:34:57 -0400 Subject: [PATCH 08/11] apps/wolfssh: Write known_hosts entries in binary mode CI's mingw-regress job showed AppendKeyToFile writing a known_hosts entry, then TestAppendKeyToFile reading it back and finding a byte mismatch on Windows. The file was opened with WFOPEN(..., "a"), a text mode append. On Windows the C runtime rewrites '\n' to CRLF on write in text mode, so the entry landed on disk with a trailing "\r\n" instead of the "\n" the test wrote and expected back. Open the file in binary mode instead, matching the WriteTextFile test helper a few hundred lines above in tests/regress.c, which already uses "wb" for the same reason. known_hosts is conventionally LF-terminated regardless of platform, so this also matches the format other SSH clients expect from the file, not just the test. --- apps/wolfssh/common.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/wolfssh/common.c b/apps/wolfssh/common.c index 013d86400..0092fd8c9 100644 --- a/apps/wolfssh/common.c +++ b/apps/wolfssh/common.c @@ -257,7 +257,10 @@ static int AppendKeyToFile(const char* filename, const char* name, if (ret == WS_SUCCESS) { const int needsNewline = AppendNeedsNewline(filename); - ret = WFOPEN(NULL, &f, filename, "a"); + /* Binary mode: text mode would translate '\n' to CRLF on Windows, + * and known_hosts is conventionally LF-terminated regardless of + * platform. */ + ret = WFOPEN(NULL, &f, filename, "ab"); if (ret == 0 && f != WBADFILE) { /* Check the write and the close so a failed or truncated entry * (for example on a full disk) is reported rather than appearing From 84f7fac5ac8e0869207290563b4a4828d93dacb0 Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Fri, 4 Sep 2026 15:35:05 -0400 Subject: [PATCH 09/11] tests: Use the Windows null device in the known_hosts regression test The next mingw-regress failure after the previous fix was TestKnownHostsLastEntry failing its "ready" assertion. It calls open("/dev/null", O_RDONLY) to point stdin at EOF while it drives the known_hosts prompt, but Windows has no /dev/null; the call simply fails there, so open() returns -1 and every check gated on ready is skipped. Add a TEST_NULL_DEVICE macro that resolves to "NUL" under USE_WINDOWS_API and "/dev/null" otherwise, and use it at both call sites in this file: TestKnownHostsLastEntry, and the same pattern in TestPasswordEofNoCrash a bit earlier, which happens to be masked in CI by its own isatty() guard but would hit the same bug if ever run against a real terminal on Windows. --- tests/regress.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/regress.c b/tests/regress.c index 2ebc243eb..8bacee2d6 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -7937,6 +7937,13 @@ static void TestClientBuffersIdempotent(void) } #endif +/* Windows has no /dev/null; the null device there is "NUL". */ +#ifdef USE_WINDOWS_API + #define TEST_NULL_DEVICE "NUL" +#else + #define TEST_NULL_DEVICE "/dev/null" +#endif + /* Simulate Ctrl+D (stdin EOF) during password prompt; expect failure but no crash. */ static void TestPasswordEofNoCrash(void) { @@ -7951,7 +7958,7 @@ static void TestPasswordEofNoCrash(void) savedStdin = dup(STDIN_FILENO); AssertTrue(savedStdin >= 0); - devNull = open("/dev/null", O_RDONLY); + devNull = open(TEST_NULL_DEVICE, O_RDONLY); AssertTrue(devNull >= 0); AssertTrue(dup2(devNull, STDIN_FILENO) >= 0); @@ -12001,7 +12008,7 @@ static void TestKnownHostsLastEntry(void) * Check each step, otherwise a failure here leaves the prompt reading * the real stdin. */ savedStdin = dup(STDIN_FILENO); - devNull = open("/dev/null", O_RDONLY); + devNull = open(TEST_NULL_DEVICE, O_RDONLY); ready = ready && (savedStdin >= 0) && (devNull >= 0) && (dup2(devNull, STDIN_FILENO) >= 0); AssertTrue(ready); From d59c5190291d82efccfab15faa4cc3749f984c97 Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Fri, 4 Sep 2026 16:46:30 -0400 Subject: [PATCH 10/11] tests: Stop TestKnownHostsLastEntry depending on WMKDIR A local run with ./configure --enable-smallstack (no --enable-sftp) showed WMKDIR as an implicit, undeclared function under MinGW. wolfssh/port.h only defines WMKDIR when WOLFSSH_SFTP, WOLFSSH_SCP, or WOLFSSH_SSHD is enabled, but TestKnownHostsLastEntry itself is gated on WOLFSSL_BASE64_ENCODE alone, so it compiles in configurations where none of those three are on and WMKDIR does not exist. Add a small TEST_MKDIR macro next to the existing TEST_SETENV and TEST_UNSETENV ones, backed directly by _mkdir() under USE_WINDOWS_API and mkdir() otherwise, and use it in place of WMKDIR. The Windows branch needs direct.h for _mkdir's declaration; include it next to the existing _WIN32 guard around arpa/inet.h at the top of the file. Verified with the MinGW cross compiler both with WOLFSSH_SFTP defined and without, and confirmed a plain Linux build configured with --enable-smallstack (no --enable-sftp) still passes tests/regress.test and tests/unit.test. --- tests/regress.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/regress.c b/tests/regress.c index 8bacee2d6..aacc173d9 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -34,6 +34,8 @@ #include #ifndef _WIN32 #include +#else + #include #endif #include #include @@ -11925,13 +11927,18 @@ static int KnownHostsCheckCapture(const byte* pubKey, word32 pubKeySz, /* setenv()/unsetenv() are POSIX and have no MSVCRT equivalent; _putenv_s() * matches their (name, value) shape and success/failure return closely - * enough for this test's own HOME juggling. */ + * enough for this test's own HOME juggling. WMKDIR is not an option here: + * it is only defined when wolfssh/port.h is built with SFTP, SCP, or sshd + * support, and this test compiles whenever WOLFSSL_BASE64_ENCODE is set, + * independent of those. */ #ifdef USE_WINDOWS_API #define TEST_SETENV(n,v) _putenv_s((n), (v)) #define TEST_UNSETENV(n) _putenv_s((n), "") + #define TEST_MKDIR(p,m) _mkdir((p)) #else #define TEST_SETENV(n,v) setenv((n), (v), 1) #define TEST_UNSETENV(n) unsetenv((n)) + #define TEST_MKDIR(p,m) mkdir((p), (m)) #endif /* known_hosts is a text file and POSIX lets its last line end without a @@ -11998,8 +12005,8 @@ static void TestKnownHostsLastEntry(void) (void)rmdir(homeDir); /* Use a single flag to avoid duplicate errors below. */ - ready = (WMKDIR(NULL, homeDir, 0700) == 0) - && (WMKDIR(NULL, sshDir, 0700) == 0) + ready = (TEST_MKDIR(homeDir, 0700) == 0) + && (TEST_MKDIR(sshDir, 0700) == 0) && (TEST_SETENV("HOME", homeDir) == 0); AssertTrue(ready); From 92d9282f749a6e8bccba5fe0de0434abe28a1ec2 Mon Sep 17 00:00:00 2001 From: Hideki Miyazaki Date: Sun, 6 Sep 2026 10:52:35 -0400 Subject: [PATCH 11/11] windows-check: Pin wolfssl to v5.9.1-stable in mingw-regress The wolfssl checkout in mingw-regress had no ref, so it always built against wolfssl/wolfssl's default branch. An upstream change there could break this job with nothing changed on the wolfssh side to explain it, and this job in particular turned out to be sensitive to exact wolfSSL build details while it was being brought up. Pin it to v5.9.1-stable, the same tag singlethread-check.yml and x509-interop.yml already use, confirmed to include wc_mlkem.c so the ML-KEM coverage this job exercises is still built. The build and asan-tests jobs in this file have the same unpinned checkout but are left alone here, since they were not touched by this change. --- .github/workflows/windows-check.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/windows-check.yml b/.github/workflows/windows-check.yml index 89ebe096d..2f65cdb58 100644 --- a/.github/workflows/windows-check.yml +++ b/.github/workflows/windows-check.yml @@ -240,6 +240,12 @@ jobs: runs-on: windows-latest timeout-minutes: 40 + # Pinned rather than left on wolfssl/wolfssl's default branch, so an + # upstream wolfSSL change can't silently break this job. Matches the + # ref singlethread-check.yml and x509-interop.yml already pin to. + env: + WOLFSSL_REF: v5.9.1-stable + defaults: run: shell: msys2 {0} @@ -261,6 +267,7 @@ jobs: uses: actions/checkout@v4 with: repository: wolfssl/wolfssl + ref: ${{ env.WOLFSSL_REF }} path: wolfssl - name: Build and install wolfssl