[flang-rt] Fix EXECUTE_COMMAND_LINE() on Windows (#184875)

Detect cmd.exe special status code 9009 that indicates "command not
found" condition. Crash the process if "command not found" detected when
CMDSTAT was not specified.
This commit is contained in:
Eugene Epshteyn 2026-03-05 18:17:57 -05:00 committed by GitHub
parent c9555f6675
commit 008dc8b5bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 52 additions and 18 deletions

View File

@ -111,7 +111,17 @@ std::int64_t TerminationCheck(std::int64_t status, const Descriptor *cmdstat,
// On WIN32 API std::system() returns exit status directly. On other OS'es,
// special status codes are handled below.
std::int64_t exitStatusVal{status};
#ifndef _WIN32
#ifdef _WIN32
if (status == 9009) {
// cmd.exe returns status code 9009 for "command not found" error
if (!cmdstat) {
terminator.Crash("Command not found.");
} else {
StoreIntToDescriptor(cmdstat, COMMAND_NOT_FOUND_ERR, terminator);
CheckAndCopyCharsToDescriptor(cmdmsg, "Command not found.");
}
}
#else
#if defined(WIFSIGNALED) && defined(WTERMSIG)
if (WIFSIGNALED(status)) {
@ -195,9 +205,36 @@ void RTNAME(ExecuteCommandLine)(const Descriptor &command, bool wait,
RUNTIME_CHECK(terminator, IsValidCharDescriptor(cmdmsg));
}
const char *cmd{newCmd};
#ifdef _WIN32
// Construct a string that looks like
// "cmd.exe /v:on /c \"mycommand & exit /b !ERRORLEVEL!\""
// Explanantion:
// /v:on - turns delayed environment variable expansion on, so
// variables written as !VAR! are expanded at execution time
// instead of at parse time. This is required for !ERRORLEVEL!
// to reflect the current error code at the moment exit runs.
// exit /b !ERRORLEVEL! - exits the current cmd instance (/b) and
// sets its process exit code to the current ERRORLEVEL value.
// Because delayed expansion is on, !ERRORLEVEL! is evaluated at
// execution time, so this cmd instance returns the same error
// code as mycommand.
// This allows cmd.exe to either return the exit code of mycommand, or
// to return its own exit code to the caller. The code 9009 is used
// by cmd.exe to indicate "not found" condition.
const char prefix[]{"cmd.exe /v:on /c \""};
const char suffix[]{" & exit /b !ERRORLEVEL!\""};
const size_t newCmdWinLen{
(sizeof(prefix) - 1) + std::strlen(newCmd) + (sizeof(suffix) - 1) + 1};
char *newCmdWin{
static_cast<char *>(AllocateMemoryOrCrash(terminator, newCmdWinLen))};
std::snprintf(newCmdWin, newCmdWinLen, "%s%s%s", prefix, newCmd, suffix);
cmd = newCmdWin;
#endif
if (wait) {
// either wait is not specified or wait is true: synchronous mode
std::int64_t status{std::system(newCmd)};
std::int64_t status{std::system(cmd)};
std::int64_t exitStatusVal{
TerminationCheck(status, cmdstat, cmdmsg, terminator)};
// If sync, assigned processor-dependent exit status. Otherwise unchanged
@ -211,13 +248,6 @@ void RTNAME(ExecuteCommandLine)(const Descriptor &command, bool wait,
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
// add "cmd.exe /c " to the beginning of command
const char *prefix{"cmd.exe /c "};
char *newCmdWin{static_cast<char *>(AllocateMemoryOrCrash(
terminator, std::strlen(prefix) + std::strlen(newCmd) + 1))};
std::strcpy(newCmdWin, prefix);
std::strcat(newCmdWin, newCmd);
// Convert the char to wide char
const size_t sizeNeeded{mbstowcs(NULL, newCmdWin, 0) + 1};
wchar_t *wcmd{static_cast<wchar_t *>(
@ -225,7 +255,6 @@ void RTNAME(ExecuteCommandLine)(const Descriptor &command, bool wait,
if (std::mbstowcs(wcmd, newCmdWin, sizeNeeded) == static_cast<size_t>(-1)) {
terminator.Crash("Char to wide char failed for newCmd");
}
FreeMemory(newCmdWin);
if (CreateProcessW(nullptr, wcmd, nullptr, nullptr, FALSE, 0, nullptr,
nullptr, &si, &pi)) {
@ -278,6 +307,11 @@ void RTNAME(ExecuteCommandLine)(const Descriptor &command, bool wait,
}
#endif
}
#ifdef _WIN32
FreeMemory(newCmdWin);
#endif
// Deallocate memory if EnsureNullTerminated dynamically allocated memory
if (newCmd != command.OffsetElement()) {
FreeMemory(newCmd);

View File

@ -365,9 +365,9 @@ TEST_F(ZeroArguments, ECLNotExecutedCommandErrorSync) {
RTNAME(ExecuteCommandLine)
(*command.get(), wait, exitStat.get(), cmdStat.get(), cmdMsg.get());
#ifdef _WIN32
CheckDescriptorEqInt<std::int64_t>(exitStat.get(), 1);
CheckDescriptorEqInt<std::int64_t>(cmdStat.get(), 0);
CheckDescriptorEqStr(cmdMsg.get(), "cmd msg buffer XXXXXXXX");
CheckDescriptorEqInt<std::int64_t>(exitStat.get(), 9009);
CheckDescriptorEqInt<std::int64_t>(cmdStat.get(), 5);
CheckDescriptorEqStr(cmdMsg.get(), "Command not found.");
#else
CheckDescriptorEqInt<std::int64_t>(exitStat.get(), 126);
CheckDescriptorEqInt<std::int64_t>(cmdStat.get(), 4);
@ -394,9 +394,9 @@ TEST_F(ZeroArguments, ECLNotFoundCommandErrorSync) {
RTNAME(ExecuteCommandLine)
(*command.get(), wait, exitStat.get(), cmdStat.get(), cmdMsg.get());
#ifdef _WIN32
CheckDescriptorEqInt<std::int64_t>(exitStat.get(), 1);
CheckDescriptorEqInt<std::int64_t>(cmdStat.get(), 0);
CheckDescriptorEqStr(cmdMsg.get(), "unmodified buffer XXXXXXXXX");
CheckDescriptorEqInt<std::int64_t>(exitStat.get(), 9009);
CheckDescriptorEqInt<std::int64_t>(cmdStat.get(), 5);
CheckDescriptorEqStr(cmdMsg.get(), "Command not found.");
#else
CheckDescriptorEqInt<std::int64_t>(exitStat.get(), 127);
CheckDescriptorEqInt<std::int64_t>(cmdStat.get(), 5);
@ -412,7 +412,7 @@ TEST_F(ZeroArguments, ECLInvalidCommandTerminatedSync) {
#ifdef _WIN32
EXPECT_DEATH(RTNAME(ExecuteCommandLine)(
*command.get(), wait, nullptr, nullptr, cmdMsg.get()),
"Invalid command quit with exit status code: 1");
"Command not found.");
#else
EXPECT_DEATH(RTNAME(ExecuteCommandLine)(
*command.get(), wait, nullptr, nullptr, cmdMsg.get()),
@ -490,7 +490,7 @@ TEST_F(ZeroArguments, SystemInvalidCommandExitStat) {
RTNAME(ExecuteCommandLine)
(*command.get(), wait, exitStat.get(), cmdStat.get(), nullptr);
#ifdef _WIN32
CheckDescriptorEqInt<std::int64_t>(exitStat.get(), 1);
CheckDescriptorEqInt<std::int64_t>(exitStat.get(), 9009);
#else
CheckDescriptorEqInt<std::int64_t>(exitStat.get(), 127);
#endif