Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions src/main/native/windows/util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -313,15 +313,21 @@ wstring AsExecutablePathForCreateProcess(wstring path, wstring* quoted_path,
// 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"";
// This works only for an absolute, normalized path to a plain executable or
// a batch file. Batch files are not prefixed with \\?\ because cmd.exe does
// not support the long-path prefix.
wstring native_path = path;
std::replace(native_path.begin(), native_path.end(), L'/', L'\\');
if (IsAbsoluteNormalizedWindowsPath(native_path)) {
QuotePath(native_path, quoted_path);
if (IsBatchFile(path)) {
// cmd.exe does NOT allow for the longpath prefix, so simply return the
// path without it.
*extended_path = native_path;
} else {
*extended_path = wstring(L"\\\\?\\") + native_path;
}
return L"";
}
return MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"AsExecutablePathForCreateProcess", path, error);
Expand Down
11 changes: 7 additions & 4 deletions src/main/native/windows/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,13 @@ wstring AsShortPath(wstring path, wstring* result);
// 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, if `path` is an absolute, normalized path, `extended_path` is set
// for use as CreateProcessW's lpApplicationName:
// - For plain executables: the extended-length form ("\\?\" prefix plus
// `path`), which lifts the MAX_PATH limit from lpApplicationName.
// - For batch files (.bat, .cmd): the native path without the "\\?\" prefix,
// because cmd.exe cannot handle extended-length paths.
// In both cases, `quoted_path` is set to the quoted `path`.
// Otherwise this function fails and returns an error message.
wstring AsExecutablePathForCreateProcess(wstring path, wstring* quoted_path,
wstring* extended_path);
Expand Down
1 change: 1 addition & 0 deletions src/test/native/windows/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ cc_test(
deps = select({
"//src/conditions:windows": [
":test_deps",
"//src/main/cpp:blaze_util",
"//src/main/cpp/util:filesystem",
"//src/main/native/windows:lib-file",
"//src/main/native/windows:lib-process",
Expand Down
114 changes: 114 additions & 0 deletions src/test/native/windows/process_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@
#include <vector>

#include "gtest/gtest.h"
#include "src/main/cpp/blaze_util_platform.h"
#include "src/main/cpp/util/file_platform.h"
#include "src/main/cpp/util/path.h"
#include "src/main/cpp/util/strings.h"
#include "src/main/native/windows/util.h"
#include "src/test/cpp/util/windows_test_util.h"
#include "rules_cc/cc/runfiles/runfiles.h"

namespace {
Expand Down Expand Up @@ -243,4 +247,114 @@ TEST(ProcessTest, WindowsEscapeArgTest) {
});
}

// Verifies that a batch file whose path is >= kMaxPath (MAX_PATH - 4) but
// < MAX_PATH can be executed via CreateProcessW when the
// AsExecutablePathForCreateProcess fallback returns the native path (without
// the "\\?\" prefix that cmd.exe cannot handle).
//
// The directory tree uses single-character names that are already
// as short as they can be, so GetShortPathNameW cannot shorten the path below kMaxPath.
// This forces AsExecutablePathForCreateProcess into its fallback path.
TEST(ProcessTest, BatchFileWithLongPathExecutes) {
static constexpr size_t kMaxPath = MAX_PATH - 4;
static const std::wstring kUncPrefix(L"\\\\?\\");

// Obtain TEST_TMPDIR in 8.3-shortened form so that every component is
// already as short as it can get.
std::string tmpdir_str;
std::string short_error;
ASSERT_TRUE(blaze_util::AsShortWindowsPath(
blaze::GetPathEnv("TEST_TMPDIR"), &tmpdir_str, &short_error))
<< short_error;
std::wstring tmpdir = blaze_util::CstringToWstring(tmpdir_str);

// We want at least a few directories + the batch file name
ASSERT_LT(tmpdir.size(), kMaxPath - 20)
<< "TEST_TMPDIR is too long for this test";

// Test root so we can recursively remove everything later on without affecting
// any other tests.
std::wstring test_root = tmpdir + L"\\bl";

// Build a deep tree of single-char directories until the full batch file
// path (dir + "\test.bat") is >= kMaxPath (256) but < MAX_PATH (260).
std::wstring bat_name = L"\\test.bat";
std::wstring dir_path = test_root;
const size_t target_dir_len = kMaxPath - bat_name.size();
while (dir_path.size() < target_dir_len) {
dir_path += L"\\a";
}
ASSERT_TRUE(blaze_util::MakeDirectoriesW(dir_path, 0755));

std::wstring bat_path = dir_path + bat_name;
ASSERT_GE(bat_path.size(), kMaxPath);
ASSERT_LT(bat_path.size(), (size_t)MAX_PATH);

// Write a batch file that echoes a known marker.
const std::string bat_content = "@echo off\r\necho BATCH_OK\r\n";
ASSERT_TRUE(blaze_util::CreateDummyFile(kUncPrefix + bat_path, bat_content));

// Verify the batch fallback returns the native path (no "\\?\" prefix).
std::wstring quoted_path, extended_path;
std::wstring error = bazel::windows::AsExecutablePathForCreateProcess(
bat_path, &quoted_path, &extended_path);
ASSERT_EQ(error, L"");
EXPECT_EQ(extended_path, bat_path);
EXPECT_EQ(quoted_path, L"\"" + bat_path + L"\"");

// Execute the batch file via WaitableProcess, the actual production path.
// WaitableProcess::Create internally calls AsExecutablePathForCreateProcess,
// so this proves the batch file fallback works end-to-end.
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = nullptr;
sa.bInheritHandle = TRUE;

bazel::windows::AutoHandle devnull(::CreateFileW(
L"NUL", GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &sa,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
ASSERT_TRUE(devnull.IsValid());

HANDLE pipe_read_h, pipe_write_h;
ASSERT_TRUE(::CreatePipe(&pipe_read_h, &pipe_write_h, &sa, 0x10000));
bazel::windows::AutoHandle pipe_read(pipe_read_h);
bazel::windows::AutoHandle pipe_write(pipe_write_h);

HANDLE stderr_h;
ASSERT_TRUE(::DuplicateHandle(
GetCurrentProcess(), GetStdHandle(STD_ERROR_HANDLE),
GetCurrentProcess(), &stderr_h, 0, TRUE, DUPLICATE_SAME_ACCESS));
bazel::windows::AutoHandle stderr_dup(stderr_h);

bazel::windows::WaitableProcess proc;
std::wstring proc_error;
ASSERT_TRUE(proc.Create(bat_path, L"", nullptr, L".", devnull, pipe_write,
stderr_dup, nullptr, &proc_error))
<< blaze_util::WstringToCstring(proc_error);

ASSERT_EQ(proc.WaitFor(3000, nullptr, &proc_error),
bazel::windows::WaitableProcess::kWaitSuccess)
<< blaze_util::WstringToCstring(proc_error);
EXPECT_EQ(proc.GetExitCode(&proc_error), 0)
<< blaze_util::WstringToCstring(proc_error);

pipe_write = INVALID_HANDLE_VALUE;
char stdout_buf[0x1000];
DWORD bytes_read = 0;
if (!::ReadFile(pipe_read, stdout_buf, sizeof(stdout_buf) - 1, &bytes_read,
nullptr)) {
DWORD err = ::GetLastError();
ASSERT_EQ(err, (DWORD)0);
}
stdout_buf[bytes_read] = '\0';

EXPECT_NE(std::string(stdout_buf, bytes_read).find("BATCH_OK"),
std::string::npos)
<< "Expected BATCH_OK in output, got: " << stdout_buf;

EXPECT_TRUE(blaze_util::RemoveRecursively(
blaze_util::WstringToCstring(test_root)));
}

} // namespace
35 changes: 27 additions & 8 deletions src/test/native/windows/util_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,18 @@ static void DeleteDirsUnder(const wstring& basedir,
ASSERT_EQ(actual, wstring(L"\"") + input + L"\""); \
}

// This is a macro so the assertions will have the correct line number.
#define ASSERT_BATCH_FALLBACK_SUCCEEDS(/* const WCHAR* */ input) \
{ \
wstring actual; \
wstring extended_path; \
ASSERT_EQ( \
AsExecutablePathForCreateProcess(input, &actual, &extended_path), \
L""); \
ASSERT_EQ(extended_path, wstring(input)); \
ASSERT_EQ(actual, wstring(L"\"") + input + L"\""); \
}

TEST(WindowsUtilTest, TestAsExecutablePathForCreateProcessBadInputs) {
ASSERT_SHORTENING_FAILS(L"", L"should not be empty");
ASSERT_SHORTENING_FAILS(L"\"cmd.exe\"", L"path should not be quoted");
Expand Down Expand Up @@ -324,10 +336,12 @@ TEST(WindowsUtilTest, TestAsExecutablePathForCreateProcessConversions) {
// When i>0 then `wfilename` is at least `kMaxPath` long, so
// `AsExecutablePathForCreateProcess` will attempt to shorten it, but
// because the file doesn't yet exist, the shortening attempt will fail.
// For batch files, the fallback returns the native path (without the
// "\\?\" prefix) because cmd.exe cannot handle extended-length paths.
if (i > 0) {
ASSERT_EQ(::GetFileAttributesW(wfilename.c_str()),
INVALID_FILE_ATTRIBUTES);
ASSERT_SHORTENING_FAILS(wfilename.c_str(), L"GetShortPathNameW");
ASSERT_BATCH_FALLBACK_SUCCEEDS(wfilename.c_str());
}

// Create the file, now we should be able to shorten it when i=0, but not
Expand All @@ -340,8 +354,9 @@ TEST(WindowsUtilTest, TestAsExecutablePathForCreateProcessConversions) {
} else {
// The wfilename was too long to begin with, and it was impossible to
// shorten any of the segments (since we deliberately created them that
// way), so shortening failed.
ASSERT_SHORTENING_FAILS(wfilename.c_str(), L"cannot shorten the path");
// way), so shortening failed. Batch files still succeed via the fallback
// which returns the native path without the "\\?\" prefix.
ASSERT_BATCH_FALLBACK_SUCCEEDS(wfilename.c_str());
}
DELETE_FILE(wfilename);
}
Expand All @@ -358,8 +373,9 @@ TEST(WindowsUtilTest, TestAsExecutablePathForCreateProcessConversions) {
wstring(L".bat");
ASSERT_GT(wshortenable.size(), kMaxPath);

// Attempt to shorten. It will fail because the file doesn't exist yet.
ASSERT_SHORTENING_FAILS(wshortenable, L"GetShortPathNameW");
// Attempt to shorten. It will fail because the file doesn't exist yet, but
// batch files still succeed via the fallback (native path, no "\\?\" prefix).
ASSERT_BATCH_FALLBACK_SUCCEEDS(wshortenable);

// Create the file so shortening will succeed.
CREATE_FILE(wshortenable);
Expand All @@ -378,9 +394,12 @@ TEST(WindowsUtilTest, TestAsExecutablePathForCreateProcessFallback) {
// 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");
// Batch files (.bat, .cmd) get the fallback with the native path (no "\\?\"
// prefix) because cmd.exe cannot handle extended-length paths.
ASSERT_BATCH_FALLBACK_SUCCEEDS(dir + L"\\foo.bat");
ASSERT_BATCH_FALLBACK_SUCCEEDS(dir + L"\\foo.cmd");
ASSERT_BATCH_FALLBACK_SUCCEEDS(dir + L"\\foo.BAT");
ASSERT_BATCH_FALLBACK_SUCCEEDS(dir + L"\\foo.Cmd");

// A non-normalized path is denied the fallback because the "\\?\" prefix
// disables path normalization, so "." and ".." would reach the filesystem
Expand Down
2 changes: 1 addition & 1 deletion tools/test/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ cc_binary(
}),
target_compatible_with = ["@platforms//os:windows"],
visibility = ["//visibility:private"],
deps = [":tw_lib"],
deps = [":tw_lib", ":tw_resources"],
)

# Test wrapper binary to run tests on Windows.
Expand Down
Loading