Skip to content

Commit 491b937

Browse files
committed
fix(Core): Environment used ANSI Win32 calls, and wrote a store it did not read
Two Windows defects, found while triaging a CNA test that could not see a variable it had just set. THE TWO STORES. SetEnvironmentVariable writes the Win32 process environment block. getenv/_dupenv_s -- which is what this file's own tryGetEnvironmentVariable reads, and what CNA's getenv callers read -- reads the CRT's copy, which the Win32 call does not touch. So on Windows a variable set through Environment::SetEnvironmentVariable was invisible to Environment::GetEnvironmentVariable, in the same process. _wputenv_s updates both and is now the normal route. The present-but-empty case still goes through the wide Win32 call alone, because _wputenv_s deletes on an empty value and cannot express it. It is then visible to a Win32 read and not to a CRT read -- a limitation of the CRT environment, recorded in the comment rather than papered over. THE ANSI CALLS. Nine Win32 entry points here were the ...A twins, which substitute '?' for anything the process code page cannot spell. Every one of them returns a path or a name: GetCurrentDirectoryA / SetCurrentDirectoryA the working directory GetModuleFileNameA the executable's own path SHGetFolderPathA every special folder GetComputerNameA the machine name GetUserNameA the user's own account name SetEnvironmentVariableA names and values The last is the case that motivated this: a user whose Windows account name is not representable in the code page had that name destroyed before any caller could do anything about it, and every special folder underneath it with it. All now use the wide entry point and convert explicitly through CP_UTF8, which is the encoding the narrow std::string API here means. GetLogicalDriveStringsA is deliberately left: drive letters are ASCII by construction. Linux: 127/127 Environment tests pass. The changed code is inside `#if defined(_WIN32)`, so Linux behaviour is untouched by construction.
1 parent ef75cd1 commit 491b937

1 file changed

Lines changed: 69 additions & 21 deletions

File tree

modules/core/src/System/Environment.cpp

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// Portions based on .NET runtime API (MIT License, Copyright .NET Foundation and Contributors)
44
#include "System/Environment.hpp"
55
#include <cctype>
6+
#include <cstdlib>
7+
#include <iterator>
68
#include <fstream>
79
#ifdef _WIN32
810
#include "System/ApplicationException.hpp"
@@ -78,6 +80,35 @@ namespace {
7880
return true;
7981
}
8082

