llvm-project/lldb/source/Host/windows/ProcessLauncherWindows.cpp
Charles Zablit 9edbf83667
[lldb][windows] fix environment handling in CreateProcessW setup (#168733)
This patch refactors and documents the setup of the `CreateProcessW`
invocation in `ProcessLauncherWindows`. It's a dependency of
https://github.com/llvm/llvm-project/pull/168729.

`CreateEnvironmentBufferW` now sorts the environment variable keys
before concatenating them into a string. From [the `CreateProcess`
documentation](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw):
> An application must manually pass the current directory information to
the new process. To do so, the application must explicitly create these
environment variable strings, sort them alphabetically (because the
system uses a sorted environment), and put them into the environment
block. Typically, they will go at the front of the environment block,
due to the environment block sort order.

`GetFlattenedWindowsCommandStringW` now returns an error which will be
surfaced, instead of failing silently.

Types were converted to their wide equivalent (i.e appending `W` to
them, see `STARTUPINFOEX`) since we are calling the `W` variant of
`CreateProcess`.
2025-12-01 18:46:16 +00:00

251 lines
9.1 KiB
C++

//===-- ProcessLauncherWindows.cpp ----------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "lldb/Host/windows/ProcessLauncherWindows.h"
#include "lldb/Host/HostProcess.h"
#include "lldb/Host/ProcessLaunchInfo.h"
#include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/Support/ConvertUTF.h"
#include "llvm/Support/Program.h"
#include <string>
#include <vector>
using namespace lldb;
using namespace lldb_private;
/// Create a UTF-16 environment block to use with CreateProcessW.
///
/// The buffer is a sequence of null-terminated UTF-16 strings, followed by an
/// extra L'\0' (two bytes of 0). An empty environment must have one
/// empty string, followed by an extra L'\0'.
///
/// The keys are sorted to comply with the CreateProcess API calling convention.
///
/// Ensure that the resulting buffer is used in conjunction with
/// CreateProcessW and be sure that dwCreationFlags includes
/// CREATE_UNICODE_ENVIRONMENT.
///
/// \param env The Environment object to convert.
/// \returns The sorted sequence of environment variables and their values,
/// separated by null terminators. The vector is guaranteed to never be empty.
static std::vector<wchar_t> CreateEnvironmentBufferW(const Environment &env) {
std::vector<std::wstring> env_entries;
for (const auto &KV : env) {
std::wstring wentry;
if (llvm::ConvertUTF8toWide(Environment::compose(KV), wentry))
env_entries.push_back(std::move(wentry));
}
std::sort(env_entries.begin(), env_entries.end(),
[](const std::wstring &a, const std::wstring &b) {
return _wcsicmp(a.c_str(), b.c_str()) < 0;
});
std::vector<wchar_t> buffer;
for (const auto &env_entry : env_entries) {
buffer.insert(buffer.end(), env_entry.begin(), env_entry.end());
buffer.push_back(L'\0');
}
if (buffer.empty())
buffer.push_back(L'\0'); // If there are no environment variables, we have
// to ensure there are 4 zero bytes in the buffer.
buffer.push_back(L'\0');
return buffer;
}
/// Flattens an Args object into a Windows command-line wide string.
///
/// Returns an empty string if args is empty.
///
/// \param args The Args object to flatten.
/// \returns A wide string containing the flattened command line.
static llvm::ErrorOr<std::wstring>
GetFlattenedWindowsCommandStringW(Args args) {
if (args.empty())
return L"";
std::vector<llvm::StringRef> args_ref;
for (auto &entry : args.entries())
args_ref.push_back(entry.ref());
return llvm::sys::flattenWindowsCommandLine(args_ref);
}
HostProcess
ProcessLauncherWindows::LaunchProcess(const ProcessLaunchInfo &launch_info,
Status &error) {
error.Clear();
std::string executable;
STARTUPINFOEXW startupinfoex = {};
STARTUPINFOW &startupinfo = startupinfoex.StartupInfo;
PROCESS_INFORMATION pi = {};
HANDLE stdin_handle = GetStdioHandle(launch_info, STDIN_FILENO);
HANDLE stdout_handle = GetStdioHandle(launch_info, STDOUT_FILENO);
HANDLE stderr_handle = GetStdioHandle(launch_info, STDERR_FILENO);
auto close_handles = llvm::make_scope_exit([&] {
if (stdin_handle)
::CloseHandle(stdin_handle);
if (stdout_handle)
::CloseHandle(stdout_handle);
if (stderr_handle)
::CloseHandle(stderr_handle);
});
startupinfo.cb = sizeof(startupinfoex);
startupinfo.dwFlags |= STARTF_USESTDHANDLES;
startupinfo.hStdError =
stderr_handle ? stderr_handle : ::GetStdHandle(STD_ERROR_HANDLE);
startupinfo.hStdInput =
stdin_handle ? stdin_handle : ::GetStdHandle(STD_INPUT_HANDLE);
startupinfo.hStdOutput =
stdout_handle ? stdout_handle : ::GetStdHandle(STD_OUTPUT_HANDLE);
std::vector<HANDLE> inherited_handles;
if (startupinfo.hStdError)
inherited_handles.push_back(startupinfo.hStdError);
if (startupinfo.hStdInput)
inherited_handles.push_back(startupinfo.hStdInput);
if (startupinfo.hStdOutput)
inherited_handles.push_back(startupinfo.hStdOutput);
SIZE_T attributelist_size = 0;
InitializeProcThreadAttributeList(/*lpAttributeList=*/nullptr,
/*dwAttributeCount=*/1, /*dwFlags=*/0,
&attributelist_size);
startupinfoex.lpAttributeList =
static_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(malloc(attributelist_size));
auto free_attributelist =
llvm::make_scope_exit([&] { free(startupinfoex.lpAttributeList); });
if (!InitializeProcThreadAttributeList(startupinfoex.lpAttributeList,
/*dwAttributeCount=*/1, /*dwFlags=*/0,
&attributelist_size)) {
error = Status(::GetLastError(), eErrorTypeWin32);
return HostProcess();
}
auto delete_attributelist = llvm::make_scope_exit(
[&] { DeleteProcThreadAttributeList(startupinfoex.lpAttributeList); });
for (size_t i = 0; i < launch_info.GetNumFileActions(); ++i) {
const FileAction *act = launch_info.GetFileActionAtIndex(i);
if (act->GetAction() == FileAction::eFileActionDuplicate &&
act->GetFD() == act->GetActionArgument())
inherited_handles.push_back(reinterpret_cast<HANDLE>(act->GetFD()));
}
if (!inherited_handles.empty()) {
if (!UpdateProcThreadAttribute(
startupinfoex.lpAttributeList, /*dwFlags=*/0,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherited_handles.data(),
inherited_handles.size() * sizeof(HANDLE),
/*lpPreviousValue=*/nullptr, /*lpReturnSize=*/nullptr)) {
error = Status(::GetLastError(), eErrorTypeWin32);
return HostProcess();
}
}
const char *hide_console_var =
getenv("LLDB_LAUNCH_INFERIORS_WITHOUT_CONSOLE");
if (hide_console_var &&
llvm::StringRef(hide_console_var).equals_insensitive("true")) {
startupinfo.dwFlags |= STARTF_USESHOWWINDOW;
startupinfo.wShowWindow = SW_HIDE;
}
DWORD flags = CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT |
EXTENDED_STARTUPINFO_PRESENT;
if (launch_info.GetFlags().Test(eLaunchFlagDebug))
flags |= DEBUG_ONLY_THIS_PROCESS;
if (launch_info.GetFlags().Test(eLaunchFlagDisableSTDIO))
flags &= ~CREATE_NEW_CONSOLE;
std::vector<wchar_t> environment =
CreateEnvironmentBufferW(launch_info.GetEnvironment());
auto wcommandLineOrErr =
GetFlattenedWindowsCommandStringW(launch_info.GetArguments());
if (!wcommandLineOrErr) {
error = Status(wcommandLineOrErr.getError());
return HostProcess();
}
std::wstring wcommandLine = *wcommandLineOrErr;
// If the command line is empty, it's best to pass a null pointer to tell
// CreateProcessW to use the executable name as the command line. If the
// command line is not empty, its contents may be modified by CreateProcessW.
WCHAR *pwcommandLine = wcommandLine.empty() ? nullptr : &wcommandLine[0];
std::wstring wexecutable, wworkingDirectory;
llvm::ConvertUTF8toWide(launch_info.GetExecutableFile().GetPath(),
wexecutable);
llvm::ConvertUTF8toWide(launch_info.GetWorkingDirectory().GetPath(),
wworkingDirectory);
BOOL result = ::CreateProcessW(
wexecutable.c_str(), pwcommandLine, NULL, NULL,
/*bInheritHandles=*/!inherited_handles.empty(), flags, environment.data(),
wworkingDirectory.size() == 0 ? NULL : wworkingDirectory.c_str(),
reinterpret_cast<STARTUPINFOW *>(&startupinfoex), &pi);
if (!result) {
// Call GetLastError before we make any other system calls.
error = Status(::GetLastError(), eErrorTypeWin32);
// Note that error 50 ("The request is not supported") will occur if you
// try debug a 64-bit inferior from a 32-bit LLDB.
}
if (result) {
// Do not call CloseHandle on pi.hProcess, since we want to pass that back
// through the HostProcess.
::CloseHandle(pi.hThread);
}
if (!result)
return HostProcess();
return HostProcess(pi.hProcess);
}
HANDLE
ProcessLauncherWindows::GetStdioHandle(const ProcessLaunchInfo &launch_info,
int fd) {
const FileAction *action = launch_info.GetFileActionForFD(fd);
if (action == nullptr)
return NULL;
SECURITY_ATTRIBUTES secattr = {};
secattr.nLength = sizeof(SECURITY_ATTRIBUTES);
secattr.bInheritHandle = TRUE;
llvm::StringRef path = action->GetPath();
DWORD access = 0;
DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
DWORD create = 0;
DWORD flags = 0;
if (fd == STDIN_FILENO) {
access = GENERIC_READ;
create = OPEN_EXISTING;
flags = FILE_ATTRIBUTE_READONLY;
}
if (fd == STDOUT_FILENO || fd == STDERR_FILENO) {
access = GENERIC_WRITE;
create = CREATE_ALWAYS;
if (fd == STDERR_FILENO)
flags = FILE_FLAG_WRITE_THROUGH;
}
std::wstring wpath;
llvm::ConvertUTF8toWide(path, wpath);
HANDLE result = ::CreateFileW(wpath.c_str(), access, share, &secattr, create,
flags, NULL);
return (result == INVALID_HANDLE_VALUE) ? NULL : result;
}