Skip to content

wolfsshd: start a Windows session with the user's environment - #1380

Open
soulmachine wants to merge 2 commits into
wolfSSL:masterfrom
soulmachine:wolfsshd-windows-user-env
Open

soulmachine wants to merge 2 commits into
wolfSSL:masterfrom
soulmachine:wolfsshd-windows-user-env

Conversation

@soulmachine

@soulmachine soulmachine commented Nov 5, 2026 •

Copy link
Copy Markdown

Summary

On Windows, wolfsshd starts the session with CreateProcessAsUserW(token, ..., lpEnvironment = NULL, ...), so the child inherits the service's environment (LocalSystem), not the user's. USERPROFILE, APPDATA and LOCALAPPDATA point at C:\Windows\system32\config\systemprofile, USERNAME is the machine account, and the user's own PATH entries are missing. Per-user tools are not found, and the ones that are found read their settings from the wrong profile.

 SHELL_Subsystem (Windows)
   wolfSSHD_GetHomeDirectory      # already loads the user profile (LoadUserProfileW)
   ImpersonateLoggedOnUser
+  SHELL_UserEnvironment(token)
+    CreateEnvironmentBlock(&full, token, TRUE)    # as Win32-OpenSSH (w32fd.c)
+    CreateEnvironmentBlock(&user, token, FALSE)   # PATH only: no LocalSystem WindowsApps
+    on failure: log the Windows error, env = NULL (old behaviour)
-  CreateProcessAsUserW(token, ..., DETACHED_PROCESS, NULL, home, ...)
+  CreateProcessAsUserW(token, ..., DETACHED_PROCESS | CREATE_UNICODE_ENVIRONMENT, env, home, ...)
   ...
 cleanup:
+  WFREE(env)

The profile is already loaded before this point, so the block resolves the user's registry variables. The block is built with TRUE, as in Win32-OpenSSH, so PSModulePath expands and the (Arm) ProgramFiles variables are set. Only PATH comes from the FALSE block. This gives the same result as Win32-OpenSSH's later PATH rebuild. userenv.lib is already linked by every wolfsshd project configuration.

Evidence

Windows 11 Pro x64, MSVC v143 (VS 2022 Build Tools), wolfSSL v5.9.4-stable, wolfsshd running as a LocalSystem service, logging in as a local administrator with an ssh-ed25519 key:

  • Before:
    > ssh -p 2223 administrator@host "echo %USERNAME% & echo %USERPROFILE% & where <per-user tool>"
    HOST$
    C:\windows\system32\config\systemprofile
    INFO: Could not find files for the given pattern(s).
    
    After:
    > ssh -p 2223 administrator@host "echo %USERNAME% & echo %USERPROFILE% & where <per-user tool>"
    Administrator
    C:\Users\Administrator
    C:\Users\Administrator\AppData\Local\...\<per-user tool>.exe
    
    The same holds in an interactive (ConPTY) session, and the per-user tools run.

No automated test is added: the Windows sshd tests in windows-check.yml do not cover the session environment yet. I'm happy to add a PowerShell check there (exec echo %USERPROFILE% and assert it is not the system profile) if you want one.

Merge Danger

Door: two-way

A revert restores the old inherited environment.

Blast Radius: Windows sessions

Only the Windows shell and exec path changes. A session now sees the user's variables instead of the service's, so a setup that relied on the service's environment leaking into sessions would notice. If CreateEnvironmentBlock fails, the session still starts as before, and the error is logged.

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.
Copilot AI balanced review requested due to automatic review settings November 5, 2026 10:09
@wolfSSL-Bot

Copy link
Copy Markdown

Can one of the admins verify this patch?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues are reported for the Windows session environment changes.

No open issues 🎉

What changed in this PR

This PR updates Windows shell and exec sessions in wolfsshd to use the authenticated user’s environment instead of inheriting the service account’s variables.

Changes:

  • Builds a user environment block and passes it to process creation with Unicode environment support.
  • Logs environment creation failures and falls back to the inherited service environment.
  • Releases the environment block during cleanup.
File Description
apps/​wolfsshd/​wolfsshd.c Adds user-specific environment creation, process integration, and cleanup for Windows sessions.

