Skip to content

Commit 4f82ad7

Browse files
committed
Reject embedded sandbox paths in Rust compiler outputs
Scan code-generating rustc outputs for the resolved ${pwd} value after successful compilation. Reject embedded CARGO_MANIFEST_DIR, OUT_DIR, and other sandbox paths while preserving compile-time include_str! and relative paths. Inspect cargo_build_script OUT_DIR through test runfiles instead of retaining compile-action paths. Assisted-by: OpenAI Codex
1 parent 0105631 commit 4f82ad7

13 files changed

Lines changed: 464 additions & 73 deletions

File tree

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
1+
fn execpath(name: &str) -> String {
2+
let path = std::env::var(name).expect("Environment variable not set");
3+
assert!(std::path::Path::new(&path).is_absolute());
4+
assert!(std::path::Path::new(&path).exists());
5+
6+
let normalized = path.replace('\\', "/");
7+
let (_, relative) = normalized
8+
.split_once("/bazel-out/")
9+
.expect("execpath does not contain bazel-out");
10+
format!("bazel-out/{}", relative)
11+
}
12+
113
fn main() {
214
println!(
315
"cargo:rustc-env=DATA_ROOTPATH={}",
416
std::env::var("DATA_ROOTPATH").expect("Environment variable not set")
517
);
618
println!(
719
"cargo:rustc-env=DATA_EXECPATH={}",
8-
std::env::var("DATA_EXECPATH").expect("Environment variable not set")
20+
execpath("DATA_EXECPATH")
921
);
1022
println!(
1123
"cargo:rustc-env=TOOL_ROOTPATH={}",
1224
std::env::var("TOOL_ROOTPATH").expect("Environment variable not set")
1325
);
1426
println!(
1527
"cargo:rustc-env=TOOL_EXECPATH={}",
16-
std::env::var("TOOL_EXECPATH").expect("Environment variable not set")
28+
execpath("TOOL_EXECPATH")
1729
);
1830
}

