PAL: Skip directories when looking for executables for CreateProcess#126888
PAL: Skip directories when looking for executables for CreateProcess#126888
Conversation
Mirror the fix from dotnet/diagnostics#5803: add fileExistsAndIsNotDirectory helper to process.cpp that uses stat() to verify a PATH candidate is a file, not a directory. Update getPath to use this helper in all three search locations (app dir, current dir, $PATH elements) so that directories with the same name as the target executable are skipped rather than treated as a match, matching Windows CreateProcessW behavior. Add threading/CreateProcessW/test3 PAL test that verifies: - (negative) CreateProcessW returns ERROR_FILE_NOT_FOUND when only a directory with the target name exists in PATH - (positive) CreateProcessW skips a directory blocker and finds the real executable in a later PATH element Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/dacf2636-f293-46c2-a747-fcbbd01bdbf6 Co-authored-by: hoyosjs <19413848+hoyosjs@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/dacf2636-f293-46c2-a747-fcbbd01bdbf6 Co-authored-by: hoyosjs <19413848+hoyosjs@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR updates Unix PAL CreateProcessW PATH resolution to skip directories when searching for an executable (aligning more closely with Windows behavior) and adds a new palsuite test to validate the behavior.
Changes:
- Add a
getPathhelper check to ignore PATH candidates that are directories. - Add new
CreateProcessW/test3(parent + child) to validate directory-skipping behavior in PATH resolution. - Register the new test in palsuite build/test lists.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/coreclr/pal/src/thread/process.cpp | Adds a helper used by getPath to skip directory matches during PATH/app-dir/current-dir probing. |
| src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test3/test3.cpp | New parent test covering negative/positive PATH scenarios involving a directory named like the executable. |
| src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test3/test3_child.cpp | New child entrypoint used by the parent test when launched via PATH resolution. |
| src/coreclr/pal/tests/palsuite/paltestlist.txt | Registers the new CreateProcessW/test3 parent test for execution. |
| src/coreclr/pal/tests/palsuite/CMakeLists.txt | Includes the new test sources in the paltests build. |
| /* Save the current PATH */ | ||
| savedPathLen = GetEnvironmentVariableA("PATH", savedPath, sizeof(savedPath)); | ||
| if (savedPathLen == 0 && GetLastError() != ERROR_ENVVAR_NOT_FOUND) | ||
| { | ||
| CleanupTestDirs(tmpDir1, blockerDir, tmpDir2, symlinkPath); | ||
| Fail("GetEnvironmentVariableA(PATH) failed with error %u\n", GetLastError()); | ||
| } | ||
| if (savedPathLen >= sizeof(savedPath)) | ||
| { | ||
| CleanupTestDirs(tmpDir1, blockerDir, tmpDir2, symlinkPath); | ||
| Fail("PATH environment variable is too long to save\n"); | ||
| } |
There was a problem hiding this comment.
GetEnvironmentVariableA("PATH", ...) can return 0 with ERROR_ENVVAR_NOT_FOUND, but savedPath is never initialized in that case and later gets used to restore PATH. That can leave PATH set to garbage or an empty string instead of restoring the original state. Track whether PATH existed and restore with SetEnvironmentVariableA("PATH", NULL) when it didn’t, and/or initialize savedPath[0] = '\0' before calling GetEnvironmentVariableA.
| /* Create a symlink named CHILD_EXE_NAME inside tmpDir2 pointing to paltests */ | ||
| snprintf(symlinkPath, sizeof(symlinkPath), "%s/" CHILD_EXE_NAME, tmpDir2); | ||
| if (symlink(argv[0], symlinkPath) != 0) | ||
| { | ||
| CleanupTestDirs(tmpDir1, blockerDir, tmpDir2, NULL); | ||
| Fail("symlink(%s -> %s) failed with errno %d\n", symlinkPath, argv[0], errno); | ||
| } |
There was a problem hiding this comment.
The symlink target uses argv[0] directly. If argv[0] is a relative path (common in test harnesses), the symlink created in tmpDir2 will point to a non-existent location when resolved from that directory, causing the positive test to fail. Resolve argv[0] to an absolute path (e.g., via realpath) before calling symlink (or create the symlink using an absolute target path).
|
|
||
| /* Build the command line (wide): CHILD_EXE_NAME CHILD_TEST_NAME */ | ||
| snprintf(cmdLineA, sizeof(cmdLineA), CHILD_EXE_NAME " " CHILD_TEST_NAME); | ||
| MultiByteToWideChar(CP_ACP, 0, cmdLineA, -1, cmdLineW, (int)(sizeof(cmdLineW) / sizeof(WCHAR))); |
There was a problem hiding this comment.
The return value of MultiByteToWideChar isn’t checked here. If conversion fails (or the buffer is too small), cmdLineW may be left uninitialized and CreateProcessW behavior becomes undefined. Capture the return value and Fail() on 0 (as done in the existing CreateProcessW test1/test2).
| MultiByteToWideChar(CP_ACP, 0, cmdLineA, -1, cmdLineW, (int)(sizeof(cmdLineW) / sizeof(WCHAR))); | |
| if (MultiByteToWideChar(CP_ACP, 0, cmdLineA, -1, cmdLineW, (int)(sizeof(cmdLineW) / sizeof(WCHAR))) == 0) | |
| { | |
| CleanupTestDirs(tmpDir1, blockerDir, tmpDir2, symlinkPath); | |
| Fail("MultiByteToWideChar failed with error %u\n", GetLastError()); | |
| } |
| /* Build unique temp directory names using the process ID */ | ||
| DWORD pid = GetCurrentProcessId(); | ||
| snprintf(tmpDir1, sizeof(tmpDir1), "/tmp/paltest_cpw_t3_%u_d1", pid); | ||
| snprintf(tmpDir2, sizeof(tmpDir2), "/tmp/paltest_cpw_t3_%u_d2", pid); | ||
| blockerDir[0] = '\0'; |
There was a problem hiding this comment.
The test hardcodes temp directories under /tmp. Other CreateProcessW palsuite tests use GetTempPath* to respect platform/temp directory configuration. Consider building the temp dir paths under GetTempPathA (and using a unique suffix like mkdtemp-style) to avoid failures on systems where /tmp is unavailable or not writable.
🤖 Copilot Code Review — PR #126888Note This review was generated by Copilot and should be treated as AI-generated analysis. Holistic AssessmentMotivation: The PR mirrors a fix from dotnet/diagnostics#5803 for a real bug: when a directory has the same name as the target executable in Approach: The fix is well-targeted — a small Summary: ✅ LGTM. The core fix in Detailed Findings✅ Correctness —
|
This pull request improves the behavior of
CreateProcessWon Unix-like systems to more closely match Windows by ensuring directories in thePATHare skipped when searching for executables. It also adds new tests to validate this behavior.