83+
#if defined(_WIN32)
84+
// Every Win32 call in this file that returns a path or a name uses the wide entry point and
85+
// converts here. The ANSI twins substitute '?' for anything the process code page cannot
86+
// spell, which silently destroys the current directory, the executable path, a special folder
87+
// and -- the case that matters most -- the user's own account name, for any user whose name is
88+
// not representable there.
89+
std::string utf8FromWide(const wchar_t* text, int length) {
90+
if (text == nullptr || length == 0) return {};
91+
const int needed = ::WideCharToMultiByte(CP_UTF8, 0, text, length, nullptr, 0, nullptr, nullptr);
92+
if (needed <= 0) return {};
93+
std::string out(static_cast<std::size_t>(needed), '\0');
94+
if (::WideCharToMultiByte(CP_UTF8, 0, text, length, out.data(), needed, nullptr, nullptr) <= 0)
95+
return {};
96+
return out;
97+
}
98+
99+
std::wstring wideFromUtf8(const std::string& text) {
100+
if (text.empty()) return {};
101+
const int needed = ::MultiByteToWideChar(CP_UTF8, 0, text.c_str(),
102+
static_cast<int>(text.size()), nullptr, 0);
103+
if (needed <= 0) return {};
104+
std::wstring out(static_cast<std::size_t>(needed), L'\0');
105+
if (::MultiByteToWideChar(CP_UTF8, 0, text.c_str(), static_cast<int>(text.size()),
106+
out.data(), needed) <= 0)
107+
return {};
108+
return out;
109+
}
110+
#endif
111+
81112
bool tryGetEnvironmentVariable(
82113
const std::string& name, std::string& value) {
83114
// getenv("") is unspecified by POSIX. Real .NET returns null for an empty name, which
@@ -177,12 +208,12 @@ std::string Environment::GetCurrentDirectory() {
177208
#if defined(_WIN32)
178209
// Win32's own documented two-call pattern: a zero-length call returns the required
179210
// buffer size INCLUDING the terminating NUL.
180-
const DWORD needed = GetCurrentDirectoryA(0, nullptr);
211+
const DWORD needed = GetCurrentDirectoryW(0, nullptr);
181212
if (needed == 0 || needed > kPathRetrievalCeiling) return "";
182-
std::vector<char> buf(needed);
183-
const DWORD written = GetCurrentDirectoryA(needed, buf.data());
213+
std::vector<wchar_t> buf(needed);
214+
const DWORD written = GetCurrentDirectoryW(needed, buf.data());
184215
if (written == 0 || written >= needed) return "";
185-
return std::string(buf.data(), written);
216+
return utf8FromWide(buf.data(), static_cast<int>(written));
186217
#else
187218
std::vector<char> buf(4096);
188219
for (;;) {
@@ -209,9 +240,9 @@ SharpRuntime::intcs Environment::getProcessorCountProperty() {
209240

210241
std::string Environment::getMachineNameProperty() {
211242
#if defined(_WIN32)
212-
char buf[MAX_COMPUTERNAME_LENGTH + 1];
213-
DWORD size = sizeof(buf);
214-
if (GetComputerNameA(buf, &size)) return std::string(buf);
243+
wchar_t buf[MAX_COMPUTERNAME_LENGTH + 1];
244+
DWORD size = static_cast<DWORD>(std::size(buf));
245+
if (GetComputerNameW(buf, &size)) return utf8FromWide(buf, static_cast<int>(size));
215246
return "";
216247
#else
217248
// Real .NET's Unix MachineName truncates at the first '.' to strip the domain suffix
@@ -228,9 +259,11 @@ std::string Environment::getMachineNameProperty() {
228259

229260
std::string Environment::getUserNameProperty() {
230261
#if defined(_WIN32)
231-
char buf[256];
232-
DWORD size = sizeof(buf);
233-
if (GetUserNameA(buf, &size)) return std::string(buf);
262+
wchar_t buf[256];
263+
DWORD size = static_cast<DWORD>(std::size(buf));
264+
// size comes back INCLUDING the terminating NUL, unlike GetComputerName's.
265+
if (GetUserNameW(buf, &size) && size > 0)
266+
return utf8FromWide(buf, static_cast<int>(size) - 1);
234267
return "";
235268
#elif defined(__EMSCRIPTEN__)
236269
const char* user = std::getenv("USER");
@@ -263,10 +296,25 @@ void Environment::SetEnvironmentVariable(const std::string& name,
263296
#if defined(_WIN32)
264297
// Win32 SetEnvironmentVariable deletes on a NULL lpValue, and _putenv_s deletes on "" -- so
265298
// the empty-value case has to go through the API that can distinguish them.
266-
if (!value.has_value())
267-
::SetEnvironmentVariableA(name.c_str(), nullptr);
268-
else
269-
::SetEnvironmentVariableA(name.c_str(), value->c_str());
299+
// Two stores, and they are not the same store. SetEnvironmentVariable writes the Win32
300+
// process environment block; getenv/_dupenv_s -- which is what tryGetEnvironmentVariable
301+
// above reads, and what CNA's own getenv callers read -- reads the CRT's copy, which the
302+
// Win32 call does not touch. Writing only one of them meant a variable set here was
303+
// invisible to a read from here, in the same process.
304+
//
305+
// _wputenv_s updates both, so it is the normal route. It cannot express present-but-empty
306+
// (it deletes on ""), so that one case still goes through the wide Win32 call alone, and is
307+
// then visible to a Win32 read but not to a CRT read -- a limitation of the CRT environment,
308+
// recorded rather than papered over.
309+
const std::wstring wideName = wideFromUtf8(name);
310+
if (!value.has_value()) {
311+
::SetEnvironmentVariableW(wideName.c_str(), nullptr);
312+
::_wputenv_s(wideName.c_str(), L"");
313+
} else if (value->empty()) {
314+
::SetEnvironmentVariableW(wideName.c_str(), L"");
315+
} else {
316+
::_wputenv_s(wideName.c_str(), wideFromUtf8(*value).c_str());
317+
}
270318
#else
271319
if (!value.has_value())
272320
::unsetenv(name.c_str());
@@ -489,10 +537,10 @@ std::string Environment::GetFolderPath(SpecialFolder folder, SpecialFolderOption
489537
// Windows resolves and applies the flags in one call: SpecialFolderOption's values ARE the
490538
// CSIDL flags, so they are simply OR-ed into the folder id, which is why this branch does not
491539
// repeat the POSIX verification below.
492-
char buf[MAX_PATH];
540+
wchar_t buf[MAX_PATH];
493541
const int csidl = static_cast<int>(folder) | static_cast<int>(option);
494-
if (SHGetFolderPathA(nullptr, csidl, nullptr, SHGFP_TYPE_CURRENT, buf) == S_OK)
495-
return std::string(buf);
542+
if (SHGetFolderPathW(nullptr, csidl, nullptr, SHGFP_TYPE_CURRENT, buf) == S_OK)
543+
return utf8FromWide(buf, -1);
496544
return "";
497545
#else
498546
const char* home = std::getenv("HOME");
@@ -646,7 +694,7 @@ void Environment::SetCurrentDirectory(const std::string& path) {
646694
// surfacing either failure.
647695
ArgumentException::ThrowIfNullOrEmpty(path, "value");
648696
#if defined(_WIN32)
649-
if (!SetCurrentDirectoryA(path.c_str()))
697+
if (!SetCurrentDirectoryW(wideFromUtf8(path).c_str()))
650698
throw System::IO::DirectoryNotFoundException("Could not find a part of the path '" + path + "'.");
651699
#else
652700
if (chdir(path.c_str()) != 0)
@@ -671,13 +719,13 @@ std::string Environment::getProcessPathProperty() {
671719
// defensive correctness on any platform that does not cap the answer for us, plus the
672720
// Windows zero-return handling, which was a real unconditional defect.
673721
#if defined(_WIN32)
674-
std::vector<char> buf(4096);
722+
std::vector<wchar_t> buf(4096);
675723
for (;;) {
676724
SetLastError(ERROR_SUCCESS);
677-
const DWORD len = GetModuleFileNameA(nullptr, buf.data(), static_cast<DWORD>(buf.size()));
725+
const DWORD len = GetModuleFileNameW(nullptr, buf.data(), static_cast<DWORD>(buf.size()));
678726
if (len == 0) return "";
679727
if (len < buf.size() && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
680-
return std::string(buf.data(), len);
728+
return utf8FromWide(buf.data(), static_cast<int>(len));
681729
if (buf.size() >= kPathRetrievalCeiling) return "";
682730
buf.resize(buf.size() * 2);
683731
}

0 commit comments

Comments
 (0)