Review effort: Balanced

@ejohnstown ejohnstown self-assigned this Nov 5, 2026
@ejohnstown

Copy link
Copy Markdown
Contributor

Thanks, this checks out on my Windows 11 VM: with the PR, the session gets the user's USERNAME, USERPROFILE, APPDATA and user PATH. One question: in a PR session, PSModulePath comes through unexpanded (the literal %ProgramFiles%\WindowsPowerShell\Modules;...), and ProgramFiles(Arm) / CommonProgramFiles(Arm) are missing. Win32-OpenSSH calls CreateEnvironmentBlock(..., TRUE) instead of FALSE. Did you pick FALSE deliberately to keep the service's variables out of the session? Have you seen PSModulePath expand correctly in your setup, or tried TRUE? We're testing TRUE on our side too.

@soulmachine

soulmachine commented Nov 6, 2026 •

Copy link
Copy Markdown
Author

More test evidence from a second machine: Windows 11 (build 26200), the same fork base (b23fbe5), wolfSSL master 6f28f43, MSVC v143. wolfsshd runs as a LocalSystem service, and the client is OpenSSH on macOS with an ssh-ed25519 key for a standard local user.

Before (unpatched build, same machine):

> ssh -p 2223 user@host "echo %USERNAME% & echo %USERPROFILE% & where claude"
HOST$
C:\WINDOWS\system32\config\systemprofile
INFO: Could not find files for the given pattern(s).

After (this PR), end-to-end suite, 13/13:

PASS exec USERNAME=<user>
PASS exec USERPROFILE                      # C:\Users\<user>
PASS exec APPDATA                          # C:\Users\<user>\AppData\Roaming
PASS exec where claude                     # tool on the user PATH only
PASS exec claude runs
PASS pty where claude                      # interactive (ConPTY) session
PASS scp upload 5 MiB
PASS scp download
PASS scp round-trip sha256
PASS sftp batch (mkdir/put/ls/rename/get/rm/rmdir)
PASS sftp round-trip sha256
PASS cleanup
PASS password auth refused

The patch changes only the environment passed to CreateProcessAsUserW. A multi-file SCP upload that I ran on both builds behaved the same; I did not run the full SCP/SFTP suite on the unpatched build.

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.
@soulmachine

Copy link
Copy Markdown
Author

Thanks for testing it. FALSE was not a tested choice: I picked it to keep the service's variables out of the session and did not try TRUE. You're right about PSModulePath, and I reproduced it here (Windows 11 x64). With FALSE, userenv expands the registry values before it adds ProgramFiles and the other process variables. So %ProgramFiles% stays literal, and the (Arm) variables, which come only from the process environment, are missing.

TRUE on its own fixes that, but on my machine it brings in one variable from the service: PATH gains LocalSystem's own user PATH entry, C:\WINDOWS\system32\config\systemprofile\AppData\Local\Microsoft\WindowsApps. Win32-OpenSSH has the same issue after its CreateEnvironmentBlock(..., TRUE) (w32fd.c). It clears it later, because the session process rebuilds PATH as HKLM PATH plus HKCU PATH (setup_session_user_path in w32-doexec.c).

I pushed d31063f, which does the same thing in one place: it builds the block with TRUE and takes only Path from the FALSE block. I compared full set dumps of a session on the same machine:

FALSE TRUE d31063f
PSModulePath %ProgramFiles%\... (literal) expanded expanded
PATH system + user system + LocalSystem's WindowsApps + user system + user
USERPROFILE, APPDATA, LOCALAPPDATA, TEMP, TMP, USERNAME user's user's user's
other variables that differ from FALSE — PATH, PSModulePath PSModulePath only

My end-to-end suite passes 15/15 on d31063f. It now also asserts that PSModulePath is expanded and that PATH has no systemprofile entry, on top of the user variables, an interactive ConPTY session, a 5 MiB SCP round trip and an SFTP batch, with SHA-256 checks. The machine is x64, so I could not check the (Arm) variables myself. They come from the process environment, so TRUE should carry them, as in your test.

If you'd rather stay with plain TRUE, like Win32-OpenSSH, I can drop the PATH step. I'll squash the two commits if you prefer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants