diff --git a/cargo/tests/cargo_build_script/location_expansion/build.rs b/cargo/tests/cargo_build_script/location_expansion/build.rs index 2fa48547d2..b52afeef06 100644 --- a/cargo/tests/cargo_build_script/location_expansion/build.rs +++ b/cargo/tests/cargo_build_script/location_expansion/build.rs @@ -1,3 +1,15 @@ +fn execpath(name: &str) -> String { + let path = std::env::var(name).expect("Environment variable not set"); + assert!(std::path::Path::new(&path).is_absolute()); + assert!(std::path::Path::new(&path).exists()); + + let normalized = path.replace('\\', "/"); + let (_, relative) = normalized + .split_once("/bazel-out/") + .expect("execpath does not contain bazel-out"); + format!("bazel-out/{}", relative) +} + fn main() { println!( "cargo:rustc-env=DATA_ROOTPATH={}", @@ -5,7 +17,7 @@ fn main() { ); println!( "cargo:rustc-env=DATA_EXECPATH={}", - std::env::var("DATA_EXECPATH").expect("Environment variable not set") + execpath("DATA_EXECPATH") ); println!( "cargo:rustc-env=TOOL_ROOTPATH={}", @@ -13,6 +25,6 @@ fn main() { ); println!( "cargo:rustc-env=TOOL_EXECPATH={}", - std::env::var("TOOL_EXECPATH").expect("Environment variable not set") + execpath("TOOL_EXECPATH") ); } diff --git a/cargo/tests/cargo_build_script/location_expansion/test.rs b/cargo/tests/cargo_build_script/location_expansion/test.rs index 62a36030c0..f7092e91f7 100644 --- a/cargo/tests/cargo_build_script/location_expansion/test.rs +++ b/cargo/tests/cargo_build_script/location_expansion/test.rs @@ -16,37 +16,14 @@ pub fn test_tool_rootpath() { #[test] pub fn test_execpath() { - // Replace `\` to ensure paths are consistent on Windows.` - let data_execpath = env!("DATA_EXECPATH").replace('\\', "/"); - let tool_execpath = env!("TOOL_EXECPATH").replace('\\', "/"); - - let data_path = data_execpath - .split_at( - data_execpath - .find("/bazel-out/") - .unwrap_or_else(|| panic!("Failed to parse execroot from: {}", data_execpath)), - ) - .1; - let tool_path = tool_execpath - .split_at( - tool_execpath - .find("/bazel-out/") - .unwrap_or_else(|| panic!("Failed to parse execroot from: {}", tool_execpath)), - ) - .1; - - let (data_cfg, data_short_path) = data_path.split_at( - data_path - .find("/bin/") - .unwrap_or_else(|| panic!("Failed to find bin in {}", data_path)) - + "/bin/".len(), - ); - let (tool_cfg, tool_short_path) = tool_path.split_at( - tool_path - .find("/bin/") - .unwrap_or_else(|| panic!("Failed to find bin in {}", tool_path)) - + "/bin/".len(), - ); + let data_execpath = env!("DATA_EXECPATH"); + let tool_execpath = env!("TOOL_EXECPATH"); + let (data_cfg, data_short_path) = data_execpath + .split_once("/bin/") + .unwrap_or_else(|| panic!("Failed to find bin in {}", data_execpath)); + let (tool_cfg, tool_short_path) = tool_execpath + .split_once("/bin/") + .unwrap_or_else(|| panic!("Failed to find bin in {}", tool_execpath)); assert_ne!( data_cfg, tool_cfg, diff --git a/cargo/tests/cargo_build_script/nondeterministic_out_dir/BUILD.bazel b/cargo/tests/cargo_build_script/nondeterministic_out_dir/BUILD.bazel index 36dd9d5581..fce39d0658 100644 --- a/cargo/tests/cargo_build_script/nondeterministic_out_dir/BUILD.bazel +++ b/cargo/tests/cargo_build_script/nondeterministic_out_dir/BUILD.bazel @@ -10,6 +10,11 @@ cargo_build_script( rust_test( name = "test", srcs = ["test.rs"], + data = [":build_script"], edition = "2021", - deps = [":build_script"], + env = {"BUILD_SCRIPT_OUT_DIR": "$(rlocationpath :build_script)"}, + deps = [ + ":build_script", + "//rust/runfiles", + ], ) diff --git a/cargo/tests/cargo_build_script/nondeterministic_out_dir/test.rs b/cargo/tests/cargo_build_script/nondeterministic_out_dir/test.rs index be0069fc88..8c8e130dfa 100644 --- a/cargo/tests/cargo_build_script/nondeterministic_out_dir/test.rs +++ b/cargo/tests/cargo_build_script/nondeterministic_out_dir/test.rs @@ -2,11 +2,23 @@ //! If the runner failed to strip config.log / *.d / *.pc files, the TreeArtifact hash //! would change on every run, causing unnecessary rebuilds for all downstream crates. +use std::path::PathBuf; + const OUTPUT: &str = include_str!(concat!(env!("OUT_DIR"), "/output.txt")); +fn build_script_out_dir() -> PathBuf { + let runfiles = runfiles::Runfiles::create().expect("unable to resolve test runfiles"); + let out_dir = std::env::var("BUILD_SCRIPT_OUT_DIR").expect("BUILD_SCRIPT_OUT_DIR is not set"); + runfiles::rlocation!(runfiles, &out_dir).expect("unable to resolve build script OUT_DIR") +} + #[test] fn legitimate_output_survives_nondeterministic_file_removal() { assert_eq!(OUTPUT, "legitimate output"); + assert_eq!( + std::fs::read_to_string(build_script_out_dir().join("output.txt")).unwrap(), + OUTPUT, + ); } // Verify that volatile files written by the build script are absent from the @@ -17,7 +29,7 @@ fn legitimate_output_survives_nondeterministic_file_removal() { #[test] fn config_log_removed() { assert!( - !std::path::Path::new(concat!(env!("OUT_DIR"), "/config.log")).exists(), + !build_script_out_dir().join("config.log").exists(), "config.log should have been removed from OUT_DIR" ); } @@ -25,7 +37,7 @@ fn config_log_removed() { #[test] fn config_status_removed() { assert!( - !std::path::Path::new(concat!(env!("OUT_DIR"), "/config.status")).exists(), + !build_script_out_dir().join("config.status").exists(), "config.status should have been removed from OUT_DIR" ); } @@ -33,7 +45,7 @@ fn config_status_removed() { #[test] fn makefile_removed() { assert!( - !std::path::Path::new(concat!(env!("OUT_DIR"), "/Makefile")).exists(), + !build_script_out_dir().join("Makefile").exists(), "Makefile should have been removed from OUT_DIR" ); } @@ -41,7 +53,7 @@ fn makefile_removed() { #[test] fn makefile_config_removed() { assert!( - !std::path::Path::new(concat!(env!("OUT_DIR"), "/Makefile.config")).exists(), + !build_script_out_dir().join("Makefile.config").exists(), "Makefile.config should have been removed from OUT_DIR" ); } @@ -49,7 +61,7 @@ fn makefile_config_removed() { #[test] fn config_cache_removed() { assert!( - !std::path::Path::new(concat!(env!("OUT_DIR"), "/config.cache")).exists(), + !build_script_out_dir().join("config.cache").exists(), "config.cache should have been removed from OUT_DIR" ); } @@ -57,11 +69,11 @@ fn config_cache_removed() { #[test] fn dot_d_files_removed() { assert!( - !std::path::Path::new(concat!(env!("OUT_DIR"), "/foo.d")).exists(), + !build_script_out_dir().join("foo.d").exists(), "foo.d should have been removed from OUT_DIR" ); assert!( - !std::path::Path::new(concat!(env!("OUT_DIR"), "/baz.d")).exists(), + !build_script_out_dir().join("baz.d").exists(), "baz.d should have been removed from OUT_DIR" ); } @@ -69,7 +81,7 @@ fn dot_d_files_removed() { #[test] fn dot_pc_file_removed() { assert!( - !std::path::Path::new(concat!(env!("OUT_DIR"), "/foo.pc")).exists(), + !build_script_out_dir().join("foo.pc").exists(), "foo.pc should have been removed from OUT_DIR" ); } diff --git a/cargo/tests/cargo_build_script/tools_exec/build.rs b/cargo/tests/cargo_build_script/tools_exec/build.rs index 397281bd57..6fe182e1f1 100644 --- a/cargo/tests/cargo_build_script/tools_exec/build.rs +++ b/cargo/tests/cargo_build_script/tools_exec/build.rs @@ -36,11 +36,12 @@ fn main() { test_encoded_rustflags(); test_toolchain_var(); - // Pass the TOOL_PATH along to the rust_test so we can assert on it. - println!( - "cargo:rustc-env=TOOL_PATH={}", - std::env::var("TOOL").unwrap() - ); + // Pass the execution-root-relative tool path to the rust_test. + let tool_path = std::env::var("TOOL").unwrap().replace('\\', "/"); + let (_, relative_tool_path) = tool_path + .split_once("/bazel-out/") + .expect("tool path does not contain bazel-out"); + println!("cargo:rustc-env=TOOL_PATH=bazel-out/{}", relative_tool_path); // Assert that the cc and rust toolchain env vars existed and were executable. // We don't assert what happens when they're executed (in particular, we don't check for a diff --git a/cargo/tests/unit/transitive_link_search_paths/transitive_link_search_paths_test.bzl b/cargo/tests/unit/transitive_link_search_paths/transitive_link_search_paths_test.bzl index e23d9d0111..3811d570c6 100644 --- a/cargo/tests/unit/transitive_link_search_paths/transitive_link_search_paths_test.bzl +++ b/cargo/tests/unit/transitive_link_search_paths/transitive_link_search_paths_test.bzl @@ -3,7 +3,13 @@ load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") load("//cargo:defs.bzl", "cargo_build_script") load("//rust:defs.bzl", "rust_binary", "rust_common", "rust_library", "rust_proc_macro") -load("//test/unit:common.bzl", "assert_action_mnemonic", "assert_list_contains") +load( + "//test/unit:common.bzl", + "assert_action_mnemonic", + "assert_list_contains", + "assert_list_contains_adjacent_elements", + "assert_list_contains_adjacent_elements_not", +) def _transitive_link_search_paths_test_impl(ctx): env = analysistest.begin(ctx) @@ -15,6 +21,28 @@ def _transitive_link_search_paths_test_impl(ctx): # of the dep of the proc_macro. asserts.equals(env, link_search_path_basenames, ["dep_build_script.linksearchpaths"]) + action = tut.actions[0] + assert_action_mnemonic(env, action, "Rustc") + archive = tut[DefaultInfo].files.to_list()[0] + checked_output = archive + if any([arg.endswith("-windows-msvc") for arg in action.argv]): + object_files = [ + output + for output in action.outputs.to_list() + if output.extension in ("o", "obj") + ] + asserts.equals(env, 1, len(object_files)) + checked_output = object_files[0] + assert_list_contains_adjacent_elements_not(env, action.argv, [ + "--check-output-for-working-dir", + archive.path, + ]) + + assert_list_contains_adjacent_elements(env, action.argv, [ + "--check-output-for-working-dir", + checked_output.path, + ]) + return analysistest.end(env) transitive_link_search_paths_test = analysistest.make(_transitive_link_search_paths_test_impl) diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl index 1b647d44f5..b6412521dd 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -1879,11 +1879,24 @@ def rustc_compile( distributed_thin_lto and crate_info.type == "bin" ) - # output_o is consumed by cc_common.link or exported for distributed - # ThinLTO. crate_info.output.basename keeps output_o distinct for targets - # that share crate_info.name, including rust_test targets for the same crate. + scan_msvc_archive_object = ( + rust_toolchain.target_abi == "msvc" and + ( + crate_info.type == "staticlib" or + ( + crate_info.type in ("lib", "rlib") and + build_info != None and + build_info.out_dir != None + ) + ) + ) + + # output_o is consumed by cc_common.link, exported for distributed ThinLTO, + # or scanned instead of an MSVC archive containing native-object paths. + # crate_info.output.basename keeps output_o distinct for targets that share + # crate_info.name, including rust_test targets for the same crate. output_o = None - if use_cc_common_link or distributed_thin_lto: + if use_cc_common_link or distributed_thin_lto or scan_msvc_archive_object: output_o = ctx.actions.declare_file(crate_info.output.basename + ".o", sibling = crate_info.output) runtime_libs = get_cc_toolchain_runtime_libs(cc_toolchain, feature_configuration, crate_info.type, resolve_cc_runtime_linkage(ctx)) @@ -2119,6 +2132,17 @@ def rustc_compile( fail("No process wrapper was defined for {}".format(ctx.label)) if not rust_toolchain._bootstrapping: + # Search code-generating rustc outputs for the resolved action working + # directory. RustcMetadata has separate arguments and no object code. + checked_outputs = [output_o] if scan_msvc_archive_object else [outputs[0]] + if output_o and output_o != checked_outputs[0]: + checked_outputs.append(output_o) + args.process_wrapper_flags.add_all( + checked_outputs, + before_each = "--check-output-for-working-dir", + expand_directories = False, + ) + # Run as normal ctx.actions.run( executable = process_wrapper, diff --git a/test/integration/cc_common_link/unit/cc_common_link_test.bzl b/test/integration/cc_common_link/unit/cc_common_link_test.bzl index d38cf129ad..51eff0a10c 100644 --- a/test/integration/cc_common_link/unit/cc_common_link_test.bzl +++ b/test/integration/cc_common_link/unit/cc_common_link_test.bzl @@ -73,10 +73,6 @@ with_exec_cfg = rule( }, ) -def _outputs_object_file(action): - object_files = [output for output in action.outputs.to_list() if output.extension in ("o", "obj")] - return len(object_files) > 0 - def _use_cc_common_link_test(ctx): env = analysistest.begin(ctx) tut = analysistest.target_under_test(env) @@ -85,7 +81,25 @@ def _use_cc_common_link_test(ctx): # When --experimental_use_cc_common_link is enabled the compile+link Rustc action produces a # .o/.obj file. rustc_action = [action for action in registered_actions if action.mnemonic == "Rustc"][0] - asserts.true(env, _outputs_object_file(rustc_action), "Rustc action did not output an object file") + object_files = [ + output + for output in rustc_action.outputs.to_list() + if output.extension in ("o", "obj") + ] + asserts.true(env, len(object_files) > 0, "Rustc action did not output an object file") + object_file = object_files[0] + expected_working_dir_check_arguments = ["--check-output-for-working-dir", object_file.path] + asserts.true( + env, + any([ + rustc_action.argv[index:index + 2] == expected_working_dir_check_arguments + for index in range(len(rustc_action.argv) - 1) + ]), + "Expected adjacent working-directory check arguments {} in {}".format( + expected_working_dir_check_arguments, + rustc_action.argv, + ), + ) has_cpp_link_action = len([action for action in registered_actions if action.mnemonic == "CppLink"]) > 0 asserts.true(env, has_cpp_link_action, "Expected that the target registers a CppLink action") diff --git a/test/process_wrapper/rustc_output_format.rs b/test/process_wrapper/rustc_output_format.rs index a3b175a48a..ab729a1fca 100644 --- a/test/process_wrapper/rustc_output_format.rs +++ b/test/process_wrapper/rustc_output_format.rs @@ -1,31 +1,35 @@ #[cfg(test)] mod test { - use std::process::Command; + use std::fs; + use std::path::PathBuf; + use std::process::{Command, Output}; use std::str; use runfiles::Runfiles; - /// fake_rustc runs the fake_rustc binary under process_wrapper with the specified - /// process wrapper arguments. No arguments are passed to fake_rustc itself. - /// - fn fake_rustc( - process_wrapper_args: &[&'static str], - fake_rustc_args: &[&'static str], - should_succeed: bool, - ) -> String { + fn run_fake_rustc(process_wrapper_args: &[&str], fake_rustc_args: &[&str]) -> Output { let r = Runfiles::create().unwrap(); let fake_rustc = runfiles::rlocation!(r, env!("FAKE_RUSTC_RLOCATIONPATH")).unwrap(); let process_wrapper = runfiles::rlocation!(r, env!("PROCESS_WRAPPER_RLOCATIONPATH")).unwrap(); - let output = Command::new(process_wrapper) + Command::new(process_wrapper) .args(process_wrapper_args) .arg("--") .arg(fake_rustc) .args(fake_rustc_args) .output() - .unwrap(); + .unwrap() + } + + /// Run `fake_rustc` under `process_wrapper` and return stderr. + fn fake_rustc( + process_wrapper_args: &[&str], + fake_rustc_args: &[&str], + should_succeed: bool, + ) -> String { + let output = run_fake_rustc(process_wrapper_args, fake_rustc_args); if should_succeed { assert!( @@ -39,6 +43,29 @@ mod test { String::from_utf8(output.stderr).unwrap() } + fn artifact_path(name: &str) -> PathBuf { + PathBuf::from(std::env::var_os("TEST_TMPDIR").expect("TEST_TMPDIR is not set")).join(name) + } + + fn write_artifact(name: &str, contents: &[u8]) -> PathBuf { + let path = artifact_path(name); + fs::write(&path, contents).unwrap(); + path + } + + fn working_dir() -> PathBuf { + std::env::current_dir().expect("unable to read current working directory") + } + + fn assert_stderr_contains_path(stderr: &str, path: &str) { + assert!( + stderr.contains(&path.escape_debug().to_string()), + "missing path {}: {}", + path, + stderr, + ); + } + #[test] fn test_rustc_output_format_rendered() { let out_content = fake_rustc(&["--rustc-output-format", "rendered"], &[], true); @@ -58,11 +85,7 @@ mod test { #[test] fn test_rustc_output_format_json() { - let json_content = fake_rustc( - &["--rustc-output-format", "json"], - &[], - true, - ); + let json_content = fake_rustc(&["--rustc-output-format", "json"], &[], true); assert_eq!( json_content, concat!( @@ -87,4 +110,107 @@ Error: ProcessWrapperError("failed to process stderr: error parsing rustc output "# ); } + + #[test] + fn test_working_dir_in_binary_artifact_is_rejected() { + let working_dir = working_dir(); + let working_dir = working_dir.to_str().unwrap(); + let artifact = write_artifact("working-dir-present", working_dir.as_bytes()); + let artifact_name = artifact.to_str().unwrap(); + + let output = run_fake_rustc(&["--check-output-for-working-dir", artifact_name], &[]); + let stderr = String::from_utf8(output.stderr).unwrap(); + + assert!( + !output.status.success(), + "embedded current working directory was accepted" + ); + assert_stderr_contains_path(&stderr, working_dir); + assert_stderr_contains_path(&stderr, artifact_name); + } + + #[test] + fn test_relative_paths_in_binary_artifact_are_allowed() { + let artifact = write_artifact("relative-present", b"relative/crate/package"); + + let output = run_fake_rustc( + &["--check-output-for-working-dir", artifact.to_str().unwrap()], + &[], + ); + + assert!( + output.status.success(), + "artifact containing only relative paths was rejected: {}", + String::from_utf8_lossy(&output.stderr), + ); + } + + #[test] + fn test_working_dir_derived_out_dir_in_binary_artifact_is_rejected() { + let out_dir = working_dir().join("bazel-out/cfg/bin/package/build_script.out_dir"); + let artifact = write_artifact("out-dir-present", out_dir.to_str().unwrap().as_bytes()); + let artifact_name = artifact.to_str().unwrap(); + + let output = run_fake_rustc(&["--check-output-for-working-dir", artifact_name], &[]); + let stderr = String::from_utf8(output.stderr).unwrap(); + + assert!( + !output.status.success(), + "working-directory-derived OUT_DIR was accepted" + ); + assert_stderr_contains_path(&stderr, artifact_name); + } + + #[test] + fn test_repeated_checked_outputs_reject_embedded_working_dir() { + let working_dir = working_dir(); + let working_dir = working_dir.to_str().unwrap(); + let clean_artifact = write_artifact("repeated-clean", b"\0artifact without a path\xff"); + let rejected_artifact = write_artifact("repeated-rejected", working_dir.as_bytes()); + let rejected_name = rejected_artifact.to_str().unwrap(); + + let output = run_fake_rustc( + &[ + "--check-output-for-working-dir", + clean_artifact.to_str().unwrap(), + "--check-output-for-working-dir", + rejected_name, + ], + &[], + ); + let stderr = String::from_utf8(output.stderr).unwrap(); + + assert!( + !output.status.success(), + "embedded current working directory in second artifact was accepted" + ); + assert_stderr_contains_path(&stderr, rejected_name); + } + + #[test] + fn test_failed_child_does_not_check_missing_artifact() { + let missing_artifact = artifact_path("missing-after-failure"); + let missing_name = missing_artifact.to_str().unwrap(); + + let output = run_fake_rustc( + &["--check-output-for-working-dir", missing_name], + &["error"], + ); + let stderr = String::from_utf8(output.stderr).unwrap(); + + assert!( + !output.status.success(), + "failed child process was accepted" + ); + assert!( + stderr.contains("ERROR!"), + "missing child process failure: {}", + stderr + ); + assert!( + !stderr.contains(missing_name), + "missing artifact masked the child process failure: {}", + stderr + ); + } } diff --git a/test/unit/native_deps/native_action_inputs_test.bzl b/test/unit/native_deps/native_action_inputs_test.bzl index c7b21259bc..09cb65bd85 100644 --- a/test/unit/native_deps/native_action_inputs_test.bzl +++ b/test/unit/native_deps/native_action_inputs_test.bzl @@ -10,7 +10,12 @@ load( "rust_shared_library", "rust_static_library", ) -load("//test/unit:common.bzl", "assert_action_mnemonic") +load( + "//test/unit:common.bzl", + "assert_action_mnemonic", + "assert_list_contains_adjacent_elements", + "assert_list_contains_adjacent_elements_not", +) def _native_action_inputs_present_test_impl(ctx): env = analysistest.begin(ctx) @@ -29,6 +34,27 @@ def _native_action_inputs_present_test_impl(ctx): ), ) + if "--crate-type=staticlib" in action.argv: + archive = tut[DefaultInfo].files.to_list()[0] + checked_output = archive + if archive.extension == "lib": + object_files = [ + output + for output in action.outputs.to_list() + if output.extension in ("o", "obj") + ] + asserts.equals(env, 1, len(object_files)) + checked_output = object_files[0] + assert_list_contains_adjacent_elements_not(env, action.argv, [ + "--check-output-for-working-dir", + archive.path, + ]) + + assert_list_contains_adjacent_elements(env, action.argv, [ + "--check-output-for-working-dir", + checked_output.path, + ]) + return analysistest.end(env) def _native_action_inputs_not_present_test_impl(ctx): diff --git a/test/unit/remap_path_prefix/remap_path_prefix_test.bzl b/test/unit/remap_path_prefix/remap_path_prefix_test.bzl index 99a1409dbc..951d94b0fb 100644 --- a/test/unit/remap_path_prefix/remap_path_prefix_test.bzl +++ b/test/unit/remap_path_prefix/remap_path_prefix_test.bzl @@ -45,6 +45,40 @@ def _subst_flags_test_impl(ctx): _subst_flags_test = analysistest.make(_subst_flags_test_impl) +def _assert_working_dir_output_check(env, target): + action = [action for action in target.actions if action.mnemonic == "Rustc"][0] + artifact = target[DefaultInfo].files.to_list()[0] + assert_list_contains_adjacent_elements(env, action.argv, [ + "--check-output-for-working-dir", + artifact.path, + ]) + +def _working_dir_output_check_test_impl(ctx): + """Verify Rustc checks its output for the action working directory.""" + env = analysistest.begin(ctx) + _assert_working_dir_output_check(env, analysistest.target_under_test(env)) + return analysistest.end(env) + +_working_dir_output_check_test = analysistest.make(_working_dir_output_check_test_impl) + +def _pipelined_working_dir_output_check_test_impl(ctx): + """Verify RustcMetadata does not check for the action working directory.""" + env = analysistest.begin(ctx) + target = analysistest.target_under_test(env) + + metadata_action = [action for action in target.actions if action.mnemonic == "RustcMetadata"][0] + _assert_working_dir_output_check(env, target) + assert_argv_contains_not(env, metadata_action, "--check-output-for-working-dir") + + return analysistest.end(env) + +_pipelined_working_dir_output_check_test = analysistest.make( + _pipelined_working_dir_output_check_test_impl, + config_settings = { + str(Label("//rust/settings:pipelined_compilation")): True, + }, +) + def _coverage_remap_path_prefix_test_impl(ctx): """Verify a single `--remap-path-prefix` flag covers the bin directory. @@ -187,6 +221,25 @@ def remap_path_prefix_test_suite(name): target_under_test = ":remap_bin", ) + _working_dir_output_check_test( + name = "working_dir_output_check_lib_test", + target_under_test = ":remap_lib", + ) + + _working_dir_output_check_test( + name = "working_dir_output_check_bin_test", + target_under_test = ":remap_bin", + ) + + _pipelined_working_dir_output_check_test( + name = "pipelined_working_dir_output_check_lib_test", + target_under_test = ":remap_lib", + target_compatible_with = select({ + "@platforms//os:windows": ["@platforms//:incompatible"], + "//conditions:default": [], + }), + ) + _coverage_remap_path_prefix_test( name = "coverage_remap_path_prefix_mixed_lib_test", target_under_test = ":remap_mixed_lib", @@ -202,6 +255,9 @@ def remap_path_prefix_test_suite(name): ":remap_path_prefix_bin_test", ":subst_flags_lib_test", ":subst_flags_bin_test", + ":working_dir_output_check_lib_test", + ":working_dir_output_check_bin_test", + ":pipelined_working_dir_output_check_lib_test", ":coverage_remap_path_prefix_mixed_lib_test", ":no_coverage_remap_path_prefix_lib_test", ] diff --git a/util/process_wrapper/main.rs b/util/process_wrapper/main.rs index 3139b45a85..8d583b6c57 100644 --- a/util/process_wrapper/main.rs +++ b/util/process_wrapper/main.rs @@ -37,6 +37,8 @@ use crate::rustc::ErrorFormat; #[cfg(windows)] use crate::util::read_file_to_array; +const ARTIFACT_SCAN_BUFFER_SIZE: usize = 4 * 1024 * 1024; + #[derive(Debug)] struct ProcessWrapperError(String); @@ -198,6 +200,13 @@ fn consolidate_dependency_search_paths( e )) })?; + let file_name = entry.file_name(); + let file_name_lower = file_name.to_string_lossy().to_ascii_lowercase(); + // Concurrent rustc actions remove temporary .rcgu.o files. + if file_name_lower.ends_with(".rcgu.o") { + continue; + } + let file_type = entry.file_type().map_err(|e| { ProcessWrapperError(format!( "unable to inspect dependency search path {}: {}", @@ -209,10 +218,6 @@ fn consolidate_dependency_search_paths( continue; } - let file_name = entry.file_name(); - let file_name_lower = file_name - .to_string_lossy() - .to_ascii_lowercase(); if !seen.insert(file_name_lower) { continue; } @@ -292,6 +297,71 @@ fn process_line( rustc::process_json(line, format) } +/// Search a byte stream in linear time with Knuth–Morris–Pratt: +/// https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm +fn contains_byte_sequence(reader: &mut impl io::Read, needle: &[u8]) -> io::Result { + if needle.is_empty() { + return Ok(true); + } + + // fallback_lengths[index] is the longest proper prefix of needle[..=index] + // that is also a suffix, so mismatches do not rescan previously read bytes. + let mut fallback_lengths = vec![0; needle.len()]; + let mut fallback_length = 0; + for (index, &byte) in needle.iter().enumerate().skip(1) { + while fallback_length > 0 && byte != needle[fallback_length] { + fallback_length = fallback_lengths[fallback_length - 1]; + } + if byte == needle[fallback_length] { + fallback_length += 1; + } + fallback_lengths[index] = fallback_length; + } + + let mut buffer = vec![0; ARTIFACT_SCAN_BUFFER_SIZE]; + // Keep matched between reads so matches can cross buffer boundaries. + let mut matched = 0; + loop { + let bytes_read = reader.read(&mut buffer)?; + if bytes_read == 0 { + return Ok(false); + } + + for &byte in &buffer[..bytes_read] { + while matched > 0 && byte != needle[matched] { + matched = fallback_lengths[matched - 1]; + } + if byte == needle[matched] { + matched += 1; + if matched == needle.len() { + return Ok(true); + } + } + } + } +} + +fn check_output_for_working_dir( + output_path: &str, + working_dir: &str, +) -> Result<(), ProcessWrapperError> { + let mut output = fs::File::open(output_path) + .map_err(|error| ProcessWrapperError(format!("failed to open {output_path}: {error}")))?; + let contains_working_dir = contains_byte_sequence(&mut output, working_dir.as_bytes()) + .map_err(|error| ProcessWrapperError(format!("failed to scan {output_path}: {error}")))?; + + if contains_working_dir { + return Err(ProcessWrapperError(format!( + "compiled Rust output {output_path} embeds the absolute working directory {working_dir}. \ + Do not retain env!(\"CARGO_MANIFEST_DIR\") or env!(\"OUT_DIR\") in compiled code; \ + include_str!() and include_bytes!() may use those values only for compile-time file \ + access" + ))); + } + + Ok(()) +} + fn main() -> Result<(), ProcessWrapperError> { let opts = options().map_err(|e| ProcessWrapperError(e.to_string()))?; @@ -383,6 +453,9 @@ fn main() -> Result<(), ProcessWrapperError> { } if code == 0 { + for output_path in &opts.check_output_for_working_dir { + check_output_for_working_dir(output_path, &opts.working_dir)?; + } if let Some(tf) = opts.touch_file { OpenOptions::new() .create(true) @@ -416,6 +489,28 @@ fn main() -> Result<(), ProcessWrapperError> { mod test { use super::*; + #[test] + fn test_contains_byte_sequence_with_overlapping_prefix() { + let mut contents = io::Cursor::new(b"aaaaaab".as_slice()); + assert!(contains_byte_sequence(&mut contents, b"aaaab").unwrap()); + } + + #[test] + fn test_contains_byte_sequence_across_buffer_boundary() { + let working_dir = b"/sandbox/execroot/_main"; + let mut contents = vec![0xff; ARTIFACT_SCAN_BUFFER_SIZE - 3]; + contents.extend_from_slice(working_dir); + contents.extend_from_slice(&[0x00, 0xfe]); + let mut contents = io::Cursor::new(contents); + assert!(contains_byte_sequence(&mut contents, working_dir).unwrap()); + } + + #[test] + fn test_contains_byte_sequence_without_match() { + let mut contents = io::Cursor::new(b"/sandbox/manifests".as_slice()); + assert!(!contains_byte_sequence(&mut contents, b"/sandbox/manifest/").unwrap()); + } + fn parse_json(json_str: &str) -> Result { json_str.parse::().map_err(|e| e.to_string()) } diff --git a/util/process_wrapper/options.rs b/util/process_wrapper/options.rs index c3e5a67aee..2f24e670a6 100644 --- a/util/process_wrapper/options.rs +++ b/util/process_wrapper/options.rs @@ -30,8 +30,12 @@ pub(crate) struct Options { pub(crate) executable: String, // Contains arguments for the child process fetched from files. pub(crate) child_arguments: Vec, + // Absolute working directory used for `${pwd}` substitutions. + pub(crate) working_dir: String, // Contains environment variables for the child process fetched from files. pub(crate) child_environment: HashMap, + // Compiler outputs checked for an embedded absolute working directory. + pub(crate) check_output_for_working_dir: Vec, // If set, create the specified file after the child process successfully // terminated its execution. pub(crate) touch_file: Option, @@ -60,6 +64,7 @@ pub(crate) fn options() -> Result { let mut env_file_raw = None; let mut out_dir_raw = None; let mut arg_file_raw = None; + let mut check_output_for_working_dir_raw = None; let mut touch_file = None; let mut copy_output_raw = None; let mut stdout_file = None; @@ -92,6 +97,11 @@ pub(crate) fn options() -> Result { "File(s) containing command line arguments to pass to the child process.", &mut arg_file_raw, ); + flags.define_repeated_flag( + "--check-output-for-working-dir", + "Compiler output(s) checked for an embedded absolute working directory.", + &mut check_output_for_working_dir_raw, + ); flags.define_flag( "--touch-file", "Create this file after the child process runs successfully.", @@ -289,7 +299,9 @@ pub(crate) fn options() -> Result { Ok(Options { executable: exec_path.to_owned(), child_arguments: args.to_vec(), + working_dir: current_dir, child_environment: vars, + check_output_for_working_dir: check_output_for_working_dir_raw.unwrap_or_default(), touch_file, copy_output, stdout_file,