From 73d95315298f6be120f9a72118f03d08daf8de5e Mon Sep 17 00:00:00 2001 From: Frank Dai Date: Thu, 5 Nov 2026 01:00:37 -0800 Subject: [PATCH 1/2] wolfsshd: start a Windows session with the user's environment CreateProcessAsUserW got a NULL environment, so the child inherited the service's (LocalSystem) block: USERPROFILE pointed at the system profile, APPDATA and LOCALAPPDATA were SYSTEM's, and the user PATH was missing. Tools installed per user were not found, and those that were found read their settings from the wrong profile. Build the block from the user's token with CreateEnvironmentBlock and pass it with CREATE_UNICODE_ENVIRONMENT. The profile is already loaded by wolfSSHD_GetHomeDirectory, so user variables resolve. If the block cannot be built, log the Windows error and fall back to the inherited environment, which is the behaviour before this change. Tested on Windows 11 (MSVC v143, wolfSSL v5.9.4-stable): over wolfsshd, USERPROFILE is C:\Users\Administrator, and per-user tools on the user PATH (mise shims for claude and codex) resolve and run, in exec and PTY sessions. --- apps/wolfsshd/wolfsshd.c | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 003851260..3345c0048 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -97,6 +97,9 @@ #include "auth.h" #include +#ifdef _WIN32 + #include /* CreateEnvironmentBlock */ +#endif #ifdef NO_INLINE #include @@ -2169,6 +2172,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, size_t conCmdSz = 0; PROCESS_INFORMATION processInfo; int processCreated = 0; + LPVOID env = NULL; size_t sz = 0; WCHAR h[MAX_PATH]; char* forcedCmd; @@ -2326,9 +2330,19 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, si.dwFlags = STARTF_USESTDHANDLES; si.lpDesktop = NULL; + /* The user's own variables (USERPROFILE, APPDATA, user PATH), not the + * service account's. The profile is loaded by now, so they resolve. */ + if (CreateEnvironmentBlock(&env, wolfSSHD_GetAuthToken(conn->auth), + FALSE) != TRUE) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Issue building user environment, Windows error %d", + GetLastError()); + env = NULL; /* fall back to the service's environment */ + } + if (CreateProcessAsUserW(wolfSSHD_GetAuthToken(conn->auth), NULL, conCmdPtr, - NULL, NULL, TRUE, DETACHED_PROCESS, NULL, h, - &si, &processInfo) != TRUE) { + NULL, NULL, TRUE, DETACHED_PROCESS | CREATE_UNICODE_ENVIRONMENT, + env, h, &si, &processInfo) != TRUE) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Issue creating process, Windows error %d", GetLastError()); ret = WS_FATAL_ERROR; @@ -2636,6 +2650,9 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, } cleanup: + if (env != NULL) { + DestroyEnvironmentBlock(env); + } if (conCmdPtr != NULL) { WFREE(conCmdPtr, NULL, DYNTYPE_SSHD); } From d31063fe81ca611c8b030f18410a2bf3430dfb4e Mon Sep 17 00:00:00 2001 From: Frank Dai Date: Thu, 5 Nov 2026 23:03:31 -0800 Subject: [PATCH 2/2] wolfsshd: build the Windows session block on the service's, PATH aside CreateEnvironmentBlock(..., FALSE) expands the registry values before it adds ProgramFiles and the other process variables, so PSModulePath came through as "%ProgramFiles%\WindowsPowerShell\Modules;..." and the (Arm) ProgramFiles variables were missing. Build the block with TRUE, as Win32-OpenSSH does, so those variables are set first. TRUE also carries the service account's own user PATH (...\config\systemprofile\AppData\Local\Microsoft\WindowsApps), which Win32-OpenSSH drops by rebuilding PATH in the session. Here PATH is taken from the FALSE block instead: system PATH plus the user's. Tested on Windows 11 x64, MSVC v143: PSModulePath expands, PATH has no systemprofile entry, USERPROFILE, APPDATA, LOCALAPPDATA and TEMP are the user's, and no other variable differs from the FALSE block. --- apps/wolfsshd/wolfsshd.c | 89 +++++++++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 10 deletions(-) diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 3345c0048..24c12c22c 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -2133,6 +2133,80 @@ static SHELL_SEND_STATE SHELL_BacklogState(WOLFSSH* ssh, int sendRet, #ifdef WIN32 +/* The PATH entry ("Path=...") of an environment block, or NULL */ +static const WCHAR* EnvBlockPath(const WCHAR* blk) +{ + const WCHAR* p; + + for (p = blk; *p != L'\0'; p += wcslen(p) + 1) { + if (_wcsnicmp(p, L"PATH=", 5) == 0) { + return p; + } + } + return NULL; +} + + +/* The session's environment, as Win32-OpenSSH builds it: the user's block + * on top of the service's, so variables such as ProgramFiles are set when + * registry values that refer to them (PSModulePath) are expanded. PATH alone + * comes from the user-only block, so the service account's own user PATH + * stays out. Returns a block to WFREE, or NULL to inherit the service's. */ +static WCHAR* SHELL_UserEnvironment(HANDLE token) +{ + LPVOID full = NULL; + LPVOID user = NULL; + const WCHAR* fullPath; + const WCHAR* userPath = NULL; + const WCHAR* p; + WCHAR* env; + WCHAR* out; + size_t sz = 2; /* the terminators of an empty block */ + + if (CreateEnvironmentBlock(&full, token, TRUE) != TRUE) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Issue building user environment, Windows error %d", + GetLastError()); + return NULL; + } + if (CreateEnvironmentBlock(&user, token, FALSE) == TRUE) { + userPath = EnvBlockPath((const WCHAR*)user); + } + else { + user = NULL; + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Issue building user PATH, Windows error %d", + GetLastError()); + } + fullPath = EnvBlockPath((const WCHAR*)full); + + for (p = (const WCHAR*)full; *p != L'\0'; p += wcslen(p) + 1) { + sz += wcslen((p == fullPath && userPath != NULL) ? userPath : p) + 1; + } + + env = (WCHAR*)WMALLOC(sizeof(WCHAR) * sz, NULL, DYNTYPE_SSHD); + if (env != NULL) { + out = env; + for (p = (const WCHAR*)full; *p != L'\0'; p += wcslen(p) + 1) { + const WCHAR* src = (p == fullPath && userPath != NULL) ? + userPath : p; + size_t len = wcslen(src) + 1; + + WMEMCPY(out, src, sizeof(WCHAR) * len); + out += len; + } + out[0] = L'\0'; + out[1] = L'\0'; + } + + if (user != NULL) { + DestroyEnvironmentBlock(user); + } + DestroyEnvironmentBlock(full); + return env; +} + + /* handles creating a new shell env. and maintains SSH connection for incoming * user input as well as output of the shell. * return WS_SUCCESS on success */ @@ -2172,7 +2246,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, size_t conCmdSz = 0; PROCESS_INFORMATION processInfo; int processCreated = 0; - LPVOID env = NULL; + WCHAR* env = NULL; size_t sz = 0; WCHAR h[MAX_PATH]; char* forcedCmd; @@ -2331,14 +2405,9 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, si.lpDesktop = NULL; /* The user's own variables (USERPROFILE, APPDATA, user PATH), not the - * service account's. The profile is loaded by now, so they resolve. */ - if (CreateEnvironmentBlock(&env, wolfSSHD_GetAuthToken(conn->auth), - FALSE) != TRUE) { - wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Issue building user environment, Windows error %d", - GetLastError()); - env = NULL; /* fall back to the service's environment */ - } + * service account's. The profile is loaded by now, so they resolve. + * NULL falls back to the service's environment. */ + env = SHELL_UserEnvironment(wolfSSHD_GetAuthToken(conn->auth)); if (CreateProcessAsUserW(wolfSSHD_GetAuthToken(conn->auth), NULL, conCmdPtr, NULL, NULL, TRUE, DETACHED_PROCESS | CREATE_UNICODE_ENVIRONMENT, @@ -2651,7 +2720,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, cleanup: if (env != NULL) { - DestroyEnvironmentBlock(env); + WFREE(env, NULL, DYNTYPE_SSHD); } if (conCmdPtr != NULL) { WFREE(conCmdPtr, NULL, DYNTYPE_SSHD);