[9.2.0] Fix launching executables whose path can't be shortened on Windows (https://github.com/bazelbuild/bazel/pull/29921) (#29984)
### Description
`AsExecutablePathForCreateProcess` shortens an executable's path with
`GetShortPathNameW` so it fits `CreateProcessW`'s `MAX_PATH`-bound
command line, and currently fails outright when shortening can't bring
it under `MAX_PATH`.
This change adds a fallback: it launches the executable through
`CreateProcessW`'s _lpApplicationName_, which is not subject to the
`MAX_PATH` limit, in extended-length (`\\?\`) form.
Supplying _lpApplicationName_ also lifts the `MAX_PATH` limit from the
executable part of _lpCommandLine_, so the original path is kept there
as `argv[0]`.
Shortening is still attempted first, so the change is a pure superset of
the current behavior: paths that shorten today are launched unchanged,
and only the currently-failing ones take the fallback.
The fallback is limited to absolute, normalized paths to plain
executables:
- a batch file is not a valid _lpApplicationName_ as it must be run
through a command interpreter,
- neither is a non-normalized path because its `.`/`..` components would
survive unresolved in the extended-length path.
The fix relies on `\\?\` bypassing `MAX_PATH` at the path-parsing layer,
**independently** of the _LongPathsEnabled_ registry value and the
_longPathAware_ manifest.
### Motivation
Fixes #19710.
Avoid errors like:
```
==================== Test output for //some/configuration/issues/invalidconfig:invalidconfig_test:
ERROR(tools/test/windows/tw.cc:1307) ERROR: src/main/native/windows/process.cc(82): WaitableProcess::Create(C:\user\execroot\_main\bazel-out\x64_windows-fastbuild-ST-000000000001\bin\some\configuration\issues\invalidconfig\invalidconfig_test_\invalidconfig_test.exe.runfiles\_main\some\configuration\issues\invalidconfig\invalidconfig_test_\invalidconfig_test.exe): ERROR: src/main/native/windows/util.cc(292): AsExecutablePathForCreateProcess(C:\user\execroot\_main\bazel-out\x64_windows-fastbuild-ST-000000000001\bin\some\configuration\issues\invalidconfig\invalidconfig_test_\invalidconfig_test.exe.runfiles\_main\some\configuration\issues\invalidconfig\invalidconfig_test_\invalidconfig_test.exe): ERROR: src/main/native/windows/util.cc(262): GetShortPathNameW(\\?\C:\user\execroot\_main\bazel-out\x64_windows-fastbuild-ST-000000000001\bin\some\configuration\issues\invalidconfig\invalidconfig_test_\invalidconfig_test.exe.runfiles\_main\some\configuration\issues\invalidconfig\invalidconfig_test_\invalidconfig_test.exe): cannot shorten the path enough
ERROR(tools/test/windows/tw.cc:1517) Failed to start test process (arg: C:\user\execroot\_main\bazel-out\x64_windows-fastbuild-ST-000000000001\bin\some\configuration\issues\invalidconfig\invalidconfig_test_\invalidconfig_test.exe.runfiles\_main\some\configuration\issues\invalidconfig\invalidconfig_test_\invalidconfig_test.exe)
================================================================================
```
Shortening fails in two situations, both handled here:
- 8dot3 short-name creation is disabled on the volume (common in
containers, microsoft/Windows-Containers#507), so `GetShortPathNameW` is
a no-op and a long runfiles executable path stays over `MAX_PATH`,
- the path is nested so deeply that even fully 8dot3-shortened it still
overflows `MAX_PATH`.
Prior art: Rust's `std::process::Command` passes a verbatim `\\?\` path
as _lpApplicationName_ for a >`MAX_PATH` executable and special-cases
batch files the same way:
- rust-lang/rust#87704,
- rust-lang/rust#92519.
### Build API Changes
No
### Checklist
- [x] I have added tests for the new use cases (if any).
- [ ] I have updated the documentation (if applicable).
### Release Notes
RELNOTES: Fixed Bazel being unable to run a Windows executable whose
path is too long to shorten under `MAX_PATH`, e.g. when 8dot3 short
names are disabled (microsoft/Windows-Containers#507).
Closes #29921.
PiperOrigin-RevId: 937426905
Change-Id: Ia48d41e000622fc122d1b258ad15daa5018be75e
Commit
https://github.com/bazelbuild/bazel/commit/4a80d43e1640d8d1eedcef4a733be3d1b13be6c7
Co-authored-by: Rgis Desgroppes <rdesgroppes@gmail.com>
diff --git a/src/main/native/windows/process.cc b/src/main/native/windows/process.cc
index 4646a65..c8f0a69 100644
--- a/src/main/native/windows/process.cc
+++ b/src/main/native/windows/process.cc
@@ -77,7 +77,9 @@
}
std::wstring argv0short;
- error_msg = AsExecutablePathForCreateProcess(argv0, &argv0short);
+ std::wstring extended_path;
+ error_msg =
+ AsExecutablePathForCreateProcess(argv0, &argv0short, &extended_path);
if (!error_msg.empty()) {
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"WaitableProcess::Create", argv0, error_msg);
@@ -159,7 +161,8 @@
STARTUPINFOEXW info;
attr_list->InitStartupInfoExW(&info);
if (!CreateProcessW(
- /* lpApplicationName */ nullptr,
+ /* lpApplicationName */ extended_path.empty() ? nullptr
+ : extended_path.c_str(),
/* lpCommandLine */ mutable_commandline.get(),
/* lpProcessAttributes */ nullptr,
/* lpThreadAttributes */ nullptr,
diff --git a/src/main/native/windows/util.cc b/src/main/native/windows/util.cc
index 615d099..872fb10 100644
--- a/src/main/native/windows/util.cc
+++ b/src/main/native/windows/util.cc
@@ -194,6 +194,15 @@
return s.find(substr) != wstring::npos;
}
+static bool IsBatchFile(const wstring& path) {
+ if (path.size() < 4) {
+ return false;
+ }
+ const wchar_t* ext = path.c_str() + (path.size() - 4);
+ return CompareStringOrdinal(ext, 4, L".bat", 4, TRUE) == CSTR_EQUAL ||
+ CompareStringOrdinal(ext, 4, L".cmd", 4, TRUE) == CSTR_EQUAL;
+}
+
wstring AsShortPath(wstring path, wstring* result) {
// Using `MAX_PATH` - 4 (256) instead of `MAX_PATH` to fix
// https://github.com/bazelbuild/bazel/issues/12310
@@ -267,7 +276,8 @@
return L"";
}
-wstring AsExecutablePathForCreateProcess(wstring path, wstring* result) {
+wstring AsExecutablePathForCreateProcess(wstring path, wstring* quoted_path,
+ wstring* extended_path) {
if (path.empty()) {
return MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"AsExecutablePathForCreateProcess", path,
@@ -287,16 +297,34 @@
}
path = cwd + L"\\" + path;
}
- wstring error = AsShortPath(path, result);
- if (!error.empty()) {
- return MakeErrorMessage(WSTR(__FILE__), __LINE__,
- L"AsExecutablePathForCreateProcess", path, error);
+ wstring error = AsShortPath(path, quoted_path);
+ if (error.empty()) {
+ // Quote the path in case it's something like "c:\foo\app name.exe".
+ // Do this unconditionally, there's no harm in quoting. Quotes are not
+ // allowed inside paths so we don't need to escape quotes.
+ QuotePath(*quoted_path, quoted_path);
+ extended_path->clear();
+ return L"";
}
- // Quote the path in case it's something like "c:\foo\app name.exe".
- // Do this unconditionally, there's no harm in quoting. Quotes are not
- // allowed inside paths so we don't need to escape quotes.
- QuotePath(*result, result);
- return L"";
+ // Shortening might fail (https://github.com/bazelbuild/bazel/issues/19710),
+ // typically when 8.3 aliases are disabled on the volume, which is common in
+ // containers (https://github.com/microsoft/Windows-Containers/issues/507).
+ // As a fallback, set `quoted_path` as above and `extended_path` to the
+ // extended-length form of `path`, suitable for CreateProcessW's
+ // lpApplicationName: it is not subject to MAX_PATH, and providing it lifts
+ // that limit from the executable part of CreateProcessW's lpCommandLine too.
+ // This works only for a plain executable with an absolute, normalized path.
+ if (!IsBatchFile(path)) {
+ wstring native_path = path;
+ std::replace(native_path.begin(), native_path.end(), L'/', L'\\');
+ if (IsAbsoluteNormalizedWindowsPath(native_path)) {
+ QuotePath(native_path, quoted_path);
+ *extended_path = wstring(L"\\\\?\\") + native_path;
+ return L"";
+ }
+ }
+ return MakeErrorMessage(WSTR(__FILE__), __LINE__,
+ L"AsExecutablePathForCreateProcess", path, error);
}
} // namespace windows
diff --git a/src/main/native/windows/util.h b/src/main/native/windows/util.h
index a98e7f8..1a107fd 100644
--- a/src/main/native/windows/util.h
+++ b/src/main/native/windows/util.h
@@ -136,12 +136,15 @@
// Same as `AsExecutablePathForCreateProcess` except it won't quote the result.
wstring AsShortPath(wstring path, wstring* result);
-// Computes a path suitable as the executable part in CreateProcessA's cmdline.
+// Computes a path suitable as the executable part in CreateProcessW's
+// lpCommandLine, plus an extended-length form suitable for CreateProcessW's
+// lpApplicationName when applicable.
//
-// The null-terminated executable path for CreateProcessA has to fit into
-// MAX_PATH, therefore the limit for the executable's path is MAX_PATH - 1
-// (not including null terminator). This method attempts to convert the input
-// `path` to a short format to fit it into the MAX_PATH - 1 limit.
+// Unless CreateProcessW's lpApplicationName is populated, the null-terminated
+// executable path in CreateProcessW's lpCommandLine has to fit into MAX_PATH,
+// therefore the limit for the executable's path is MAX_PATH - 1 (not including
+// null terminator). This method attempts to convert the input `path` to a short
+// format to fit it into the MAX_PATH - 1 limit.
//
// `path` must be either an absolute, normalized, Windows-style path with drive
// letter (e.g. "c:\foo\bar.exe", but no "\foo\bar.exe"), or must be just a file
@@ -149,17 +152,23 @@
// In both cases, `path` must be unquoted.
//
// If this function succeeds, it returns an empty string (indicating no error),
-// and sets `result` to the resulting path, which is always quoted, and is
+// and sets `quoted_path` to the resulting path, which is always quoted, and is
// always at most MAX_PATH + 1 long (MAX_PATH - 1 without null terminator, plus
-// two quotes). If there's any error, this function returns the error message.
+// two quotes), unless the `extended_path` fallback below applies. If there's
+// any error, this function returns the error message.
//
// If `path` is at most MAX_PATH - 1 long (not including null terminator), the
// result will be that (plus quotes).
// Otherwise this method attempts to compute an 8dot3 style short name for
// `path`, and if that succeeds and the result is at most MAX_PATH - 1 long (not
// including null terminator), then that will be the result (plus quotes).
+// Otherwise, if `path` is an absolute, normalized path to a plain executable
+// (not a batch file), `extended_path` is set to its extended-length form (the
+// "\\?\" prefix plus `path`) for use as CreateProcessW's lpApplicationName, and
+// `quoted_path` to the quoted `path`.
// Otherwise this function fails and returns an error message.
-wstring AsExecutablePathForCreateProcess(wstring path, wstring* result);
+wstring AsExecutablePathForCreateProcess(wstring path, wstring* quoted_path,
+ wstring* extended_path);
} // namespace windows
} // namespace bazel
diff --git a/src/test/java/com/google/devtools/build/lib/windows/WindowsProcessesTest.java b/src/test/java/com/google/devtools/build/lib/windows/WindowsProcessesTest.java
index f01c236..a28ad58 100644
--- a/src/test/java/com/google/devtools/build/lib/windows/WindowsProcessesTest.java
+++ b/src/test/java/com/google/devtools/build/lib/windows/WindowsProcessesTest.java
@@ -328,6 +328,24 @@
}
@Test
+ public void testExecutableWithExtendedLengthPath() throws Exception {
+ StringBuilder dir = new StringBuilder(System.getenv("TEST_TMPDIR"));
+ for (char c = 'a'; c <= 'z'; c++) {
+ dir.append('\\').repeat(c, 8).append('.').repeat(c, 3);
+ }
+ String exe = dir + "\\cmd.exe";
+ assertThat(exe.length()).isGreaterThan(260); // Windows MAX_PATH
+ Files.createDirectories(Paths.get("\\\\?\\" + dir));
+ Files.copy(
+ Paths.get(System.getenv("SystemRoot"), "System32", "cmd.exe"), Paths.get("\\\\?\\" + exe));
+
+ process = WindowsProcesses.createProcess(exe, "/c exit 42", null, null, null, null);
+ assertNoProcessError();
+ assertThat(WindowsProcesses.waitFor(process, -1)).isEqualTo(0);
+ assertThat(WindowsProcesses.getExitCode(process)).isEqualTo(42);
+ }
+
+ @Test
public void testReadingAndWritingAfterTermination() throws Exception {
process =
WindowsProcesses.createProcess(mockBinary, mockArgs("X42"), null, null, null, null);
diff --git a/src/test/native/windows/util_test.cc b/src/test/native/windows/util_test.cc
index 2d4cd54..09cbde1 100644
--- a/src/test/native/windows/util_test.cc
+++ b/src/test/native/windows/util_test.cc
@@ -236,21 +236,40 @@
}
// This is a macro so the assertions will have the correct line number.
-#define ASSERT_SHORTENING_FAILS(/* const WCHAR* */ input, \
- /* const WCHAR* */ error_msg) \
- { \
- wstring actual; \
- wstring result(AsExecutablePathForCreateProcess(input, &actual)); \
- ASSERT_CONTAINS(result, error_msg); \
+#define ASSERT_SHORTENING_FAILS(/* const WCHAR* */ input, \
+ /* const WCHAR* */ error_msg) \
+ { \
+ wstring actual; \
+ wstring extended_path; \
+ wstring result( \
+ AsExecutablePathForCreateProcess(input, &actual, &extended_path)); \
+ ASSERT_CONTAINS(result, error_msg); \
+ ASSERT_EQ(extended_path, L""); \
}
// This is a macro so the assertions will have the correct line number.
-#define ASSERT_SHORTENING_SUCCEEDS(/* const WCHAR* */ input, \
- /* const wstring& */ expected_result) \
- { \
- wstring actual; \
- ASSERT_EQ(AsExecutablePathForCreateProcess(input, &actual), L""); \
- ASSERT_EQ(actual, expected_result); \
+#define ASSERT_SHORTENING_SUCCEEDS(/* const WCHAR* */ input, \
+ /* const wstring& */ expected_result) \
+ { \
+ wstring actual; \
+ wstring extended_path = L"stale"; \
+ ASSERT_EQ( \
+ AsExecutablePathForCreateProcess(input, &actual, &extended_path), \
+ L""); \
+ ASSERT_EQ(actual, expected_result); \
+ ASSERT_EQ(extended_path, L""); \
+ }
+
+// This is a macro so the assertions will have the correct line number.
+#define ASSERT_FALLBACK_SUCCEEDS(/* const WCHAR* */ input) \
+ { \
+ wstring actual; \
+ wstring extended_path; \
+ ASSERT_EQ( \
+ AsExecutablePathForCreateProcess(input, &actual, &extended_path), \
+ L""); \
+ ASSERT_EQ(extended_path, wstring(L"\\\\?\\") + input); \
+ ASSERT_EQ(actual, wstring(L"\"") + input + L"\""); \
}
TEST(WindowsUtilTest, TestAsExecutablePathForCreateProcessBadInputs) {
@@ -271,7 +290,9 @@
// Relative paths are fine, they are absolutized.
std::wstring rel(L"foo\\bar.exe");
std::wstring actual;
- EXPECT_EQ(AsExecutablePathForCreateProcess(rel, &actual), L"");
+ std::wstring extended_path;
+ EXPECT_EQ(AsExecutablePathForCreateProcess(rel, &actual, &extended_path),
+ L"");
EXPECT_GT(actual.size(), rel.size());
EXPECT_EQ(actual.rfind(rel), actual.size() - rel.size() - 1);
}
@@ -298,7 +319,7 @@
// When i=0 then `wfilename` is `kMaxPath` - 1 long, so
// `AsExecutablePathForCreateProcess` will not attempt to shorten it, and
// so it also won't notice that the file doesn't exist. If however we pass
- // a non-existent path to CreateProcessA, then it'll fail, so we'll find out
+ // a non-existent path to CreateProcessW, then it'll fail, so we'll find out
// about this error in production code.
// When i>0 then `wfilename` is at least `kMaxPath` long, so
// `AsExecutablePathForCreateProcess` will attempt to shorten it, but
@@ -349,5 +370,23 @@
DeleteDirsUnder(tmpdir, short_root);
}
+TEST(WindowsUtilTest, TestAsExecutablePathForCreateProcessFallback) {
+ wstring dir = L"c:\\" + wstring(kMaxPath, L'a');
+
+ // A deep, absolute, normalized executable path that can't be shortened (here
+ // because it doesn't exist) takes the fallback: `extended_path` gets the
+ // extended-length form and `quoted_path` the quoted path.
+ ASSERT_FALLBACK_SUCCEEDS(dir + L"\\foo.exe");
+
+ // A batch file is denied the fallback even with an absolute, normalized path:
+ // CreateProcessW's lpApplicationName can only take a plain executable.
+ ASSERT_SHORTENING_FAILS(dir + L"\\foo.bat", L"GetShortPathNameW");
+
+ // A non-normalized path is denied the fallback because the "\\?\" prefix
+ // disables path normalization, so "." and ".." would reach the filesystem
+ // verbatim instead of being resolved.
+ ASSERT_SHORTENING_FAILS(dir + L"\\..\\foo.exe", L"path is not normalized");
+}
+
} // namespace windows
} // namespace bazel