Skip to content

Commit 525cb88

Browse files
committed
add a test to make rustc_incremental finalize_session_directory rename fail
Use a proc macro to observe the incremental session directory and do something platform specific so that renaming the '-working' session directory during finalize_session_directory will fail. On Unix, change the permissions on the parent directory to be read-only. On Windows, open and leak a file inside the `-working` directory.
1 parent 4845f78 commit 525cb88

3 files changed

Lines changed: 211 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
poison::poison_finalize!();
2+
3+
pub fn hello() -> i32 {
4+
42
5+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//! A proc macro that sabotages the incremental compilation finalize step.
2+
//!
3+
//! When invoked, it locates the `-working` session directory inside the
4+
//! incremental compilation directory (passed via POISON_INCR_DIR) and
5+
//! makes it impossible to rename:
6+
//!
7+
//! - On Unix: removes write permission from the parent (crate) directory.
8+
//! - On Windows: creates a file inside the -working directory and leaks
9+
//! the file handle, preventing the directory from being renamed.
10+
11+
extern crate proc_macro;
12+
13+
use proc_macro::TokenStream;
14+
use std::fs;
15+
use std::path::PathBuf;
16+
17+
#[proc_macro]
18+
pub fn poison_finalize(_input: TokenStream) -> TokenStream {
19+
let incr_dir = std::env::var("POISON_INCR_DIR")
20+
.expect("POISON_INCR_DIR must be set");
21+
22+
let crate_dir = find_crate_dir(&incr_dir);
23+
let working_dir = find_working_dir(&crate_dir);
24+
25+
#[cfg(unix)]
26+
poison_unix(&crate_dir);
27+
28+
#[cfg(windows)]
29+
poison_windows(&working_dir);
30+
31+
TokenStream::new()
32+
}
33+
34+
/// Remove write permission from the crate directory.
35+
/// This causes rename() to fail with EACCES
36+
#[cfg(unix)]
37+
fn poison_unix(crate_dir: &PathBuf) {
38+
use std::os::unix::fs::PermissionsExt;
39+
let mut perms = fs::metadata(crate_dir).unwrap().permissions();
40+
perms.set_mode(0o555); // r-xr-xr-x
41+
fs::set_permissions(crate_dir, perms).unwrap();
42+
}
43+
44+
/// Create a file inside the -working directory and leak the
45+
/// handle. Windows prevents renaming a directory when any file inside it
46+
/// has an open handle. The handle stays open until the rustc process exits.
47+
#[cfg(windows)]
48+
fn poison_windows(working_dir: &PathBuf) {
49+
let poison_file = working_dir.join("_poison_handle");
50+
let f = fs::File::create(&poison_file).unwrap();
51+
// Leak the handle so it stays open for the lifetime of the rustc process.
52+
std::mem::forget(f);
53+
}
54+
55+
/// Find the crate directory for `foo` inside the incremental compilation dir.
56+
///
57+
/// The incremental directory layout is:
58+
/// {incr_dir}/{crate_name}-{stable_crate_id}/
59+
fn find_crate_dir(incr_dir: &str) -> PathBuf {
60+
let mut dirs = fs::read_dir(incr_dir)
61+
.unwrap()
62+
.filter_map(|e| {
63+
let e = e.ok()?;
64+
let name = e.file_name();
65+
let name = name.to_str()?;
66+
if e.file_type().ok()?.is_dir() && name.starts_with("foo-") {
67+
Some(e.path())
68+
} else {
69+
None
70+
}
71+
});
72+
73+
let first = dirs.next().unwrap_or_else(|| {
74+
panic!("no foo-* crate directory found in {incr_dir}")
75+
});
76+
assert!(
77+
dirs.next().is_none(),
78+
"expected exactly one foo-* crate directory in {incr_dir}, found multiple"
79+
);
80+
first
81+
}
82+
83+
/// Find the session directory ending in "-working" inside the crate directory
84+
fn find_working_dir(crate_dir: &PathBuf) -> PathBuf {
85+
for entry in fs::read_dir(crate_dir).unwrap() {
86+
let entry = entry.unwrap();
87+
let name = entry.file_name();
88+
let name = name.to_str().unwrap().to_string();
89+
if name.starts_with("s-") && name.ends_with("-working") {
90+
return entry.path();
91+
}
92+
}
93+
panic!(
94+
"no -working session directory found in {}",
95+
crate_dir.display()
96+
);
97+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//! Test that a failure to finalize the incremental compilation session directory
2+
//! (i.e., the rename from "-working" to the SVH-based name) results in a
3+
//! note, not an ICE, and that the compilation output is still produced.
4+
//!
5+
//! Strategy:
6+
//! 1. Build the `poison` proc-macro crate
7+
//! 2. Compile foo.rs with incremental compilation
8+
//! The proc macro runs mid-compilation (after prepare_session_directory
9+
//! but before finalize_session_directory) and sabotages the rename:
10+
//! - On Unix: removes write permission from the crate directory,
11+
//! so rename() fails with EACCES.
12+
//! - On Windows: creates and leaks an open file handle inside the
13+
//! -working directory, so rename() fails with ERROR_ACCESS_DENIED.
14+
//! 3. Assert that stderr contains the finalize failure messages
15+
16+
use run_make_support::rustc;
17+
use std::fs;
18+
use std::path::{Path, PathBuf};
19+
20+
/// Guard that restores permissions on the incremental directory on drop,
21+
/// to ensure cleanup is possible
22+
struct IncrDirCleanup;
23+
24+
fn main() {
25+
let _cleanup = IncrDirCleanup;
26+
27+
// Build the poison proc-macro crate
28+
rustc()
29+
.input("poison/lib.rs")
30+
.crate_name("poison")
31+
.crate_type("proc-macro")
32+
.run();
33+
34+
let poison_dylib = find_proc_macro_dylib("poison");
35+
36+
// Incremental compile with the poison macro active
37+
let out = rustc()
38+
.input("foo.rs")
39+
.crate_type("rlib")
40+
.incremental("incr")
41+
.extern_("poison", &poison_dylib)
42+
.env("POISON_INCR_DIR", "incr")
43+
.run();
44+
45+
out.assert_stderr_contains("note: did not finalize incremental compilation session directory");
46+
out.assert_stderr_contains("help: the next build will not be able to reuse work from this compilation");
47+
out.assert_stderr_not_contains("internal compiler error");
48+
}
49+
50+
impl Drop for IncrDirCleanup {
51+
fn drop(&mut self) {
52+
let incr = Path::new("incr");
53+
if !incr.exists() {
54+
return;
55+
}
56+
57+
#[cfg(unix)]
58+
restore_permissions(incr);
59+
}
60+
}
61+
62+
/// Recursively restore write permissions so rm -rf works after the chmod trick
63+
#[cfg(unix)]
64+
fn restore_permissions(path: &Path) {
65+
use std::os::unix::fs::PermissionsExt;
66+
if let Ok(entries) = fs::read_dir(path) {
67+
for entry in entries.filter_map(|e| e.ok()) {
68+
if entry.file_type().map_or(false, |ft| ft.is_dir()) {
69+
let mut perms = match fs::metadata(entry.path()) {
70+
Ok(m) => m.permissions(),
71+
Err(_) => continue,
72+
};
73+
perms.set_mode(0o755);
74+
let _ = fs::set_permissions(entry.path(), perms);
75+
}
76+
}
77+
}
78+
}
79+
80+
/// Locate the compiled proc-macro dylib by scanning the current directory.
81+
fn find_proc_macro_dylib(name: &str) -> PathBuf {
82+
let prefix = if cfg!(target_os = "windows") {
83+
""
84+
} else {
85+
"lib"
86+
};
87+
88+
let ext: &str = if cfg!(target_os = "macos") {
89+
"dylib"
90+
} else if cfg!(target_os = "windows") {
91+
"dll"
92+
} else {
93+
"so"
94+
};
95+
96+
let lib_name = format!("{prefix}{name}.{ext}");
97+
98+
for entry in fs::read_dir(".").unwrap() {
99+
let entry = entry.unwrap();
100+
let name = entry.file_name();
101+
let name = name.to_str().unwrap();
102+
if name == lib_name {
103+
return entry.path();
104+
}
105+
}
106+
107+
panic!("could not find proc-macro dylib for `{name}`");
108+
}
109+

0 commit comments

Comments
 (0)