PowerShell / PowerShell/Win32-OpenSSH
User-defined environment variables referenced in the user PATH are not expanded in SSH sessions
Nobody has claimed this yet.
- Dominant language
- No language data
- Stars
- 8.3k
- Forks
- 819
- PR merge metrics
- No merged PRs in 30d
Description
Summary
When HKCU\Environment\Path (a REG_EXPAND_SZ value) contains a reference to a user-defined variable such as %PNPM_HOME%\bin, that entry reaches the SSH session unexpanded, and every executable in that directory becomes unreachable.
The same value expands correctly on a local interactive logon, so this is specific to sshd.
The failure is narrow and easy to miss:
- References to profile-derived variables (
%LOCALAPPDATA%,%USERPROFILE%, …) insidePATHdo expand. - References to user-defined variables from
HKCU\EnvironmentinsidePATHdo not expand. - The very same user-defined variable, read on its own (
$env:PNPM_HOME), is present and correct in the session.
This bites anyone whose installer writes the variable form into PATH. pnpm setup / npx get-pnpm does exactly that (%PNPM_HOME%\bin), so pnpm and every globally installed CLI silently vanish over SSH while working fine at the console.
Environment
OpenSSH_for_Windows_10.0p2 Win32-OpenSSH-GitHub, LibreSSL 4.2.0- Windows 11 Pro 26200 (x64)
- Login shell:
pwsh(also reproduces with the default shell) - Auth: public key
Repro
On the Windows host, add one user-defined variable and two PATH entries, keeping the value type REG_EXPAND_SZ:
$k = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true)
$k.SetValue('MYTOOL_HOME', 'C:\mytool', [Microsoft.Win32.RegistryValueKind]::ExpandString)
$raw = $k.GetValue('Path', '', 'DoNotExpandEnvironmentNames')
$k.SetValue('Path', $raw + ';%MYTOOL_HOME%\bin;%LOCALAPPDATA%\probe',
[Microsoft.Win32.RegistryValueKind]::ExpandString)
$k.Close()
SSH into the host and inspect:
$env:Path -split ';' | Select-Object -Last 2
"MYTOOL_HOME = [$env:MYTOOL_HOME]"
Expected
C:\mytool\bin
C:\Users\<user>\AppData\Local\probe
MYTOOL_HOME = [C:\mytool]
Actual
%MYTOOL_HOME%\bin <-- NOT expanded
C:\Users\<user>\AppData\Local\probe <-- expanded
MYTOOL_HOME = [C:\mytool] <-- the variable itself is set correctly
A local interactive logon expands both entries.
Root cause
setup_session_user_vars() in contrib/win32/win32compat/w32-doexec.c builds PATH by reading the HKLM and HKCU values and concatenating them:
/* PATH is a special case. The System Path value is prepended to the User Path value */
wchar_t* hklm_path = get_registry_key_value(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment", L"PATH", &hklm_path_sz);
wchar_t* hkcu_path = get_registry_key_value(HKEY_CURRENT_USER, L"Environment", L"PATH", &hkcu_path_sz);
...
SetEnvironmentVariableW(L"PATH", user_path);
get_registry_key_value() uses RegGetValueW(..., RRF_RT_REG_SZ, ...). RRF_NOEXPAND is not passed, so expansion is attempted — but RegGetValue expands against the environment of the calling process, which here is sshd-session.exe.
The ordering in setup_session_env() is the problem:
setup_session_user_vars(pw_dir_w); /* PATH is assembled and expanded HERE */
env = do_setup_env_proxy(ssh, s, s->pw->pw_shell);
while (env_name = env[i]) {
...
/* SKIP, if not applicable on WINDOWS
PATH is already set. */
if ((0 == strncmp(env_name, "PATH=", strlen("PATH="))) || ...)
continue;
...
SetEnvironmentVariableW(env_name_w, env_value_w); /* user vars land HERE, too late */
}
At the moment PATH is expanded, the process environment holds the profile-derived variables (via LoadUserProfileW and the user token) but not the user-defined values from HKCU\Environment — those are only injected by the loop that runs afterwards. ExpandEnvironmentStrings leaves unresolved references untouched, so %MYTOOL_HOME%\bin survives verbatim. And because the loop explicitly skips PATH=, nothing ever revisits it.
Consistent with this, sshd-session.exe imports only LoadUserProfileW from USERENV.dll; it never calls CreateEnvironmentBlock, which is what a local logon uses and which resolves these references correctly.
Suggested fix
Any of:
- Assemble
PATHafter the user-defined variables have been applied to the process environment, and drop thePATH=skip. - Read the HKCU
PATHwithRRF_NOEXPANDand expand it explicitly once the user variables are in place. - Use
ExpandEnvironmentStringsForUserW()with the user token instead of relying on the service process environment.
Workaround
Write absolute paths into HKCU\Environment\Path instead of variable references, preserving the REG_EXPAND_SZ type:
$k = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true)
$raw = $k.GetValue('Path', '', 'DoNotExpandEnvironmentNames')
$k.SetValue('Path', $raw.Replace('%PNPM_HOME%\bin', "$env:LOCALAPPDATA\pnpm\bin"),
[Microsoft.Win32.RegistryValueKind]::ExpandString)
$k.Close()
Note that setx truncates PATH at 1024 characters and [Environment]::SetEnvironmentVariable(..., 'User') rewrites the value as REG_SZ, so neither is safe here.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in contrib/win32/win32compat/w32-doexec.c, reading setup_session_user_vars(), setup_session_env(), and get_registry_key_value(). Reproduce the SSH PowerShell session with a REG_EXPAND_SZ user PATH containing a user-defined variable, then verify the change makes that reference expand while preserving profile-derived expansion and the user variable itself.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c
- Domain
- operating-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100