1use std::{
11 ffi::OsString,
12 fs::{self, OpenOptions},
13 io::{Cursor, Write},
14 path::{Component, Path, PathBuf},
15 process::Command,
16 thread,
17 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
18};
19
20use ar_archive_writer::{
21 ArchiveKind as WritableArchiveKind, DEFAULT_OBJECT_READER, NewArchiveMember,
22 write_archive_to_stream,
23};
24use object::read::archive::{ArchiveFile, ArchiveKind as ReadableArchiveKind};
25use serde::{Deserialize, Serialize};
26use sha2::{Digest, Sha256};
27use supercov_contracts::{
28 RUST_LIBTEST_COMPANION_BUNDLE_SCHEMA_VERSION, RUST_LIBTEST_EVENT_PROTOCOL_VERSION,
29 RustCompilerIdentity,
30};
31
32use crate::{
33 rust_compiler_selection::{
34 SelectedRustCompilerCompanion, probe_rustc_identity, select_rust_compiler_companion,
35 },
36 rust_libtest_events::rust_libtest_event_runtime_source,
37};
38
39const SOURCE_IDENTITY_FILE: &str = "supercov-libtest-source.json";
40const BUILD_LOCK_TIMEOUT: Duration = Duration::from_secs(300);
41
42const LIB_ANCHOR: &str = "mod console;";
43const LIB_REPLACEMENT: &str = "mod console;\nmod supercov_events;";
44const CONSOLE_ANCHOR: &str = "fn on_test_event(\n event: &TestEvent,\n st: &mut ConsoleTestState,\n out: &mut dyn OutputFormatter,\n) -> io::Result<()> {\n match (*event).clone() {";
45const CONSOLE_REPLACEMENT: &str = "fn on_test_event(\n event: &TestEvent,\n st: &mut ConsoleTestState,\n out: &mut dyn OutputFormatter,\n) -> io::Result<()> {\n crate::supercov_events::emit(event)?;\n match (*event).clone() {";
46const LISTING_ANCHOR: &str =
47 " out.write_discovery_start()?;\n for test in filter_tests(opts, tests).into_iter() {";
48const LISTING_REPLACEMENT: &str = " out.write_discovery_start()?;\n let tests_len = tests.len();\n let filtered_tests = filter_tests(opts, tests);\n crate::supercov_events::emit_listing(tests_len - filtered_tests.len(), filtered_tests.len())?;\n for test in filtered_tests {";
49const IN_PROCESS_ANCHOR: &str = " // Buffer for capturing standard I/O\n let data =";
50const IN_PROCESS_REPLACEMENT: &str = " let _supercov_context = crate::supercov_events::enter_test(desc.name.as_slice())\n .expect(\"Supercov could not enter the exact libtest context\");\n\n // Buffer for capturing standard I/O\n let data =";
51const SPAWNED_PROCESS_ANCHOR: &str = "fn run_test_in_spawned_subprocess(desc: TestDesc, runnable_test: RunnableTest) -> ! {\n let builtin_panic_hook";
52const SPAWNED_PROCESS_REPLACEMENT: &str = "fn run_test_in_spawned_subprocess(desc: TestDesc, runnable_test: RunnableTest) -> ! {\n let _supercov_context = crate::supercov_events::enter_test(desc.name.as_slice())\n .expect(\"Supercov could not enter the exact spawned libtest context\");\n let builtin_panic_hook";
53const BENCH_ANCHOR: &str = " Runnable::Bench(runnable_bench) => {\n // Benchmarks aren't expected to panic, so we run them all in-process.\n runnable_bench.run";
54const BENCH_REPLACEMENT: &str = " Runnable::Bench(runnable_bench) => {\n // Benchmarks aren't expected to panic, so we run them all in-process.\n let _supercov_context = crate::supercov_events::enter_test(desc.name.as_slice())\n .expect(\"Supercov could not enter the exact benchmark context\");\n runnable_bench.run";
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase", deny_unknown_fields)]
58pub struct RustLibtestCompanionSourceIdentity {
59 pub event_protocol_version: u32,
60 pub rustc_commit_hash: String,
61 pub rustc_release: String,
62 pub host_triple: String,
63 pub original_source_sha256: String,
64 pub event_runtime_sha256: String,
65 pub patched_source_sha256: String,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct RustLibtestCompanionBuildPlan {
70 pub source: PathBuf,
71 pub output: PathBuf,
72 pub arguments: Vec<OsString>,
73 pub rustc_bootstrap: OsString,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase", deny_unknown_fields)]
78pub struct RustLibtestCompanionBundle {
79 pub schema_version: u32,
80 pub event_protocol_version: u32,
81 pub compiler_companion_build_id: String,
82 pub rustc_commit_hash: String,
83 pub host_triple: String,
84 pub original_source_sha256: String,
85 pub event_runtime_sha256: String,
86 pub patched_source_sha256: String,
87 pub artifact_file: String,
88 pub artifact_sha256: String,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct SelectedRustLibtestCompanion {
93 pub bundle_path: PathBuf,
94 pub artifact_path: PathBuf,
95 pub bundle: RustLibtestCompanionBundle,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum RustLibtestCompanionError {
100 Io {
101 path: PathBuf,
102 reason: String,
103 },
104 UnsafeSource(PathBuf),
105 NonUtf8Path(PathBuf),
106 UnrecognizedSource {
107 path: PathBuf,
108 anchor: &'static str,
109 },
110 DependencyMetadata {
111 directory: PathBuf,
112 crate_name: &'static str,
113 count: usize,
114 },
115 InvalidBundle {
116 path: PathBuf,
117 reason: String,
118 },
119 InvalidArchive(String),
120 BundleMismatch(String),
121 BuildFailed {
122 program: PathBuf,
123 status: Option<i32>,
124 stdout: String,
125 stderr: String,
126 },
127 LockTimeout(PathBuf),
128}
129
130impl std::fmt::Display for RustLibtestCompanionError {
131 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 match self {
133 Self::Io { path, reason } => write!(formatter, "{}: {reason}", path.display()),
134 Self::UnsafeSource(path) => write!(
135 formatter,
136 "exact libtest source contains a symlink or special file: {}",
137 path.display()
138 ),
139 Self::NonUtf8Path(path) => write!(
140 formatter,
141 "exact libtest source contains a non-UTF-8 path: {}",
142 path.display()
143 ),
144 Self::UnrecognizedSource { path, anchor } => write!(
145 formatter,
146 "selected toolchain libtest source {} does not contain exactly one {anchor} patch anchor",
147 path.display()
148 ),
149 Self::DependencyMetadata {
150 directory,
151 crate_name,
152 count,
153 } => write!(
154 formatter,
155 "expected exactly one full {crate_name} metadata file in {}, found {count}",
156 directory.display()
157 ),
158 Self::InvalidBundle { path, reason } => write!(
159 formatter,
160 "invalid libtest companion bundle {}: {reason}",
161 path.display()
162 ),
163 Self::InvalidArchive(reason) => {
164 write!(formatter, "invalid libtest companion rlib: {reason}")
165 }
166 Self::BundleMismatch(reason) => {
167 write!(formatter, "libtest companion bundle mismatch: {reason}")
168 }
169 Self::BuildFailed {
170 program,
171 status,
172 stdout,
173 stderr,
174 } => write!(
175 formatter,
176 "{} failed with status {status:?}: {stderr}{stdout}",
177 program.display()
178 ),
179 Self::LockTimeout(path) => write!(
180 formatter,
181 "timed out waiting for the exact libtest builder lock {}",
182 path.display()
183 ),
184 }
185 }
186}
187
188impl std::error::Error for RustLibtestCompanionError {}
189
190fn io_error(path: &Path, error: impl std::fmt::Display) -> RustLibtestCompanionError {
191 RustLibtestCompanionError::Io {
192 path: path.to_path_buf(),
193 reason: error.to_string(),
194 }
195}
196
197#[cfg(unix)]
198fn sync_directory(path: &Path) -> Result<(), RustLibtestCompanionError> {
199 let directory = OpenOptions::new()
200 .read(true)
201 .open(path)
202 .map_err(|error| io_error(path, error))?;
203 directory.sync_all().map_err(|error| io_error(path, error))
204}
205
206#[cfg(not(unix))]
207fn sync_directory(_path: &Path) -> Result<(), RustLibtestCompanionError> {
208 Ok(())
209}
210
211fn regular_directory(path: &Path) -> Result<PathBuf, RustLibtestCompanionError> {
212 let canonical = fs::canonicalize(path).map_err(|error| io_error(path, error))?;
213 let metadata = fs::symlink_metadata(&canonical).map_err(|error| io_error(&canonical, error))?;
214 if !metadata.file_type().is_dir() {
215 return Err(RustLibtestCompanionError::UnsafeSource(canonical));
216 }
217 Ok(canonical)
218}
219
220fn safe_component(path: &Path) -> Result<String, RustLibtestCompanionError> {
221 if path
222 .components()
223 .any(|component| !matches!(component, Component::Normal(_)))
224 {
225 return Err(RustLibtestCompanionError::UnsafeSource(path.to_path_buf()));
226 }
227 path.to_str()
228 .map(|value| value.replace('\\', "/"))
229 .ok_or_else(|| RustLibtestCompanionError::NonUtf8Path(path.to_path_buf()))
230}
231
232fn collect_tree(
233 root: &Path,
234 relative: &Path,
235 entries: &mut Vec<(String, PathBuf, bool)>,
236) -> Result<(), RustLibtestCompanionError> {
237 let directory = root.join(relative);
238 let mut children = fs::read_dir(&directory)
239 .map_err(|error| io_error(&directory, error))?
240 .collect::<Result<Vec<_>, _>>()
241 .map_err(|error| io_error(&directory, error))?;
242 children.sort_by_key(std::fs::DirEntry::file_name);
243 for child in children {
244 let name = child.file_name();
245 let child_relative = relative.join(name);
246 let display = safe_component(&child_relative)?;
247 let file_type = child
248 .file_type()
249 .map_err(|error| io_error(&child.path(), error))?;
250 if file_type.is_dir() {
251 entries.push((display, child.path(), true));
252 collect_tree(root, &child_relative, entries)?;
253 } else if file_type.is_file() {
254 entries.push((display, child.path(), false));
255 } else {
256 return Err(RustLibtestCompanionError::UnsafeSource(child.path()));
257 }
258 }
259 Ok(())
260}
261
262fn source_tree_digest(root: &Path) -> Result<String, RustLibtestCompanionError> {
263 source_tree_digest_excluding(root, None)
264}
265
266fn source_tree_digest_excluding(
267 root: &Path,
268 excluded_root_file: Option<&str>,
269) -> Result<String, RustLibtestCompanionError> {
270 let mut entries = Vec::new();
271 collect_tree(root, Path::new(""), &mut entries)?;
272 let mut digest = Sha256::new();
273 for (relative, path, directory) in entries {
274 if excluded_root_file.is_some_and(|excluded| relative == excluded) {
275 continue;
276 }
277 digest.update([u8::from(directory)]);
278 digest.update((relative.len() as u64).to_le_bytes());
279 digest.update(relative.as_bytes());
280 if !directory {
281 let bytes = fs::read(&path).map_err(|error| io_error(&path, error))?;
282 digest.update((bytes.len() as u64).to_le_bytes());
283 digest.update(bytes);
284 }
285 }
286 Ok(format!("{:x}", digest.finalize()))
287}
288
289fn copy_regular_tree(source: &Path, destination: &Path) -> Result<(), RustLibtestCompanionError> {
290 let mut entries = Vec::new();
291 collect_tree(source, Path::new(""), &mut entries)?;
292 for (relative, path, directory) in entries {
293 let destination_path = destination.join(&relative);
294 if directory {
295 fs::create_dir(&destination_path)
296 .map_err(|error| io_error(&destination_path, error))?;
297 } else {
298 let bytes = fs::read(&path).map_err(|error| io_error(&path, error))?;
299 let mut options = OpenOptions::new();
300 options.write(true).create_new(true);
301 #[cfg(unix)]
302 {
303 use std::os::unix::fs::OpenOptionsExt as _;
304 options.mode(0o600);
305 }
306 let mut file = options
307 .open(&destination_path)
308 .map_err(|error| io_error(&destination_path, error))?;
309 file.write_all(&bytes)
310 .and_then(|()| file.sync_all())
311 .map_err(|error| io_error(&destination_path, error))?;
312 }
313 }
314 Ok(())
315}
316
317fn replace_once(
318 path: &Path,
319 anchor: &'static str,
320 replacement: &str,
321) -> Result<(), RustLibtestCompanionError> {
322 let source = fs::read_to_string(path).map_err(|error| io_error(path, error))?;
323 if source.matches(anchor).count() != 1 {
324 return Err(RustLibtestCompanionError::UnrecognizedSource {
325 path: path.into(),
326 anchor,
327 });
328 }
329 let bytes = source.replacen(anchor, replacement, 1).into_bytes();
330 let mut file = OpenOptions::new()
331 .write(true)
332 .truncate(true)
333 .open(path)
334 .map_err(|error| io_error(path, error))?;
335 file.write_all(&bytes)
336 .and_then(|()| file.sync_all())
337 .map_err(|error| io_error(path, error))
338}
339
340struct RemoveDirectoryOnDrop(Option<PathBuf>);
341
342impl Drop for RemoveDirectoryOnDrop {
343 fn drop(&mut self) {
344 if let Some(path) = self.0.take() {
345 let _ = fs::remove_dir_all(path);
346 }
347 }
348}
349
350struct RemoveFileOnDrop(Option<PathBuf>);
351
352impl Drop for RemoveFileOnDrop {
353 fn drop(&mut self) {
354 if let Some(path) = self.0.take() {
355 let _ = fs::remove_file(path);
356 }
357 }
358}
359
360fn acquire_kernel_lock(path: &Path) -> Result<fs::File, RustLibtestCompanionError> {
361 if let Ok(metadata) = fs::symlink_metadata(path)
362 && !metadata.file_type().is_file()
363 {
364 return Err(RustLibtestCompanionError::UnsafeSource(path.to_path_buf()));
365 }
366 let started = Instant::now();
367 loop {
368 let mut options = OpenOptions::new();
369 options.read(true).write(true).create(true);
370 #[cfg(unix)]
371 {
372 use std::os::unix::fs::OpenOptionsExt as _;
373 #[cfg(target_os = "linux")]
374 const O_NOFOLLOW: i32 = 0x2_0000;
375 #[cfg(target_os = "macos")]
376 const O_NOFOLLOW: i32 = 0x100;
377 #[cfg(any(target_os = "linux", target_os = "macos"))]
378 options.custom_flags(O_NOFOLLOW);
379 options.mode(0o600);
380 }
381 let mut file = options.open(path).map_err(|error| io_error(path, error))?;
382 if !file
383 .metadata()
384 .map_err(|error| io_error(path, error))?
385 .file_type()
386 .is_file()
387 {
388 return Err(RustLibtestCompanionError::UnsafeSource(path.to_path_buf()));
389 }
390 match file.try_lock() {
391 Ok(()) => {
392 file.set_len(0)
393 .and_then(|()| writeln!(file, "{}", std::process::id()))
394 .and_then(|()| file.sync_all())
395 .map_err(|error| io_error(path, error))?;
396 return Ok(file);
397 }
398 Err(fs::TryLockError::WouldBlock) => {
399 if started.elapsed() >= BUILD_LOCK_TIMEOUT {
400 return Err(RustLibtestCompanionError::LockTimeout(path.to_path_buf()));
401 }
402 thread::sleep(Duration::from_millis(25));
403 }
404 Err(fs::TryLockError::Error(error)) => return Err(io_error(path, error)),
405 }
406 }
407}
408
409#[cfg(unix)]
410fn inherit_lock_through_exec(command: &mut Command, lock: &fs::File) {
411 use std::os::{fd::AsRawFd as _, unix::process::CommandExt as _};
412
413 let lock_fd = lock.as_raw_fd();
414 unsafe {
420 command.pre_exec(move || {
421 if libc::dup(lock_fd) < 0 {
422 Err(std::io::Error::last_os_error())
423 } else {
424 Ok(())
425 }
426 });
427 }
428}
429
430#[cfg(not(unix))]
431fn inherit_lock_through_exec(_command: &mut Command, _lock: &fs::File) {
432 }
437
438fn remove_owned_path(path: &Path, expect_directory: bool) -> Result<(), RustLibtestCompanionError> {
439 let metadata = match fs::symlink_metadata(path) {
440 Ok(metadata) => metadata,
441 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
442 Err(error) => return Err(io_error(path, error)),
443 };
444 if expect_directory && metadata.file_type().is_dir() {
445 fs::remove_dir_all(path).map_err(|error| io_error(path, error))
446 } else if !expect_directory && metadata.file_type().is_file() {
447 fs::remove_file(path).map_err(|error| io_error(path, error))
448 } else {
449 Err(RustLibtestCompanionError::UnsafeSource(path.to_path_buf()))
450 }
451}
452
453fn remove_owned_partials(
454 directory: &Path,
455 prefix: &str,
456 expect_directory: bool,
457) -> Result<(), RustLibtestCompanionError> {
458 for entry in fs::read_dir(directory).map_err(|error| io_error(directory, error))? {
459 let entry = entry.map_err(|error| io_error(directory, error))?;
460 let name = entry.file_name();
461 let Some(name) = name.to_str() else {
462 continue;
463 };
464 if name.starts_with(prefix) && name.ends_with(".partial") {
465 remove_owned_path(&entry.path(), expect_directory)?;
466 }
467 }
468 Ok(())
469}
470
471fn validate_prepared_libtest_source(
472 source_root: &Path,
473 destination: &Path,
474 compiler: &RustCompilerIdentity,
475) -> Result<RustLibtestCompanionSourceIdentity, RustLibtestCompanionError> {
476 let destination = regular_directory(destination)?;
477 let identity_path = destination.join(SOURCE_IDENTITY_FILE);
478 let identity: RustLibtestCompanionSourceIdentity =
479 serde_json::from_slice(&read_regular_file(&identity_path)?).map_err(|error| {
480 RustLibtestCompanionError::InvalidBundle {
481 path: identity_path.clone(),
482 reason: error.to_string(),
483 }
484 })?;
485 let runtime_sha256 = format!(
486 "{:x}",
487 Sha256::digest(rust_libtest_event_runtime_source().as_bytes())
488 );
489 if identity.event_protocol_version != RUST_LIBTEST_EVENT_PROTOCOL_VERSION
490 || identity.rustc_commit_hash != compiler.rustc_commit_hash
491 || identity.rustc_release != compiler.rustc_release
492 || identity.host_triple != compiler.host_triple
493 || identity.event_runtime_sha256 != runtime_sha256
494 || [
495 identity.original_source_sha256.as_str(),
496 identity.event_runtime_sha256.as_str(),
497 identity.patched_source_sha256.as_str(),
498 ]
499 .iter()
500 .any(|value| !canonical_lower_sha256(value))
501 {
502 return Err(RustLibtestCompanionError::BundleMismatch(
503 "prepared libtest source identity differs from the exact compiler/runtime".into(),
504 ));
505 }
506 if source_tree_digest(source_root)? != identity.original_source_sha256 {
507 return Err(RustLibtestCompanionError::BundleMismatch(
508 "exact toolchain libtest source changed after preparation".into(),
509 ));
510 }
511 if source_tree_digest_excluding(&destination, Some(SOURCE_IDENTITY_FILE))?
512 != identity.patched_source_sha256
513 {
514 return Err(RustLibtestCompanionError::BundleMismatch(
515 "prepared libtest source tree digest differs".into(),
516 ));
517 }
518 Ok(identity)
519}
520
521pub fn prepare_exact_libtest_source(
525 source_root: &Path,
526 destination: &Path,
527 compiler: &RustCompilerIdentity,
528) -> Result<RustLibtestCompanionSourceIdentity, RustLibtestCompanionError> {
529 let source_root = regular_directory(source_root)?;
530 let parent = destination
531 .parent()
532 .ok_or_else(|| RustLibtestCompanionError::UnsafeSource(destination.to_path_buf()))?;
533 let parent = regular_directory(parent)?;
534 let name = destination
535 .file_name()
536 .and_then(|name| name.to_str())
537 .filter(|name| !name.is_empty() && *name != "." && *name != "..")
538 .ok_or_else(|| RustLibtestCompanionError::UnsafeSource(destination.to_path_buf()))?;
539 let destination = parent.join(name);
540 let lock_path = parent.join(format!(".{name}.lock"));
541 let _lock = acquire_kernel_lock(&lock_path)?;
542 remove_owned_partials(&parent, &format!(".{name}."), true)?;
543 match fs::symlink_metadata(&destination) {
544 Ok(metadata) if metadata.file_type().is_dir() => {
545 return validate_prepared_libtest_source(&source_root, &destination, compiler);
546 }
547 Ok(_) => return Err(RustLibtestCompanionError::UnsafeSource(destination)),
548 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
549 Err(error) => return Err(io_error(&destination, error)),
550 }
551 let nonce = SystemTime::now()
552 .duration_since(UNIX_EPOCH)
553 .map_err(|error| io_error(&destination, error))?
554 .as_nanos();
555 let partial = parent.join(format!(".{name}.{}-{nonce}.partial", std::process::id()));
556 fs::create_dir(&partial).map_err(|error| io_error(&partial, error))?;
557 let mut cleanup = RemoveDirectoryOnDrop(Some(partial.clone()));
558
559 let original_source_sha256 = source_tree_digest(&source_root)?;
560 copy_regular_tree(&source_root, &partial)?;
561 if source_tree_digest(&source_root)? != original_source_sha256 {
562 return Err(RustLibtestCompanionError::BundleMismatch(
563 "exact toolchain libtest source changed while it was copied".into(),
564 ));
565 }
566 let lib = partial.join("src/lib.rs");
567 let console = partial.join("src/console.rs");
568 replace_once(&lib, LIB_ANCHOR, LIB_REPLACEMENT)?;
569 replace_once(&lib, IN_PROCESS_ANCHOR, IN_PROCESS_REPLACEMENT)?;
570 replace_once(&lib, SPAWNED_PROCESS_ANCHOR, SPAWNED_PROCESS_REPLACEMENT)?;
571 replace_once(&lib, BENCH_ANCHOR, BENCH_REPLACEMENT)?;
572 replace_once(&console, CONSOLE_ANCHOR, CONSOLE_REPLACEMENT)?;
573 replace_once(&console, LISTING_ANCHOR, LISTING_REPLACEMENT)?;
574 let event_runtime = rust_libtest_event_runtime_source();
575 let event_path = partial.join("src/supercov_events.rs");
576 let mut options = OpenOptions::new();
577 options.write(true).create_new(true);
578 #[cfg(unix)]
579 {
580 use std::os::unix::fs::OpenOptionsExt as _;
581 options.mode(0o600);
582 }
583 let mut event_file = options
584 .open(&event_path)
585 .map_err(|error| io_error(&event_path, error))?;
586 event_file
587 .write_all(event_runtime.as_bytes())
588 .and_then(|()| event_file.sync_all())
589 .map_err(|error| io_error(&event_path, error))?;
590 drop(event_file);
591
592 let identity = RustLibtestCompanionSourceIdentity {
593 event_protocol_version: RUST_LIBTEST_EVENT_PROTOCOL_VERSION,
594 rustc_commit_hash: compiler.rustc_commit_hash.clone(),
595 rustc_release: compiler.rustc_release.clone(),
596 host_triple: compiler.host_triple.clone(),
597 original_source_sha256,
598 event_runtime_sha256: format!("{:x}", Sha256::digest(event_runtime.as_bytes())),
599 patched_source_sha256: source_tree_digest(&partial)?,
600 };
601 let identity_path = partial.join(SOURCE_IDENTITY_FILE);
602 let mut identity_bytes =
603 serde_json::to_vec_pretty(&identity).map_err(|error| io_error(&identity_path, error))?;
604 identity_bytes.push(b'\n');
605 let mut identity_file = OpenOptions::new()
606 .write(true)
607 .create_new(true)
608 .open(&identity_path)
609 .map_err(|error| io_error(&identity_path, error))?;
610 identity_file
611 .write_all(&identity_bytes)
612 .and_then(|()| identity_file.sync_all())
613 .map_err(|error| io_error(&identity_path, error))?;
614 drop(identity_file);
618 fs::rename(&partial, &destination).map_err(|error| io_error(&destination, error))?;
619 sync_directory(&parent)?;
620 cleanup.0 = None;
621 Ok(identity)
622}
623
624fn one_metadata(
625 directory: &Path,
626 crate_name: &'static str,
627) -> Result<PathBuf, RustLibtestCompanionError> {
628 let prefix = format!("lib{crate_name}-");
629 let mut matches = fs::read_dir(directory)
630 .map_err(|error| io_error(directory, error))?
631 .collect::<Result<Vec<_>, _>>()
632 .map_err(|error| io_error(directory, error))?
633 .into_iter()
634 .filter(|entry| {
635 let metadata = entry
636 .file_name()
637 .to_str()
638 .is_some_and(|name| name.starts_with(&prefix) && name.ends_with(".rmeta"));
639 let archive = entry.path().with_extension("rlib");
640 metadata
641 && entry.file_type().is_ok_and(|file_type| file_type.is_file())
642 && fs::symlink_metadata(archive)
643 .is_ok_and(|metadata| metadata.file_type().is_file())
644 })
645 .map(|entry| entry.path())
646 .collect::<Vec<_>>();
647 matches.sort();
648 matches.dedup();
649 match matches.as_slice() {
650 [path] => Ok(path.clone()),
651 _ => Err(RustLibtestCompanionError::DependencyMetadata {
652 directory: directory.to_path_buf(),
653 crate_name,
654 count: matches.len(),
655 }),
656 }
657}
658
659pub fn rust_libtest_companion_build_plan(
660 patched_source: &Path,
661 target_libdir: &Path,
662 output: &Path,
663) -> Result<RustLibtestCompanionBuildPlan, RustLibtestCompanionError> {
664 let patched_source = regular_directory(patched_source)?;
665 let target_libdir = regular_directory(target_libdir)?;
666 let source = patched_source.join("src/lib.rs");
667 if !fs::symlink_metadata(&source).is_ok_and(|metadata| metadata.file_type().is_file()) {
668 return Err(RustLibtestCompanionError::UnsafeSource(source));
669 }
670 let getopts = one_metadata(&target_libdir, "getopts")?;
671 let libc = one_metadata(&target_libdir, "libc")?;
672 let identity_path = patched_source.join("supercov-libtest-source.json");
673 let identity: RustLibtestCompanionSourceIdentity =
674 serde_json::from_slice(&read_regular_file(&identity_path)?).map_err(|error| {
675 RustLibtestCompanionError::InvalidBundle {
676 path: identity_path,
677 reason: error.to_string(),
678 }
679 })?;
680 if !canonical_lower_sha256(&identity.patched_source_sha256) {
681 return Err(RustLibtestCompanionError::BundleMismatch(
682 "patched libtest source identity is noncanonical".into(),
683 ));
684 }
685 Ok(RustLibtestCompanionBuildPlan {
686 source: source.clone(),
687 output: output.to_path_buf(),
688 arguments: vec![
689 source.into_os_string(),
690 "--crate-name".into(),
691 "test".into(),
692 "--crate-type".into(),
693 "rlib".into(),
694 "--edition".into(),
695 "2024".into(),
696 "-Zcrate-attr=feature(rustc_private)".into(),
697 format!(
698 "--remap-path-prefix={}=/supercov/libtest-source",
699 patched_source.display()
700 )
701 .into(),
702 format!(
703 "-Cmetadata=supercov_{}",
704 &identity.patched_source_sha256[..16]
705 )
706 .into(),
707 "-L".into(),
708 format!("dependency={}", target_libdir.display()).into(),
709 "--extern".into(),
710 format!("getopts={}", getopts.display()).into(),
711 "--extern".into(),
712 format!("libc={}", libc.display()).into(),
713 "-o".into(),
714 output.as_os_str().to_owned(),
715 ],
716 rustc_bootstrap: "1".into(),
717 })
718}
719
720fn canonical_archive_kind(
721 kind: ReadableArchiveKind,
722 host_triple: &str,
723) -> Result<WritableArchiveKind, RustLibtestCompanionError> {
724 match kind {
725 ReadableArchiveKind::Gnu => Ok(WritableArchiveKind::Gnu),
726 ReadableArchiveKind::Gnu64 => Ok(WritableArchiveKind::Gnu64),
727 ReadableArchiveKind::Bsd if host_triple.contains("-apple-") => {
728 Ok(WritableArchiveKind::Darwin)
729 }
730 ReadableArchiveKind::Bsd => Ok(WritableArchiveKind::Bsd),
731 ReadableArchiveKind::Bsd64 => Ok(WritableArchiveKind::Darwin64),
732 ReadableArchiveKind::Coff => Ok(WritableArchiveKind::Coff),
733 ReadableArchiveKind::AixBig => Ok(WritableArchiveKind::AixBig),
734 _ => Err(RustLibtestCompanionError::InvalidArchive(format!(
735 "unsupported archive kind {kind:?} for {host_triple}"
736 ))),
737 }
738}
739
740pub fn canonicalize_rust_libtest_rlib(
750 bytes: &[u8],
751 host_triple: &str,
752) -> Result<Vec<u8>, RustLibtestCompanionError> {
753 let archive = ArchiveFile::parse(bytes).map_err(|error| {
754 RustLibtestCompanionError::InvalidArchive(format!("cannot parse archive: {error}"))
755 })?;
756 if archive.is_thin() {
757 return Err(RustLibtestCompanionError::InvalidArchive(
758 "thin archives are not self-contained".into(),
759 ));
760 }
761 let archive_kind = canonical_archive_kind(archive.kind(), host_triple)?;
762
763 let mut metadata = None;
764 let mut objects = Vec::new();
765 for member in archive.members() {
766 let member = member.map_err(|error| {
767 RustLibtestCompanionError::InvalidArchive(format!(
768 "cannot parse archive member: {error}"
769 ))
770 })?;
771 let name = std::str::from_utf8(member.name()).map_err(|_| {
772 RustLibtestCompanionError::InvalidArchive(
773 "archive contains a non-UTF-8 member name".into(),
774 )
775 })?;
776 let data = member.data(bytes).map_err(|error| {
777 RustLibtestCompanionError::InvalidArchive(format!(
778 "cannot read archive member {name}: {error}"
779 ))
780 })?;
781 if name == "lib.rmeta" {
782 if metadata.replace(data.to_vec()).is_some() {
783 return Err(RustLibtestCompanionError::InvalidArchive(
784 "archive contains more than one lib.rmeta member".into(),
785 ));
786 }
787 } else if name.ends_with(".rcgu.o") {
788 objects.push((format!("{:x}", Sha256::digest(data)), data.to_vec()));
789 } else {
790 return Err(RustLibtestCompanionError::InvalidArchive(format!(
791 "unexpected archive member {name}"
792 )));
793 }
794 }
795 let metadata = metadata.ok_or_else(|| {
796 RustLibtestCompanionError::InvalidArchive("archive has no lib.rmeta member".into())
797 })?;
798 if objects.is_empty() {
799 return Err(RustLibtestCompanionError::InvalidArchive(
800 "archive has no codegen object members".into(),
801 ));
802 }
803 objects.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1)));
804
805 let mut owned_members = Vec::with_capacity(objects.len() + 1);
806 owned_members.push(("lib.rmeta".to_owned(), metadata));
807 for (ordinal, (digest, data)) in objects.into_iter().enumerate() {
808 owned_members.push((
809 format!("supercov-libtest-{ordinal:06}-{digest}.rcgu.o"),
810 data,
811 ));
812 }
813 let members = owned_members
814 .iter()
815 .map(|(name, data)| NewArchiveMember::new(data, &DEFAULT_OBJECT_READER, name.clone()))
816 .collect::<Vec<_>>();
817 let mut output = Cursor::new(Vec::new());
818 write_archive_to_stream(
819 &mut output,
820 &members,
821 archive_kind,
822 false,
823 Some(host_triple.contains("arm64ec")),
824 )
825 .map_err(|error| {
826 RustLibtestCompanionError::InvalidArchive(format!(
827 "cannot write canonical archive: {error}"
828 ))
829 })?;
830 let output = output.into_inner();
831 let reparsed = ArchiveFile::parse(output.as_slice()).map_err(|error| {
832 RustLibtestCompanionError::InvalidArchive(format!(
833 "canonical archive did not parse: {error}"
834 ))
835 })?;
836 if reparsed.is_thin() || reparsed.members().count() != owned_members.len() {
837 return Err(RustLibtestCompanionError::InvalidArchive(
838 "canonical archive failed structural verification".into(),
839 ));
840 }
841 Ok(output)
842}
843
844pub fn rust_libtest_companion_bundle_path(compiler_companion: &Path) -> PathBuf {
845 let mut value = compiler_companion.as_os_str().to_owned();
846 value.push(".libtest.json");
847 PathBuf::from(value)
848}
849
850fn canonical_lower_sha256(value: &str) -> bool {
851 value.len() == 64
852 && value
853 .bytes()
854 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
855}
856
857fn sha256_file(path: &Path) -> Result<String, RustLibtestCompanionError> {
858 Ok(format!("{:x}", Sha256::digest(read_regular_file(path)?)))
859}
860
861fn read_regular_file(path: &Path) -> Result<Vec<u8>, RustLibtestCompanionError> {
862 let metadata = fs::symlink_metadata(path).map_err(|error| io_error(path, error))?;
863 if !metadata.file_type().is_file() {
864 return Err(RustLibtestCompanionError::UnsafeSource(path.to_path_buf()));
865 }
866 let mut options = OpenOptions::new();
867 options.read(true);
868 #[cfg(any(target_os = "linux", target_os = "macos"))]
869 {
870 use std::os::unix::fs::OpenOptionsExt as _;
871 #[cfg(target_os = "linux")]
872 const O_NOFOLLOW: i32 = 0x2_0000;
873 #[cfg(target_os = "macos")]
874 const O_NOFOLLOW: i32 = 0x100;
875 options.custom_flags(O_NOFOLLOW);
876 }
877 let mut file = options.open(path).map_err(|error| io_error(path, error))?;
878 if !file
879 .metadata()
880 .map_err(|error| io_error(path, error))?
881 .file_type()
882 .is_file()
883 {
884 return Err(RustLibtestCompanionError::UnsafeSource(path.to_path_buf()));
885 }
886 let mut bytes = Vec::new();
887 std::io::Read::read_to_end(&mut file, &mut bytes).map_err(|error| io_error(path, error))?;
888 Ok(bytes)
889}
890
891fn safe_artifact_basename(value: &str) -> bool {
892 let path = Path::new(value);
893 !value.is_empty()
894 && value != "."
895 && value != ".."
896 && path.components().count() == 1
897 && path
898 .components()
899 .all(|component| matches!(component, Component::Normal(_)))
900}
901
902pub fn select_rust_libtest_companion(
907 selection: &SelectedRustCompilerCompanion,
908) -> Result<SelectedRustLibtestCompanion, RustLibtestCompanionError> {
909 let bundle_path = rust_libtest_companion_bundle_path(&selection.companion_path);
910 let bundle_metadata =
911 fs::symlink_metadata(&bundle_path).map_err(|error| io_error(&bundle_path, error))?;
912 if !bundle_metadata.file_type().is_file() {
913 return Err(RustLibtestCompanionError::UnsafeSource(bundle_path));
914 }
915 let bundle: RustLibtestCompanionBundle =
916 serde_json::from_slice(&read_regular_file(&bundle_path)?).map_err(|error| {
917 RustLibtestCompanionError::InvalidBundle {
918 path: bundle_path.clone(),
919 reason: error.to_string(),
920 }
921 })?;
922 if bundle.schema_version != RUST_LIBTEST_COMPANION_BUNDLE_SCHEMA_VERSION
923 || bundle.event_protocol_version != RUST_LIBTEST_EVENT_PROTOCOL_VERSION
924 || !safe_artifact_basename(&bundle.artifact_file)
925 || [
926 bundle.compiler_companion_build_id.as_str(),
927 bundle.original_source_sha256.as_str(),
928 bundle.event_runtime_sha256.as_str(),
929 bundle.patched_source_sha256.as_str(),
930 bundle.artifact_sha256.as_str(),
931 ]
932 .iter()
933 .any(|value| !canonical_lower_sha256(value))
934 {
935 return Err(RustLibtestCompanionError::InvalidBundle {
936 path: bundle_path,
937 reason: "unsupported schema/protocol, unsafe artifact name or noncanonical digest"
938 .into(),
939 });
940 }
941 if bundle.compiler_companion_build_id != selection.handshake.companion_build_id {
942 return Err(RustLibtestCompanionError::BundleMismatch(
943 "compiler companion build ID differs".into(),
944 ));
945 }
946 if bundle.rustc_commit_hash != selection.compiler.rustc_commit_hash
947 || bundle.host_triple != selection.compiler.host_triple
948 {
949 return Err(RustLibtestCompanionError::BundleMismatch(
950 "selected rustc identity differs".into(),
951 ));
952 }
953 let directory = selection.companion_path.parent().ok_or_else(|| {
954 RustLibtestCompanionError::BundleMismatch(
955 "compiler companion has no artifact directory".into(),
956 )
957 })?;
958 let artifact_path = directory.join(&bundle.artifact_file);
959 if sha256_file(&artifact_path)? != bundle.artifact_sha256 {
960 return Err(RustLibtestCompanionError::BundleMismatch(
961 "libtest artifact digest differs".into(),
962 ));
963 }
964 Ok(SelectedRustLibtestCompanion {
965 bundle_path,
966 artifact_path,
967 bundle,
968 })
969}
970
971pub fn write_rust_libtest_companion_bundle(
972 compiler_companion: &Path,
973 source_identity: &RustLibtestCompanionSourceIdentity,
974 artifact: &Path,
975) -> Result<PathBuf, RustLibtestCompanionError> {
976 let compiler_companion = fs::canonicalize(compiler_companion)
977 .map_err(|error| io_error(compiler_companion, error))?;
978 let artifact = fs::canonicalize(artifact).map_err(|error| io_error(artifact, error))?;
979 let directory = compiler_companion.parent().ok_or_else(|| {
980 RustLibtestCompanionError::BundleMismatch(
981 "compiler companion has no artifact directory".into(),
982 )
983 })?;
984 if artifact.parent() != Some(directory) {
985 return Err(RustLibtestCompanionError::BundleMismatch(
986 "libtest artifact is not adjacent to the compiler companion".into(),
987 ));
988 }
989 let artifact_file = artifact
990 .file_name()
991 .and_then(|value| value.to_str())
992 .filter(|value| safe_artifact_basename(value))
993 .ok_or_else(|| {
994 RustLibtestCompanionError::BundleMismatch(
995 "libtest artifact has an unsafe filename".into(),
996 )
997 })?
998 .to_owned();
999 let bundle = RustLibtestCompanionBundle {
1000 schema_version: RUST_LIBTEST_COMPANION_BUNDLE_SCHEMA_VERSION,
1001 event_protocol_version: source_identity.event_protocol_version,
1002 compiler_companion_build_id: sha256_file(&compiler_companion)?,
1003 rustc_commit_hash: source_identity.rustc_commit_hash.clone(),
1004 host_triple: source_identity.host_triple.clone(),
1005 original_source_sha256: source_identity.original_source_sha256.clone(),
1006 event_runtime_sha256: source_identity.event_runtime_sha256.clone(),
1007 patched_source_sha256: source_identity.patched_source_sha256.clone(),
1008 artifact_file,
1009 artifact_sha256: sha256_file(&artifact)?,
1010 };
1011 let path = rust_libtest_companion_bundle_path(&compiler_companion);
1012 let mut bytes = serde_json::to_vec_pretty(&bundle).map_err(|error| io_error(&path, error))?;
1013 bytes.push(b'\n');
1014 let partial = path.with_file_name(format!(
1015 ".{}.{}-{}.partial",
1016 path.file_name()
1017 .and_then(|value| value.to_str())
1018 .unwrap_or("libtest"),
1019 std::process::id(),
1020 SystemTime::now()
1021 .duration_since(UNIX_EPOCH)
1022 .map_err(|error| io_error(&path, error))?
1023 .as_nanos()
1024 ));
1025 let mut cleanup = RemoveFileOnDrop(Some(partial.clone()));
1026 let mut options = OpenOptions::new();
1027 options.write(true).create_new(true);
1028 #[cfg(unix)]
1029 {
1030 use std::os::unix::fs::OpenOptionsExt as _;
1031 options.mode(0o600);
1032 }
1033 let mut file = options
1034 .open(&partial)
1035 .map_err(|error| io_error(&partial, error))?;
1036 file.write_all(&bytes)
1037 .and_then(|()| file.sync_all())
1038 .map_err(|error| io_error(&partial, error))?;
1039 drop(file);
1040 if let Ok(metadata) = fs::symlink_metadata(&path)
1041 && !metadata.file_type().is_file()
1042 {
1043 return Err(RustLibtestCompanionError::UnsafeSource(path));
1044 }
1045 fs::rename(&partial, &path).map_err(|error| io_error(&path, error))?;
1046 sync_directory(directory)?;
1047 cleanup.0 = None;
1048 Ok(path)
1049}
1050
1051fn libtest_builder_lock_path(compiler_companion: &Path) -> PathBuf {
1052 let mut value = compiler_companion.as_os_str().to_owned();
1053 value.push(".libtest.lock");
1054 PathBuf::from(value)
1055}
1056
1057fn bundle_matches_source_identity(
1058 selected: &SelectedRustLibtestCompanion,
1059 identity: &RustLibtestCompanionSourceIdentity,
1060) -> Result<(), RustLibtestCompanionError> {
1061 if selected.bundle.event_protocol_version != identity.event_protocol_version
1062 || selected.bundle.rustc_commit_hash != identity.rustc_commit_hash
1063 || selected.bundle.host_triple != identity.host_triple
1064 || selected.bundle.original_source_sha256 != identity.original_source_sha256
1065 || selected.bundle.event_runtime_sha256 != identity.event_runtime_sha256
1066 || selected.bundle.patched_source_sha256 != identity.patched_source_sha256
1067 {
1068 return Err(RustLibtestCompanionError::BundleMismatch(
1069 "published bundle differs from the authenticated prepared source".into(),
1070 ));
1071 }
1072 Ok(())
1073}
1074
1075pub fn build_exact_rust_libtest_companion(
1083 source_root: &Path,
1084 work_root: &Path,
1085 rustc: &Path,
1086 compiler_companion: &Path,
1087) -> Result<SelectedRustLibtestCompanion, RustLibtestCompanionError> {
1088 let rustc = fs::canonicalize(rustc).map_err(|error| io_error(rustc, error))?;
1089 let compiler = probe_rustc_identity(&rustc).map_err(|error| {
1090 RustLibtestCompanionError::BundleMismatch(format!(
1091 "could not authenticate exact rustc: {error}"
1092 ))
1093 })?;
1094 let work_root = regular_directory(work_root)?;
1095 let compiler_companion = fs::canonicalize(compiler_companion)
1096 .map_err(|error| io_error(compiler_companion, error))?;
1097 if !fs::symlink_metadata(&compiler_companion)
1098 .is_ok_and(|metadata| metadata.file_type().is_file())
1099 {
1100 return Err(RustLibtestCompanionError::UnsafeSource(compiler_companion));
1101 }
1102 let artifact_directory = compiler_companion.parent().ok_or_else(|| {
1103 RustLibtestCompanionError::BundleMismatch(
1104 "compiler companion has no artifact directory".into(),
1105 )
1106 })?;
1107 let lock_path = libtest_builder_lock_path(&compiler_companion);
1108 let builder_lock = acquire_kernel_lock(&lock_path)?;
1109 let selection =
1110 select_rust_compiler_companion(&rustc, std::slice::from_ref(&compiler_companion), false)
1111 .map_err(|error| {
1112 RustLibtestCompanionError::BundleMismatch(format!(
1113 "could not authenticate compiler companion: {error}"
1114 ))
1115 })?;
1116
1117 let bundle_path = rust_libtest_companion_bundle_path(&compiler_companion);
1118 let bundle_name = bundle_path
1119 .file_name()
1120 .and_then(|name| name.to_str())
1121 .ok_or_else(|| RustLibtestCompanionError::UnsafeSource(bundle_path.clone()))?;
1122 remove_owned_partials(artifact_directory, &format!(".{bundle_name}."), false)?;
1123
1124 let patched_source = work_root.join("patched-libtest");
1125 let source_identity = prepare_exact_libtest_source(source_root, &patched_source, &compiler)?;
1126 match fs::symlink_metadata(&bundle_path) {
1127 Ok(metadata) if metadata.file_type().is_file() => {
1128 let selected = select_rust_libtest_companion(&selection)?;
1129 bundle_matches_source_identity(&selected, &source_identity)?;
1130 return Ok(selected);
1131 }
1132 Ok(_) => return Err(RustLibtestCompanionError::UnsafeSource(bundle_path)),
1133 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1134 Err(error) => return Err(io_error(&bundle_path, error)),
1135 }
1136
1137 let target_libdir_output = Command::new(&rustc)
1138 .args(["--print", "target-libdir"])
1139 .env_remove("RUSTC_WRAPPER")
1140 .env_remove("RUSTC_WORKSPACE_WRAPPER")
1141 .output()
1142 .map_err(|error| io_error(&rustc, error))?;
1143 if !target_libdir_output.status.success() || !target_libdir_output.stderr.is_empty() {
1144 return Err(RustLibtestCompanionError::BuildFailed {
1145 program: rustc,
1146 status: target_libdir_output.status.code(),
1147 stdout: String::from_utf8_lossy(&target_libdir_output.stdout).into_owned(),
1148 stderr: String::from_utf8_lossy(&target_libdir_output.stderr).into_owned(),
1149 });
1150 }
1151 let target_libdir = PathBuf::from(
1152 std::str::from_utf8(&target_libdir_output.stdout)
1153 .map_err(|_| {
1154 RustLibtestCompanionError::BundleMismatch("rustc target libdir is not UTF-8".into())
1155 })?
1156 .trim(),
1157 );
1158 let artifact_name = format!(
1159 "libtest-supercov-v{}-{}-{}.rlib",
1160 RUST_LIBTEST_COMPANION_BUNDLE_SCHEMA_VERSION,
1161 &source_identity.rustc_commit_hash[..12],
1162 &source_identity.patched_source_sha256[..12]
1163 );
1164 let artifact = artifact_directory.join(&artifact_name);
1165 remove_owned_partials(artifact_directory, &format!(".{artifact_name}."), false)?;
1166
1167 let build_output = work_root.join(&artifact_name);
1171 remove_owned_path(&build_output, false)?;
1172 let mut build_cleanup = RemoveFileOnDrop(Some(build_output.clone()));
1173 let plan = rust_libtest_companion_build_plan(&patched_source, &target_libdir, &build_output)?;
1174 let mut command = Command::new(&rustc);
1175 command
1176 .args(&plan.arguments)
1177 .env("RUSTC_BOOTSTRAP", &plan.rustc_bootstrap)
1178 .env_remove("RUSTC_WRAPPER")
1179 .env_remove("RUSTC_WORKSPACE_WRAPPER")
1180 .env_remove(crate::rust_compiler_orchestration::RUST_COMPILER_WRAPPER_CONFIG_ENV)
1181 .env_remove(crate::rust_compiler_orchestration::RUST_COMPILER_INNER_MODE_ENV);
1182 inherit_lock_through_exec(&mut command, &builder_lock);
1183 let output = command.output().map_err(|error| io_error(&rustc, error))?;
1184 if !output.status.success() {
1185 return Err(RustLibtestCompanionError::BuildFailed {
1186 program: rustc,
1187 status: output.status.code(),
1188 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
1189 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
1190 });
1191 }
1192 let compiled = read_regular_file(&build_output)?;
1193 let canonical = canonicalize_rust_libtest_rlib(&compiled, &compiler.host_triple)?;
1194 let partial = artifact_directory.join(format!(
1195 ".{artifact_name}.{}-{}.partial",
1196 std::process::id(),
1197 SystemTime::now()
1198 .duration_since(UNIX_EPOCH)
1199 .map_err(|error| io_error(&artifact, error))?
1200 .as_nanos()
1201 ));
1202 let mut partial_cleanup = RemoveFileOnDrop(Some(partial.clone()));
1203 let mut options = OpenOptions::new();
1204 options.write(true).create_new(true);
1205 #[cfg(unix)]
1206 {
1207 use std::os::unix::fs::OpenOptionsExt as _;
1208 options.mode(0o600);
1209 }
1210 let mut partial_file = options
1211 .open(&partial)
1212 .map_err(|error| io_error(&partial, error))?;
1213 partial_file
1214 .write_all(&canonical)
1215 .and_then(|()| partial_file.sync_all())
1216 .map_err(|error| io_error(&partial, error))?;
1217 drop(partial_file);
1218
1219 match fs::symlink_metadata(&artifact) {
1220 Ok(metadata) if metadata.file_type().is_file() => {
1221 if read_regular_file(&artifact)? != canonical {
1222 return Err(RustLibtestCompanionError::BundleMismatch(
1223 "the same exact libtest identity produced different artifact bytes".into(),
1224 ));
1225 }
1226 fs::remove_file(&partial).map_err(|error| io_error(&partial, error))?;
1227 partial_cleanup.0 = None;
1228 }
1229 Ok(_) => return Err(RustLibtestCompanionError::UnsafeSource(artifact)),
1230 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
1231 fs::rename(&partial, &artifact).map_err(|error| io_error(&artifact, error))?;
1232 sync_directory(artifact_directory)?;
1233 partial_cleanup.0 = None;
1234 }
1235 Err(error) => return Err(io_error(&artifact, error)),
1236 }
1237 fs::remove_file(&build_output).map_err(|error| io_error(&build_output, error))?;
1238 build_cleanup.0 = None;
1239
1240 let published_bundle =
1241 write_rust_libtest_companion_bundle(&compiler_companion, &source_identity, &artifact)?;
1242 let selected = select_rust_libtest_companion(&selection)?;
1243 bundle_matches_source_identity(&selected, &source_identity)?;
1244 if selected.bundle_path != published_bundle || selected.artifact_path != artifact {
1245 return Err(RustLibtestCompanionError::BundleMismatch(
1246 "published libtest companion did not reselect exactly".into(),
1247 ));
1248 }
1249 Ok(selected)
1250}
1251
1252#[cfg(test)]
1253mod tests {
1254 use std::{
1255 sync::atomic::{AtomicU64, Ordering},
1256 time::{SystemTime, UNIX_EPOCH},
1257 };
1258
1259 use super::*;
1260 use supercov_contracts::{
1261 EVIDENCE_ARCHIVE_SCHEMA_VERSION, RUST_COMPILER_COMPANION_PROTOCOL_VERSION,
1262 RustCompilerCompanionCapabilities, RustCompilerCompanionHandshake,
1263 };
1264
1265 fn compiler() -> RustCompilerIdentity {
1266 RustCompilerIdentity {
1267 rustc_commit_hash: "a".repeat(40),
1268 rustc_release: "1.95.0".into(),
1269 host_triple: "aarch64-apple-darwin".into(),
1270 rustc_driver_sha256: "b".repeat(64),
1271 }
1272 }
1273
1274 fn selection(
1275 compiler_companion: PathBuf,
1276 compiler: RustCompilerIdentity,
1277 ) -> SelectedRustCompilerCompanion {
1278 let build_id = sha256_file(&compiler_companion).unwrap();
1279 SelectedRustCompilerCompanion {
1280 rustc_path: compiler_companion.with_file_name("rustc"),
1281 compiler_library_directory: compiler_companion.parent().unwrap().to_path_buf(),
1282 companion_path: compiler_companion,
1283 compiler: compiler.clone(),
1284 handshake: RustCompilerCompanionHandshake {
1285 protocol_version: RUST_COMPILER_COMPANION_PROTOCOL_VERSION,
1286 frontend_id: "rust".into(),
1287 coverage_model_variant: "rust-source-v1".into(),
1288 evidence_schema_version: EVIDENCE_ARCHIVE_SCHEMA_VERSION,
1289 companion_build_id: build_id,
1290 compiler,
1291 capabilities: RustCompilerCompanionCapabilities {
1292 expanded_hir_provenance: true,
1293 runtime_mir_probe_insertion: true,
1294 generated_source_provenance: true,
1295 ctfe_path_tracing: true,
1296 rustdoc_doctest_tracing: true,
1297 exact_test_harness_attribution: true,
1298 },
1299 },
1300 }
1301 }
1302
1303 fn fixture() -> PathBuf {
1304 static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
1305 let nonce = SystemTime::now()
1306 .duration_since(UNIX_EPOCH)
1307 .unwrap()
1308 .as_nanos();
1309 let root = std::env::temp_dir().join(format!(
1310 "supercov-libtest-source-{}-{nonce}-{}",
1311 std::process::id(),
1312 NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed)
1313 ));
1314 fs::create_dir_all(root.join("source/src")).unwrap();
1315 fs::write(root.join("source/Cargo.toml"), b"[package]\nname='test'\n").unwrap();
1316 fs::write(root.join("source/src/lib.rs"), fixture_lib_source()).unwrap();
1317 fs::write(
1318 root.join("source/src/console.rs"),
1319 format!("use std::io;\n{CONSOLE_ANCHOR}\n }}\n}}\n{LISTING_ANCHOR}\n}}\n"),
1320 )
1321 .unwrap();
1322 root
1323 }
1324
1325 fn fixture_lib_source() -> String {
1326 format!(
1327 "#![feature(test)]\n{LIB_ANCHOR}\n{IN_PROCESS_ANCHOR} synthetic;\n{SPAWNED_PROCESS_ANCHOR};\n{BENCH_ANCHOR}(synthetic);\n"
1328 )
1329 }
1330
1331 #[test]
1332 fn patches_atomically_with_relocation_stable_identity() {
1333 let first = fixture();
1334 let second = fixture();
1335 let first_identity = prepare_exact_libtest_source(
1336 &first.join("source"),
1337 &first.join("patched"),
1338 &compiler(),
1339 )
1340 .unwrap();
1341 let second_identity = prepare_exact_libtest_source(
1342 &second.join("source"),
1343 &second.join("patched"),
1344 &compiler(),
1345 )
1346 .unwrap();
1347 assert_eq!(first_identity, second_identity);
1348 assert!(
1349 fs::read_to_string(first.join("patched/src/lib.rs"))
1350 .unwrap()
1351 .contains("mod supercov_events;")
1352 );
1353 assert_eq!(
1354 fs::read_to_string(first.join("patched/src/lib.rs"))
1355 .unwrap()
1356 .matches("supercov_events::enter_test")
1357 .count(),
1358 3
1359 );
1360 assert!(
1361 fs::read_to_string(first.join("patched/src/console.rs"))
1362 .unwrap()
1363 .contains("crate::supercov_events::emit(event)?;")
1364 );
1365 assert!(
1366 fs::read_to_string(first.join("patched/src/console.rs"))
1367 .unwrap()
1368 .contains("crate::supercov_events::emit_listing(")
1369 );
1370 assert_eq!(
1371 fs::read_to_string(first.join("source/src/lib.rs")).unwrap(),
1372 fixture_lib_source()
1373 );
1374 assert!(first.join("patched/supercov-libtest-source.json").is_file());
1375 assert_eq!(
1376 prepare_exact_libtest_source(
1377 &first.join("source"),
1378 &first.join("patched"),
1379 &compiler()
1380 )
1381 .unwrap(),
1382 first_identity
1383 );
1384 fs::remove_dir_all(first).unwrap();
1385 fs::remove_dir_all(second).unwrap();
1386 }
1387
1388 #[test]
1389 fn prepared_source_reuse_fails_closed_on_tree_or_identity_tampering() {
1390 let root = fixture();
1391 let source = root.join("source");
1392 let patched = root.join("patched");
1393 prepare_exact_libtest_source(&source, &patched, &compiler()).unwrap();
1394 fs::write(patched.join("src/lib.rs"), b"tampered\n").unwrap();
1395 assert!(matches!(
1396 prepare_exact_libtest_source(&source, &patched, &compiler()),
1397 Err(RustLibtestCompanionError::BundleMismatch(reason))
1398 if reason.contains("tree digest")
1399 ));
1400
1401 fs::remove_dir_all(&patched).unwrap();
1402 prepare_exact_libtest_source(&source, &patched, &compiler()).unwrap();
1403 let identity_path = patched.join(SOURCE_IDENTITY_FILE);
1404 let mut identity: serde_json::Value =
1405 serde_json::from_slice(&fs::read(&identity_path).unwrap()).unwrap();
1406 identity["unknown"] = serde_json::json!(true);
1407 fs::write(&identity_path, serde_json::to_vec(&identity).unwrap()).unwrap();
1408 assert!(matches!(
1409 prepare_exact_libtest_source(&source, &patched, &compiler()),
1410 Err(RustLibtestCompanionError::InvalidBundle { .. })
1411 ));
1412 fs::remove_dir_all(root).unwrap();
1413 }
1414
1415 #[test]
1416 fn concurrent_source_preparation_converges_without_partial_debris() {
1417 let root = fixture();
1418 let source = root.join("source");
1419 let patched = root.join("patched");
1420 let workers = (0..8)
1421 .map(|_| {
1422 let source = source.clone();
1423 let patched = patched.clone();
1424 std::thread::spawn(move || {
1425 prepare_exact_libtest_source(&source, &patched, &compiler()).unwrap()
1426 })
1427 })
1428 .collect::<Vec<_>>();
1429 let identities = workers
1430 .into_iter()
1431 .map(|worker| worker.join().unwrap())
1432 .collect::<Vec<_>>();
1433 assert!(identities.windows(2).all(|pair| pair[0] == pair[1]));
1434 assert!(fs::read_dir(&root).unwrap().all(|entry| {
1435 !entry
1436 .unwrap()
1437 .file_name()
1438 .to_string_lossy()
1439 .ends_with(".partial")
1440 }));
1441 fs::remove_dir_all(root).unwrap();
1442 }
1443
1444 #[cfg(unix)]
1445 #[test]
1446 fn libtest_builder_lock_holder_helper() {
1447 let Some(lock) = std::env::var_os("SUPERCOV_TEST_LIBTEST_LOCK") else {
1448 return;
1449 };
1450 let partial =
1451 PathBuf::from(std::env::var_os("SUPERCOV_TEST_LIBTEST_PARTIAL").expect("partial path"));
1452 let ready =
1453 PathBuf::from(std::env::var_os("SUPERCOV_TEST_LIBTEST_READY").expect("ready path"));
1454 let _lock = acquire_kernel_lock(Path::new(&lock)).unwrap();
1455 fs::write(&partial, b"incomplete\n").unwrap();
1456 fs::write(&ready, b"locked\n").unwrap();
1457 loop {
1458 std::thread::sleep(Duration::from_secs(1));
1459 }
1460 }
1461
1462 #[cfg(unix)]
1463 #[test]
1464 fn killed_builder_releases_lock_and_owned_partial_is_recoverable() {
1465 use std::process::Stdio;
1466
1467 let root = fixture();
1468 let lock = root.join("companion.libtest.lock");
1469 let partial = root.join(".artifact.123.partial");
1470 let ready = root.join("builder-ready");
1471 let mut child = Command::new(std::env::current_exe().unwrap())
1472 .args([
1473 "--exact",
1474 "rust_libtest_companion::tests::libtest_builder_lock_holder_helper",
1475 "--nocapture",
1476 ])
1477 .env("SUPERCOV_TEST_LIBTEST_LOCK", &lock)
1478 .env("SUPERCOV_TEST_LIBTEST_PARTIAL", &partial)
1479 .env("SUPERCOV_TEST_LIBTEST_READY", &ready)
1480 .stdin(Stdio::null())
1481 .stdout(Stdio::null())
1482 .stderr(Stdio::null())
1483 .spawn()
1484 .unwrap();
1485 let started = Instant::now();
1486 while !ready.is_file() {
1487 assert!(
1488 started.elapsed() < Duration::from_secs(10),
1489 "libtest builder helper did not acquire its kernel lock"
1490 );
1491 std::thread::sleep(Duration::from_millis(10));
1492 }
1493 assert_eq!(
1494 unsafe { libc::kill(child.id().try_into().unwrap(), libc::SIGKILL) },
1495 0
1496 );
1497 assert_eq!(child.wait().unwrap().code(), None);
1498 let recovery_started = Instant::now();
1499 let _lock = acquire_kernel_lock(&lock).unwrap();
1500 assert!(recovery_started.elapsed() < Duration::from_secs(5));
1501 remove_owned_partials(&root, ".artifact.", false).unwrap();
1502 assert!(!partial.exists());
1503 fs::remove_dir_all(root).unwrap();
1504 }
1505
1506 #[cfg(unix)]
1507 #[test]
1508 fn libtest_builder_child_lock_holder_helper() {
1509 let Some(lock) = std::env::var_os("SUPERCOV_TEST_LIBTEST_CHILD_LOCK") else {
1510 return;
1511 };
1512 let ready = PathBuf::from(
1513 std::env::var_os("SUPERCOV_TEST_LIBTEST_CHILD_READY").expect("ready path"),
1514 );
1515 let lock = acquire_kernel_lock(Path::new(&lock)).unwrap();
1516 let mut command = Command::new("/bin/sh");
1517 command.args(["-c", "sleep 2"]);
1518 inherit_lock_through_exec(&mut command, &lock);
1519 let child = command.spawn().unwrap();
1520 fs::write(&ready, format!("{}\n", child.id())).unwrap();
1521 drop(child);
1525 loop {
1526 std::thread::sleep(Duration::from_secs(1));
1527 }
1528 }
1529
1530 #[cfg(unix)]
1531 #[test]
1532 fn killed_builder_keeps_lock_until_its_compiler_child_exits() {
1533 use std::process::Stdio;
1534
1535 let root = fixture();
1536 let lock = root.join("companion-child.libtest.lock");
1537 let ready = root.join("compiler-ready");
1538 let mut builder = Command::new(std::env::current_exe().unwrap())
1539 .args([
1540 "--exact",
1541 "rust_libtest_companion::tests::libtest_builder_child_lock_holder_helper",
1542 "--nocapture",
1543 ])
1544 .env("SUPERCOV_TEST_LIBTEST_CHILD_LOCK", &lock)
1545 .env("SUPERCOV_TEST_LIBTEST_CHILD_READY", &ready)
1546 .stdin(Stdio::null())
1547 .stdout(Stdio::null())
1548 .stderr(Stdio::null())
1549 .spawn()
1550 .unwrap();
1551 let started = Instant::now();
1552 while !ready.is_file() {
1553 assert!(started.elapsed() < Duration::from_secs(10));
1554 std::thread::sleep(Duration::from_millis(10));
1555 }
1556 assert_eq!(
1557 unsafe { libc::kill(builder.id().try_into().unwrap(), libc::SIGKILL) },
1558 0
1559 );
1560 assert_eq!(builder.wait().unwrap().code(), None);
1561 let recovery_started = Instant::now();
1562 let _lock = acquire_kernel_lock(&lock).unwrap();
1563 assert!(
1564 recovery_started.elapsed() >= Duration::from_millis(500),
1565 "the compiler child did not retain the publication lock"
1566 );
1567 assert!(recovery_started.elapsed() < Duration::from_secs(5));
1568 fs::remove_dir_all(root).unwrap();
1569 }
1570
1571 #[test]
1572 fn rejects_unrecognized_or_unsafe_exact_source() {
1573 let root = fixture();
1574 fs::write(root.join("source/src/console.rs"), "not libtest\n").unwrap();
1575 assert!(matches!(
1576 prepare_exact_libtest_source(&root.join("source"), &root.join("patched"), &compiler()),
1577 Err(RustLibtestCompanionError::UnrecognizedSource { .. })
1578 ));
1579 assert!(!root.join("patched").exists());
1580 assert_eq!(
1581 fs::read_to_string(root.join("source/src/console.rs")).unwrap(),
1582 "not libtest\n"
1583 );
1584 fs::remove_dir_all(root).unwrap();
1585 }
1586
1587 #[cfg(unix)]
1588 #[test]
1589 fn rejects_symlinks_without_leaving_a_destination() {
1590 use std::os::unix::fs::symlink;
1591
1592 let root = fixture();
1593 symlink("lib.rs", root.join("source/src/alias.rs")).unwrap();
1594 assert!(matches!(
1595 prepare_exact_libtest_source(&root.join("source"), &root.join("patched"), &compiler()),
1596 Err(RustLibtestCompanionError::UnsafeSource(_))
1597 ));
1598 assert!(!root.join("patched").exists());
1599 fs::remove_dir_all(root).unwrap();
1600 }
1601
1602 #[test]
1603 fn build_plan_requires_exact_full_metadata() {
1604 let root = fixture();
1605 prepare_exact_libtest_source(&root.join("source"), &root.join("patched"), &compiler())
1606 .unwrap();
1607 fs::create_dir(root.join("libdir")).unwrap();
1608 fs::write(root.join("libdir/libgetopts-a.rmeta"), b"getopts").unwrap();
1609 fs::write(root.join("libdir/libgetopts-a.rlib"), b"getopts archive").unwrap();
1610 fs::write(root.join("libdir/liblibc-b.rmeta"), b"libc").unwrap();
1611 fs::write(root.join("libdir/liblibc-b.rlib"), b"libc archive").unwrap();
1612 let plan = rust_libtest_companion_build_plan(
1613 &root.join("patched"),
1614 &root.join("libdir"),
1615 &root.join("libtest-supercov.rlib"),
1616 )
1617 .unwrap();
1618 assert_eq!(
1619 plan.source,
1620 fs::canonicalize(root.join("patched/src/lib.rs")).unwrap()
1621 );
1622 assert!(
1623 plan.arguments
1624 .iter()
1625 .any(|value| value == "-Zcrate-attr=feature(rustc_private)")
1626 );
1627 assert!(
1628 !plan
1629 .arguments
1630 .iter()
1631 .any(|value| value.to_string_lossy().starts_with("-Cincremental"))
1632 );
1633 fs::write(root.join("libdir/liblibc-c.rmeta"), b"duplicate").unwrap();
1634 fs::write(root.join("libdir/liblibc-c.rlib"), b"duplicate archive").unwrap();
1635 assert!(
1636 rust_libtest_companion_build_plan(
1637 &root.join("patched"),
1638 &root.join("libdir"),
1639 &root.join("duplicate.rlib")
1640 )
1641 .is_err()
1642 );
1643 fs::remove_dir_all(root).unwrap();
1644 }
1645
1646 fn synthetic_rlib(object_suffix: &str, reverse: bool) -> Vec<u8> {
1647 let metadata = b"metadata".as_slice();
1648 let first = b"first object".as_slice();
1649 let second = b"second object".as_slice();
1650 let first_name = format!("test.alpha.{object_suffix}.rcgu.o");
1651 let second_name = format!("test.beta.{object_suffix}.rcgu.o");
1652 let mut owned = [
1653 ("lib.rmeta".to_owned(), metadata),
1654 (first_name, first),
1655 (second_name, second),
1656 ];
1657 if reverse {
1658 owned[1..].reverse();
1659 }
1660 let members = owned
1661 .iter()
1662 .map(|(name, data)| NewArchiveMember::new(data, &DEFAULT_OBJECT_READER, name.clone()))
1663 .collect::<Vec<_>>();
1664 let mut output = Cursor::new(Vec::new());
1665 write_archive_to_stream(
1666 &mut output,
1667 &members,
1668 WritableArchiveKind::Darwin,
1669 false,
1670 Some(false),
1671 )
1672 .unwrap();
1673 output.into_inner()
1674 }
1675
1676 #[test]
1677 fn canonical_rlib_ignores_session_names_and_member_order() {
1678 let first = canonicalize_rust_libtest_rlib(
1679 &synthetic_rlib("random-one", false),
1680 "aarch64-apple-darwin",
1681 )
1682 .unwrap();
1683 let second = canonicalize_rust_libtest_rlib(
1684 &synthetic_rlib("random-two", true),
1685 "aarch64-apple-darwin",
1686 )
1687 .unwrap();
1688 assert_eq!(first, second);
1689
1690 let archive = ArchiveFile::parse(first.as_slice()).unwrap();
1691 let names = archive
1692 .members()
1693 .map(|member| {
1694 std::str::from_utf8(member.unwrap().name())
1695 .unwrap()
1696 .to_owned()
1697 })
1698 .collect::<Vec<_>>();
1699 assert_eq!(names.first().map(String::as_str), Some("lib.rmeta"));
1700 assert!(
1701 names[1..]
1702 .iter()
1703 .all(|name| name.starts_with("supercov-libtest-") && name.ends_with(".rcgu.o"))
1704 );
1705 }
1706
1707 #[test]
1708 fn bundle_binds_exact_compiler_source_runtime_and_artifact_bytes() {
1709 let root = fixture();
1710 let compiler = compiler();
1711 let source_identity =
1712 prepare_exact_libtest_source(&root.join("source"), &root.join("patched"), &compiler)
1713 .unwrap();
1714 let compiler_companion = root.join("supercov-rustc-companion");
1715 let artifact = root.join("libtest-supercov.rlib");
1716 fs::write(&compiler_companion, b"compiler companion").unwrap();
1717 fs::write(&artifact, b"exact libtest rlib").unwrap();
1718 let selected = selection(
1719 fs::canonicalize(&compiler_companion).unwrap(),
1720 compiler.clone(),
1721 );
1722 let bundle_path =
1723 write_rust_libtest_companion_bundle(&compiler_companion, &source_identity, &artifact)
1724 .unwrap();
1725 let bound = select_rust_libtest_companion(&selected).unwrap();
1726 assert_eq!(bound.bundle_path, bundle_path);
1727 assert_eq!(bound.artifact_path, fs::canonicalize(&artifact).unwrap());
1728 assert_eq!(
1729 bound.bundle.compiler_companion_build_id,
1730 selected.handshake.companion_build_id
1731 );
1732 assert_eq!(
1733 bound.bundle.original_source_sha256,
1734 source_identity.original_source_sha256
1735 );
1736
1737 fs::write(&artifact, b"tampered").unwrap();
1738 assert!(matches!(
1739 select_rust_libtest_companion(&selected),
1740 Err(RustLibtestCompanionError::BundleMismatch(_))
1741 ));
1742 fs::remove_dir_all(root).unwrap();
1743 }
1744
1745 #[test]
1746 fn bundle_rejects_unknown_fields_unsafe_paths_and_companion_mismatch() {
1747 let root = fixture();
1748 let compiler = compiler();
1749 let source_identity =
1750 prepare_exact_libtest_source(&root.join("source"), &root.join("patched"), &compiler)
1751 .unwrap();
1752 let compiler_companion = root.join("supercov-rustc-companion");
1753 let artifact = root.join("libtest-supercov.rlib");
1754 fs::write(&compiler_companion, b"compiler companion").unwrap();
1755 fs::write(&artifact, b"exact libtest rlib").unwrap();
1756 let mut selected = selection(
1757 fs::canonicalize(&compiler_companion).unwrap(),
1758 compiler.clone(),
1759 );
1760 let bundle_path =
1761 write_rust_libtest_companion_bundle(&compiler_companion, &source_identity, &artifact)
1762 .unwrap();
1763 let mut value: serde_json::Value =
1764 serde_json::from_slice(&fs::read(&bundle_path).unwrap()).unwrap();
1765 value["unknown"] = serde_json::json!(true);
1766 fs::write(&bundle_path, serde_json::to_vec(&value).unwrap()).unwrap();
1767 assert!(matches!(
1768 select_rust_libtest_companion(&selected),
1769 Err(RustLibtestCompanionError::InvalidBundle { .. })
1770 ));
1771
1772 value.as_object_mut().unwrap().remove("unknown");
1773 value["artifactFile"] = serde_json::json!("../escaped.rlib");
1774 fs::write(&bundle_path, serde_json::to_vec(&value).unwrap()).unwrap();
1775 assert!(matches!(
1776 select_rust_libtest_companion(&selected),
1777 Err(RustLibtestCompanionError::InvalidBundle { .. })
1778 ));
1779
1780 value["artifactFile"] = serde_json::json!("libtest-supercov.rlib");
1781 fs::write(&bundle_path, serde_json::to_vec(&value).unwrap()).unwrap();
1782 selected.handshake.companion_build_id = "f".repeat(64);
1783 assert!(matches!(
1784 select_rust_libtest_companion(&selected),
1785 Err(RustLibtestCompanionError::BundleMismatch(_))
1786 ));
1787 fs::remove_dir_all(root).unwrap();
1788 }
1789}