cargo/tests/cargo_build_script/location_expansion/test.rs

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,14 @@ pub fn test_tool_rootpath() {
1616

1717
#[test]
1818
pub fn test_execpath() {
19-
// Replace `\` to ensure paths are consistent on Windows.`
20-
let data_execpath = env!("DATA_EXECPATH").replace('\\', "/");
21-
let tool_execpath = env!("TOOL_EXECPATH").replace('\\', "/");
22-
23-
let data_path = data_execpath
24-
.split_at(
25-
data_execpath
26-
.find("/bazel-out/")
27-
.unwrap_or_else(|| panic!("Failed to parse execroot from: {}", data_execpath)),
28-
)
29-
.1;
30-
let tool_path = tool_execpath
31-
.split_at(
32-
tool_execpath
33-
.find("/bazel-out/")
34-
.unwrap_or_else(|| panic!("Failed to parse execroot from: {}", tool_execpath)),
35-
)
36-
.1;
37-
38-
let (data_cfg, data_short_path) = data_path.split_at(
39-
data_path
40-
.find("/bin/")
41-
.unwrap_or_else(|| panic!("Failed to find bin in {}", data_path))
42-
+ "/bin/".len(),
43-
);
44-
let (tool_cfg, tool_short_path) = tool_path.split_at(
45-
tool_path
46-
.find("/bin/")
47-
.unwrap_or_else(|| panic!("Failed to find bin in {}", tool_path))
48-
+ "/bin/".len(),
49-
);
19+
let data_execpath = env!("DATA_EXECPATH");
20+
let tool_execpath = env!("TOOL_EXECPATH");
21+
let (data_cfg, data_short_path) = data_execpath
22+
.split_once("/bin/")
23+
.unwrap_or_else(|| panic!("Failed to find bin in {}", data_execpath));
24+
let (tool_cfg, tool_short_path) = tool_execpath
25+
.split_once("/bin/")
26+
.unwrap_or_else(|| panic!("Failed to find bin in {}", tool_execpath));
5027

5128
assert_ne!(
5229
data_cfg, tool_cfg,

cargo/tests/cargo_build_script/nondeterministic_out_dir/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ cargo_build_script(
1010
rust_test(
1111
name = "test",
1212
srcs = ["test.rs"],
13+
data = [":build_script"],
1314
edition = "2021",
15+
env = {"BUILD_SCRIPT_OUT_DIR": "$(rootpath :build_script)"},
1416
deps = [":build_script"],
1517
)

cargo/tests/cargo_build_script/nondeterministic_out_dir/test.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,23 @@
22
//! If the runner failed to strip config.log / *.d / *.pc files, the TreeArtifact hash
33
//! would change on every run, causing unnecessary rebuilds for all downstream crates.
44
5+
use std::path::PathBuf;
6+
57
const OUTPUT: &str = include_str!(concat!(env!("OUT_DIR"), "/output.txt"));
68

9+
fn build_script_out_dir() -> PathBuf {
10+
PathBuf::from(
11+
std::env::var_os("BUILD_SCRIPT_OUT_DIR").expect("BUILD_SCRIPT_OUT_DIR is not set"),
12+
)
13+
}
14+
715
#[test]
816
fn legitimate_output_survives_nondeterministic_file_removal() {
917
assert_eq!(OUTPUT, "legitimate output");
18+
assert_eq!(
19+
std::fs::read_to_string(build_script_out_dir().join("output.txt")).unwrap(),
20+
OUTPUT,
21+
);
1022
}
1123

1224
// Verify that volatile files written by the build script are absent from the
@@ -17,59 +29,59 @@ fn legitimate_output_survives_nondeterministic_file_removal() {
1729
#[test]
1830
fn config_log_removed() {
1931
assert!(
20-
!std::path::Path::new(concat!(env!("OUT_DIR"), "/config.log")).exists(),
32+
!build_script_out_dir().join("config.log").exists(),
2133
"config.log should have been removed from OUT_DIR"
2234
);
2335
}
2436

2537
#[test]
2638
fn config_status_removed() {
2739
assert!(
28-
!std::path::Path::new(concat!(env!("OUT_DIR"), "/config.status")).exists(),
40+
!build_script_out_dir().join("config.status").exists(),
2941
"config.status should have been removed from OUT_DIR"
3042
);
3143
}
3244

3345
#[test]
3446
fn makefile_removed() {
3547
assert!(
36-
!std::path::Path::new(concat!(env!("OUT_DIR"), "/Makefile")).exists(),
48+
!build_script_out_dir().join("Makefile").exists(),
3749
"Makefile should have been removed from OUT_DIR"
3850
);
3951
}
4052

4153
#[test]
4254
fn makefile_config_removed() {
4355
assert!(
44-
!std::path::Path::new(concat!(env!("OUT_DIR"), "/Makefile.config")).exists(),
56+
!build_script_out_dir().join("Makefile.config").exists(),
4557
"Makefile.config should have been removed from OUT_DIR"
4658
);
4759
}
4860

4961
#[test]
5062
fn config_cache_removed() {
5163
assert!(
52-
!std::path::Path::new(concat!(env!("OUT_DIR"), "/config.cache")).exists(),
64+
!build_script_out_dir().join("config.cache").exists(),
5365
"config.cache should have been removed from OUT_DIR"
5466
);
5567
}
5668

5769
#[test]
5870
fn dot_d_files_removed() {
5971
assert!(
60-
!std::path::Path::new(concat!(env!("OUT_DIR"), "/foo.d")).exists(),
72+
!build_script_out_dir().join("foo.d").exists(),
6173
"foo.d should have been removed from OUT_DIR"
6274
);
6375
assert!(
64-
!std::path::Path::new(concat!(env!("OUT_DIR"), "/baz.d")).exists(),
76+
!build_script_out_dir().join("baz.d").exists(),
6577
"baz.d should have been removed from OUT_DIR"
6678
);
6779
}
6880

6981
#[test]
7082
fn dot_pc_file_removed() {
7183
assert!(
72-
!std::path::Path::new(concat!(env!("OUT_DIR"), "/foo.pc")).exists(),
84+
!build_script_out_dir().join("foo.pc").exists(),
7385
"foo.pc should have been removed from OUT_DIR"
7486
);
7587
}

cargo/tests/cargo_build_script/tools_exec/build.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@ fn main() {
3636
test_encoded_rustflags();
3737
test_toolchain_var();
3838

39-
// Pass the TOOL_PATH along to the rust_test so we can assert on it.
40-
println!(
41-
"cargo:rustc-env=TOOL_PATH={}",
42-
std::env::var("TOOL").unwrap()
43-
);
39+
// Pass the execution-root-relative tool path to the rust_test.
40+
let tool_path = std::env::var("TOOL").unwrap().replace('\\', "/");
41+
let (_, relative_tool_path) = tool_path
42+
.split_once("/bazel-out/")
43+
.expect("tool path does not contain bazel-out");
44+
println!("cargo:rustc-env=TOOL_PATH=bazel-out/{}", relative_tool_path);
4445

4546
// Assert that the cc and rust toolchain env vars existed and were executable.
4647
// We don't assert what happens when they're executed (in particular, we don't check for a

cargo/tests/unit/transitive_link_search_paths/transitive_link_search_paths_test.bzl

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
44
load("//cargo:defs.bzl", "cargo_build_script")
55
load("//rust:defs.bzl", "rust_binary", "rust_common", "rust_library", "rust_proc_macro")
6-
load("//test/unit:common.bzl", "assert_action_mnemonic", "assert_list_contains")
6+
load(
7+
"//test/unit:common.bzl",
8+
"assert_action_mnemonic",
9+
"assert_list_contains",
10+
"assert_list_contains_adjacent_elements",
11+
"assert_list_contains_adjacent_elements_not",
12+
)
713

814
def _transitive_link_search_paths_test_impl(ctx):
915
env = analysistest.begin(ctx)
@@ -15,6 +21,28 @@ def _transitive_link_search_paths_test_impl(ctx):
1521
# of the dep of the proc_macro.
1622
asserts.equals(env, link_search_path_basenames, ["dep_build_script.linksearchpaths"])
1723

24+
action = tut.actions[0]
25+
assert_action_mnemonic(env, action, "Rustc")
26+
archive = tut[DefaultInfo].files.to_list()[0]
27+
checked_output = archive
28+
if any([arg.endswith("-windows-msvc") for arg in action.argv]):
29+
object_files = [
30+
output
31+
for output in action.outputs.to_list()
32+
if output.extension in ("o", "obj")
33+
]
34+
asserts.equals(env, 1, len(object_files))
35+
checked_output = object_files[0]
36+
assert_list_contains_adjacent_elements_not(env, action.argv, [
37+
"--check-output-for-working-dir",
38+
archive.path,
39+
])
40+
41+
assert_list_contains_adjacent_elements(env, action.argv, [
42+
"--check-output-for-working-dir",
43+
checked_output.path,
44+
])
45+
1846
return analysistest.end(env)
1947

2048
transitive_link_search_paths_test = analysistest.make(_transitive_link_search_paths_test_impl)

rust/private/rustc.bzl

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1879,11 +1879,24 @@ def rustc_compile(
18791879
distributed_thin_lto and crate_info.type == "bin"
18801880
)
18811881

1882-
# output_o is consumed by cc_common.link or exported for distributed
1883-
# ThinLTO. crate_info.output.basename keeps output_o distinct for targets
1884-
# that share crate_info.name, including rust_test targets for the same crate.
1882+
scan_msvc_archive_object = (
1883+
rust_toolchain.target_abi == "msvc" and
1884+
(
1885+
crate_info.type == "staticlib" or
1886+
(
1887+
crate_info.type in ("lib", "rlib") and
1888+
build_info != None and
1889+
build_info.out_dir != None
1890+
)
1891+
)
1892+
)
1893+
1894+
# output_o is consumed by cc_common.link, exported for distributed ThinLTO,
1895+
# or scanned instead of an MSVC archive containing native-object paths.
1896+
# crate_info.output.basename keeps output_o distinct for targets that share
1897+
# crate_info.name, including rust_test targets for the same crate.
18851898
output_o = None
1886-
if use_cc_common_link or distributed_thin_lto:
1899+
if use_cc_common_link or distributed_thin_lto or scan_msvc_archive_object:
18871900
output_o = ctx.actions.declare_file(crate_info.output.basename + ".o", sibling = crate_info.output)
18881901

18891902
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(
21192132
fail("No process wrapper was defined for {}".format(ctx.label))
21202133

21212134
if not rust_toolchain._bootstrapping:
2135+
# Search code-generating rustc outputs for the resolved action working
2136+
# directory. RustcMetadata has separate arguments and no object code.
2137+
checked_outputs = [output_o] if scan_msvc_archive_object else [outputs[0]]
2138+
if output_o and output_o != checked_outputs[0]:
2139+
checked_outputs.append(output_o)
2140+
args.process_wrapper_flags.add_all(
2141+
checked_outputs,
2142+
before_each = "--check-output-for-working-dir",
2143+
expand_directories = False,
2144+
)
2145+
21222146
# Run as normal
21232147
ctx.actions.run(
21242148
executable = process_wrapper,

test/integration/cc_common_link/unit/cc_common_link_test.bzl

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,6 @@ with_exec_cfg = rule(
7373
},
7474
)
7575

76-
def _outputs_object_file(action):
77-
object_files = [output for output in action.outputs.to_list() if output.extension in ("o", "obj")]
78-
return len(object_files) > 0
79-
8076
def _use_cc_common_link_test(ctx):
8177
env = analysistest.begin(ctx)
8278
tut = analysistest.target_under_test(env)
@@ -85,7 +81,25 @@ def _use_cc_common_link_test(ctx):
8581
# When --experimental_use_cc_common_link is enabled the compile+link Rustc action produces a
8682
# .o/.obj file.
8783
rustc_action = [action for action in registered_actions if action.mnemonic == "Rustc"][0]
88-
asserts.true(env, _outputs_object_file(rustc_action), "Rustc action did not output an object file")
84+
object_files = [
85+
output
86+
for output in rustc_action.outputs.to_list()
87+
if output.extension in ("o", "obj")
88+
]
89+
asserts.true(env, len(object_files) > 0, "Rustc action did not output an object file")
90+
object_file = object_files[0]
91+
expected_working_dir_check_arguments = ["--check-output-for-working-dir", object_file.path]
92+
asserts.true(
93+
env,
94+
any([
95+
rustc_action.argv[index:index + 2] == expected_working_dir_check_arguments
96+
for index in range(len(rustc_action.argv) - 1)
97+
]),
98+
"Expected adjacent working-directory check arguments {} in {}".format(
99+
expected_working_dir_check_arguments,
100+
rustc_action.argv,
101+
),
102+
)
89103

90104
has_cpp_link_action = len([action for action in registered_actions if action.mnemonic == "CppLink"]) > 0
91105
asserts.true(env, has_cpp_link_action, "Expected that the target registers a CppLink action")

0 commit comments

Comments
 (0)