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 }
435
436fn remove_owned_path(path: &Path, expect_directory: bool) -> Result<(), RustLibtestCompanionError> {
437 let metadata = match fs::symlink_metadata(path) {
438 Ok(metadata) => metadata,
439 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
440 Err(error) => return Err(io_error(path, error)),
441 };
442 if expect_directory && metadata.file_type().is_dir() {
443 fs::remove_dir_all(path).map_err(|error| io_error(path, error))
444 } else if !expect_directory && metadata.file_type().is_file() {
445 fs::remove_file(path).map_err(|error| io_error(path, error))
446 } else {
447 Err(RustLibtestCompanionError::UnsafeSource(path.to_path_buf()))
448 }
449}
450
451fn remove_owned_partials(
452 directory: &Path,
453 prefix: &str,
454 expect_directory: bool,
455) -> Result<(), RustLibtestCompanionError> {
456 for entry in fs::read_dir(directory).map_err(|error| io_error(directory, error))? {
457 let entry = entry.map_err(|error| io_error(directory, error))?;
458 let name = entry.file_name();
459 let Some(name) = name.to_str() else {
460 continue;
461 };
462 if name.starts_with(prefix) && name.ends_with(".partial") {
463 remove_owned_path(&entry.path(), expect_directory)?;
464 }
465 }
466 Ok(())
467}
468
469fn validate_prepared_libtest_source(
470 source_root: &Path,
471 destination: &Path,
472 compiler: &RustCompilerIdentity,
473) -> Result<RustLibtestCompanionSourceIdentity, RustLibtestCompanionError> {
474 let destination = regular_directory(destination)?;
475 let identity_path = destination.join(SOURCE_IDENTITY_FILE);
476 let identity: RustLibtestCompanionSourceIdentity =
477 serde_json::from_slice(&read_regular_file(&identity_path)?).map_err(|error| {
478 RustLibtestCompanionError::InvalidBundle {
479 path: identity_path.clone(),
480 reason: error.to_string(),
481 }
482 })?;
483 let runtime_sha256 = format!(
484 "{:x}",
485 Sha256::digest(rust_libtest_event_runtime_source().as_bytes())
486 );
487 if identity.event_protocol_version != RUST_LIBTEST_EVENT_PROTOCOL_VERSION
488 || identity.rustc_commit_hash != compiler.rustc_commit_hash
489 || identity.rustc_release != compiler.rustc_release
490 || identity.host_triple != compiler.host_triple
491 || identity.event_runtime_sha256 != runtime_sha256
492 || [
493 identity.original_source_sha256.as_str(),
494 identity.event_runtime_sha256.as_str(),
495 identity.patched_source_sha256.as_str(),
496 ]
497 .iter()
498 .any(|value| !canonical_lower_sha256(value))
499 {
500 return Err(RustLibtestCompanionError::BundleMismatch(
501 "prepared libtest source identity differs from the exact compiler/runtime".into(),
502 ));
503 }
504 if source_tree_digest(source_root)? != identity.original_source_sha256 {
505 return Err(RustLibtestCompanionError::BundleMismatch(
506 "exact toolchain libtest source changed after preparation".into(),
507 ));
508 }
509 if source_tree_digest_excluding(&destination, Some(SOURCE_IDENTITY_FILE))?
510 != identity.patched_source_sha256
511 {
512 return Err(RustLibtestCompanionError::BundleMismatch(
513 "prepared libtest source tree digest differs".into(),
514 ));
515 }
516 Ok(identity)
517}
518
519pub fn prepare_exact_libtest_source(
523 source_root: &Path,
524 destination: &Path,
525 compiler: &RustCompilerIdentity,
526) -> Result<RustLibtestCompanionSourceIdentity, RustLibtestCompanionError> {
527 let source_root = regular_directory(source_root)?;
528 let parent = destination
529 .parent()
530 .ok_or_else(|| RustLibtestCompanionError::UnsafeSource(destination.to_path_buf()))?;
531 let parent = regular_directory(parent)?;
532 let name = destination
533 .file_name()
534 .and_then(|name| name.to_str())
535 .filter(|name| !name.is_empty() && *name != "." && *name != "..")
536 .ok_or_else(|| RustLibtestCompanionError::UnsafeSource(destination.to_path_buf()))?;
537 let destination = parent.join(name);
538 let lock_path = parent.join(format!(".{name}.lock"));
539 let _lock = acquire_kernel_lock(&lock_path)?;
540 remove_owned_partials(&parent, &format!(".{name}."), true)?;
541 match fs::symlink_metadata(&destination) {
542 Ok(metadata) if metadata.file_type().is_dir() => {
543 return validate_prepared_libtest_source(&source_root, &destination, compiler);
544 }
545 Ok(_) => return Err(RustLibtestCompanionError::UnsafeSource(destination)),
546 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
547 Err(error) => return Err(io_error(&destination, error)),
548 }
549 let nonce = SystemTime::now()
550 .duration_since(UNIX_EPOCH)
551 .map_err(|error| io_error(&destination, error))?
552 .as_nanos();
553 let partial = parent.join(format!(".{name}.{}-{nonce}.partial", std::process::id()));
554 fs::create_dir(&partial).map_err(|error| io_error(&partial, error))?;
555 let mut cleanup = RemoveDirectoryOnDrop(Some(partial.clone()));
556
557 let original_source_sha256 = source_tree_digest(&source_root)?;
558 copy_regular_tree(&source_root, &partial)?;
559 if source_tree_digest(&source_root)? != original_source_sha256 {
560 return Err(RustLibtestCompanionError::BundleMismatch(
561 "exact toolchain libtest source changed while it was copied".into(),
562 ));
563 }
564 let lib = partial.join("src/lib.rs");
565 let console = partial.join("src/console.rs");
566 replace_once(&lib, LIB_ANCHOR, LIB_REPLACEMENT)?;
567 replace_once(&lib, IN_PROCESS_ANCHOR, IN_PROCESS_REPLACEMENT)?;
568 replace_once(&lib, SPAWNED_PROCESS_ANCHOR, SPAWNED_PROCESS_REPLACEMENT)?;
569 replace_once(&lib, BENCH_ANCHOR, BENCH_REPLACEMENT)?;
570 replace_once(&console, CONSOLE_ANCHOR, CONSOLE_REPLACEMENT)?;
571 replace_once(&console, LISTING_ANCHOR, LISTING_REPLACEMENT)?;
572 let event_runtime = rust_libtest_event_runtime_source();
573 let event_path = partial.join("src/supercov_events.rs");
574 let mut options = OpenOptions::new();
575 options.write(true).create_new(true);
576 #[cfg(unix)]
577 {
578 use std::os::unix::fs::OpenOptionsExt as _;
579 options.mode(0o600);
580 }
581 let mut event_file = options
582 .open(&event_path)
583 .map_err(|error| io_error(&event_path, error))?;
584 event_file
585 .write_all(event_runtime.as_bytes())
586 .and_then(|()| event_file.sync_all())
587 .map_err(|error| io_error(&event_path, error))?;
588
589 let identity = RustLibtestCompanionSourceIdentity {
590 event_protocol_version: RUST_LIBTEST_EVENT_PROTOCOL_VERSION,
591 rustc_commit_hash: compiler.rustc_commit_hash.clone(),
592 rustc_release: compiler.rustc_release.clone(),
593 host_triple: compiler.host_triple.clone(),
594 original_source_sha256,
595 event_runtime_sha256: format!("{:x}", Sha256::digest(event_runtime.as_bytes())),
596 patched_source_sha256: source_tree_digest(&partial)?,
597 };
598 let identity_path = partial.join(SOURCE_IDENTITY_FILE);
599 let mut identity_bytes =
600 serde_json::to_vec_pretty(&identity).map_err(|error| io_error(&identity_path, error))?;
601 identity_bytes.push(b'\n');
602 let mut identity_file = OpenOptions::new()
603 .write(true)
604 .create_new(true)
605 .open(&identity_path)
606 .map_err(|error| io_error(&identity_path, error))?;
607 identity_file
608 .write_all(&identity_bytes)
609 .and_then(|()| identity_file.sync_all())
610 .map_err(|error| io_error(&identity_path, error))?;
611 fs::rename(&partial, &destination).map_err(|error| io_error(&destination, error))?;
612 sync_directory(&parent)?;
613 cleanup.0 = None;
614 Ok(identity)
615}
616
617fn one_metadata(
618 directory: &Path,
619 crate_name: &'static str,
620) -> Result<PathBuf, RustLibtestCompanionError> {
621 let prefix = format!("lib{crate_name}-");
622 let mut matches = fs::read_dir(directory)
623 .map_err(|error| io_error(directory, error))?
624 .collect::<Result<Vec<_>, _>>()
625 .map_err(|error| io_error(directory, error))?
626 .into_iter()
627 .filter(|entry| {
628 let metadata = entry
629 .file_name()
630 .to_str()
631 .is_some_and(|name| name.starts_with(&prefix) && name.ends_with(".rmeta"));
632 let archive = entry.path().with_extension("rlib");
633 metadata
634 && entry.file_type().is_ok_and(|file_type| file_type.is_file())
635 && fs::symlink_metadata(archive)
636 .is_ok_and(|metadata| metadata.file_type().is_file())
637 })
638 .map(|entry| entry.path())
639 .collect::<Vec<_>>();
640 matches.sort();
641 matches.dedup();
642 match matches.as_slice() {
643 [path] => Ok(path.clone()),
644 _ => Err(RustLibtestCompanionError::DependencyMetadata {
645 directory: directory.to_path_buf(),
646 crate_name,
647 count: matches.len(),
648 }),
649 }
650}
651
652pub fn rust_libtest_companion_build_plan(
653 patched_source: &Path,
654 target_libdir: &Path,
655 output: &Path,
656) -> Result<RustLibtestCompanionBuildPlan, RustLibtestCompanionError> {
657 let patched_source = regular_directory(patched_source)?;
658 let target_libdir = regular_directory(target_libdir)?;
659 let source = patched_source.join("src/lib.rs");
660 if !fs::symlink_metadata(&source).is_ok_and(|metadata| metadata.file_type().is_file()) {
661 return Err(RustLibtestCompanionError::UnsafeSource(source));
662 }
663 let getopts = one_metadata(&target_libdir, "getopts")?;
664 let libc = one_metadata(&target_libdir, "libc")?;
665 let identity_path = patched_source.join("supercov-libtest-source.json");
666 let identity: RustLibtestCompanionSourceIdentity =
667 serde_json::from_slice(&read_regular_file(&identity_path)?).map_err(|error| {
668 RustLibtestCompanionError::InvalidBundle {
669 path: identity_path,
670 reason: error.to_string(),
671 }
672 })?;
673 if !canonical_lower_sha256(&identity.patched_source_sha256) {
674 return Err(RustLibtestCompanionError::BundleMismatch(
675 "patched libtest source identity is noncanonical".into(),
676 ));
677 }
678 Ok(RustLibtestCompanionBuildPlan {
679 source: source.clone(),
680 output: output.to_path_buf(),
681 arguments: vec![
682 source.into_os_string(),
683 "--crate-name".into(),
684 "test".into(),
685 "--crate-type".into(),
686 "rlib".into(),
687 "--edition".into(),
688 "2024".into(),
689 "-Zcrate-attr=feature(rustc_private)".into(),
690 format!(
691 "--remap-path-prefix={}=/supercov/libtest-source",
692 patched_source.display()
693 )
694 .into(),
695 format!(
696 "-Cmetadata=supercov_{}",
697 &identity.patched_source_sha256[..16]
698 )
699 .into(),
700 "-L".into(),
701 format!("dependency={}", target_libdir.display()).into(),
702 "--extern".into(),
703 format!("getopts={}", getopts.display()).into(),
704 "--extern".into(),
705 format!("libc={}", libc.display()).into(),
706 "-o".into(),
707 output.as_os_str().to_owned(),
708 ],
709 rustc_bootstrap: "1".into(),
710 })
711}
712
713fn canonical_archive_kind(
714 kind: ReadableArchiveKind,
715 host_triple: &str,
716) -> Result<WritableArchiveKind, RustLibtestCompanionError> {
717 match kind {
718 ReadableArchiveKind::Gnu => Ok(WritableArchiveKind::Gnu),
719 ReadableArchiveKind::Gnu64 => Ok(WritableArchiveKind::Gnu64),
720 ReadableArchiveKind::Bsd if host_triple.contains("-apple-") => {
721 Ok(WritableArchiveKind::Darwin)
722 }
723 ReadableArchiveKind::Bsd => Ok(WritableArchiveKind::Bsd),
724 ReadableArchiveKind::Bsd64 => Ok(WritableArchiveKind::Darwin64),
725 ReadableArchiveKind::Coff => Ok(WritableArchiveKind::Coff),
726 ReadableArchiveKind::AixBig => Ok(WritableArchiveKind::AixBig),
727 _ => Err(RustLibtestCompanionError::InvalidArchive(format!(
728 "unsupported archive kind {kind:?} for {host_triple}"
729 ))),
730 }
731}
732
733pub fn canonicalize_rust_libtest_rlib(
743 bytes: &[u8],
744 host_triple: &str,
745) -> Result<Vec<u8>, RustLibtestCompanionError> {
746 let archive = ArchiveFile::parse(bytes).map_err(|error| {
747 RustLibtestCompanionError::InvalidArchive(format!("cannot parse archive: {error}"))
748 })?;
749 if archive.is_thin() {
750 return Err(RustLibtestCompanionError::InvalidArchive(
751 "thin archives are not self-contained".into(),
752 ));
753 }
754 let archive_kind = canonical_archive_kind(archive.kind(), host_triple)?;
755
756 let mut metadata = None;
757 let mut objects = Vec::new();
758 for member in archive.members() {
759 let member = member.map_err(|error| {
760 RustLibtestCompanionError::InvalidArchive(format!(
761 "cannot parse archive member: {error}"
762 ))
763 })?;
764 let name = std::str::from_utf8(member.name()).map_err(|_| {
765 RustLibtestCompanionError::InvalidArchive(
766 "archive contains a non-UTF-8 member name".into(),
767 )
768 })?;
769 let data = member.data(bytes).map_err(|error| {
770 RustLibtestCompanionError::InvalidArchive(format!(
771 "cannot read archive member {name}: {error}"
772 ))
773 })?;
774 if name == "lib.rmeta" {
775 if metadata.replace(data.to_vec()).is_some() {
776 return Err(RustLibtestCompanionError::InvalidArchive(
777 "archive contains more than one lib.rmeta member".into(),
778 ));
779 }
780 } else if name.ends_with(".rcgu.o") {
781 objects.push((format!("{:x}", Sha256::digest(data)), data.to_vec()));
782 } else {
783 return Err(RustLibtestCompanionError::InvalidArchive(format!(
784 "unexpected archive member {name}"
785 )));
786 }
787 }
788 let metadata = metadata.ok_or_else(|| {
789 RustLibtestCompanionError::InvalidArchive("archive has no lib.rmeta member".into())
790 })?;
791 if objects.is_empty() {
792 return Err(RustLibtestCompanionError::InvalidArchive(
793 "archive has no codegen object members".into(),
794 ));
795 }
796 objects.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1)));
797
798 let mut owned_members = Vec::with_capacity(objects.len() + 1);
799 owned_members.push(("lib.rmeta".to_owned(), metadata));
800 for (ordinal, (digest, data)) in objects.into_iter().enumerate() {
801 owned_members.push((
802 format!("supercov-libtest-{ordinal:06}-{digest}.rcgu.o"),
803 data,
804 ));
805 }
806 let members = owned_members
807 .iter()
808 .map(|(name, data)| NewArchiveMember::new(data, &DEFAULT_OBJECT_READER, name.clone()))
809 .collect::<Vec<_>>();
810 let mut output = Cursor::new(Vec::new());
811 write_archive_to_stream(
812 &mut output,
813 &members,
814 archive_kind,
815 false,
816 Some(host_triple.contains("arm64ec")),
817 )
818 .map_err(|error| {
819 RustLibtestCompanionError::InvalidArchive(format!(
820 "cannot write canonical archive: {error}"
821 ))
822 })?;
823 let output = output.into_inner();
824 let reparsed = ArchiveFile::parse(output.as_slice()).map_err(|error| {
825 RustLibtestCompanionError::InvalidArchive(format!(
826 "canonical archive did not parse: {error}"
827 ))
828 })?;
829 if reparsed.is_thin() || reparsed.members().count() != owned_members.len() {
830 return Err(RustLibtestCompanionError::InvalidArchive(
831 "canonical archive failed structural verification".into(),
832 ));
833 }
834 Ok(output)
835}
836
837pub fn rust_libtest_companion_bundle_path(compiler_companion: &Path) -> PathBuf {
838 let mut value = compiler_companion.as_os_str().to_owned();
839 value.push(".libtest.json");
840 PathBuf::from(value)
841}
842
843fn canonical_lower_sha256(value: &str) -> bool {
844 value.len() == 64
845 && value
846 .bytes()
847 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
848}
849
850fn sha256_file(path: &Path) -> Result<String, RustLibtestCompanionError> {
851 Ok(format!("{:x}", Sha256::digest(read_regular_file(path)?)))
852}
853
854fn read_regular_file(path: &Path) -> Result<Vec<u8>, RustLibtestCompanionError> {
855 let metadata = fs::symlink_metadata(path).map_err(|error| io_error(path, error))?;
856 if !metadata.file_type().is_file() {
857 return Err(RustLibtestCompanionError::UnsafeSource(path.to_path_buf()));
858 }
859 let mut options = OpenOptions::new();
860 options.read(true);
861 #[cfg(any(target_os = "linux", target_os = "macos"))]
862 {
863 use std::os::unix::fs::OpenOptionsExt as _;
864 #[cfg(target_os = "linux")]
865 const O_NOFOLLOW: i32 = 0x2_0000;
866 #[cfg(target_os = "macos")]
867 const O_NOFOLLOW: i32 = 0x100;
868 options.custom_flags(O_NOFOLLOW);
869 }
870 let mut file = options.open(path).map_err(|error| io_error(path, error))?;
871 if !file
872 .metadata()
873 .map_err(|error| io_error(path, error))?
874 .file_type()
875 .is_file()
876 {
877 return Err(RustLibtestCompanionError::UnsafeSource(path.to_path_buf()));
878 }
879 let mut bytes = Vec::new();
880 std::io::Read::read_to_end(&mut file, &mut bytes).map_err(|error| io_error(path, error))?;
881 Ok(bytes)
882}
883
884fn safe_artifact_basename(value: &str) -> bool {
885 let path = Path::new(value);
886 !value.is_empty()
887 && value != "."
888 && value != ".."
889 && path.components().count() == 1
890 && path
891 .components()
892 .all(|component| matches!(component, Component::Normal(_)))
893}
894
895pub fn select_rust_libtest_companion(
900 selection: &SelectedRustCompilerCompanion,
901) -> Result<SelectedRustLibtestCompanion, RustLibtestCompanionError> {
902 let bundle_path = rust_libtest_companion_bundle_path(&selection.companion_path);
903 let bundle_metadata =
904 fs::symlink_metadata(&bundle_path).map_err(|error| io_error(&bundle_path, error))?;
905 if !bundle_metadata.file_type().is_file() {
906 return Err(RustLibtestCompanionError::UnsafeSource(bundle_path));
907 }
908 let bundle: RustLibtestCompanionBundle =
909 serde_json::from_slice(&read_regular_file(&bundle_path)?).map_err(|error| {
910 RustLibtestCompanionError::InvalidBundle {
911 path: bundle_path.clone(),
912 reason: error.to_string(),
913 }
914 })?;
915 if bundle.schema_version != RUST_LIBTEST_COMPANION_BUNDLE_SCHEMA_VERSION
916 || bundle.event_protocol_version != RUST_LIBTEST_EVENT_PROTOCOL_VERSION
917 || !safe_artifact_basename(&bundle.artifact_file)
918 || [
919 bundle.compiler_companion_build_id.as_str(),
920 bundle.original_source_sha256.as_str(),
921 bundle.event_runtime_sha256.as_str(),
922 bundle.patched_source_sha256.as_str(),
923 bundle.artifact_sha256.as_str(),
924 ]
925 .iter()
926 .any(|value| !canonical_lower_sha256(value))
927 {
928 return Err(RustLibtestCompanionError::InvalidBundle {
929 path: bundle_path,
930 reason: "unsupported schema/protocol, unsafe artifact name or noncanonical digest"
931 .into(),
932 });
933 }
934 if bundle.compiler_companion_build_id != selection.handshake.companion_build_id {
935 return Err(RustLibtestCompanionError::BundleMismatch(
936 "compiler companion build ID differs".into(),
937 ));
938 }
939 if bundle.rustc_commit_hash != selection.compiler.rustc_commit_hash
940 || bundle.host_triple != selection.compiler.host_triple
941 {
942 return Err(RustLibtestCompanionError::BundleMismatch(
943 "selected rustc identity differs".into(),
944 ));
945 }
946 let directory = selection.companion_path.parent().ok_or_else(|| {
947 RustLibtestCompanionError::BundleMismatch(
948 "compiler companion has no artifact directory".into(),
949 )
950 })?;
951 let artifact_path = directory.join(&bundle.artifact_file);
952 if sha256_file(&artifact_path)? != bundle.artifact_sha256 {
953 return Err(RustLibtestCompanionError::BundleMismatch(
954 "libtest artifact digest differs".into(),
955 ));
956 }
957 Ok(SelectedRustLibtestCompanion {
958 bundle_path,
959 artifact_path,
960 bundle,
961 })
962}
963
964pub fn write_rust_libtest_companion_bundle(
965 compiler_companion: &Path,
966 source_identity: &RustLibtestCompanionSourceIdentity,
967 artifact: &Path,
968) -> Result<PathBuf, RustLibtestCompanionError> {
969 let compiler_companion = fs::canonicalize(compiler_companion)
970 .map_err(|error| io_error(compiler_companion, error))?;
971 let artifact = fs::canonicalize(artifact).map_err(|error| io_error(artifact, error))?;
972 let directory = compiler_companion.parent().ok_or_else(|| {
973 RustLibtestCompanionError::BundleMismatch(
974 "compiler companion has no artifact directory".into(),
975 )
976 })?;
977 if artifact.parent() != Some(directory) {
978 return Err(RustLibtestCompanionError::BundleMismatch(
979 "libtest artifact is not adjacent to the compiler companion".into(),
980 ));
981 }
982 let artifact_file = artifact
983 .file_name()
984 .and_then(|value| value.to_str())
985 .filter(|value| safe_artifact_basename(value))
986 .ok_or_else(|| {
987 RustLibtestCompanionError::BundleMismatch(
988 "libtest artifact has an unsafe filename".into(),
989 )
990 })?
991 .to_owned();
992 let bundle = RustLibtestCompanionBundle {
993 schema_version: RUST_LIBTEST_COMPANION_BUNDLE_SCHEMA_VERSION,
994 event_protocol_version: source_identity.event_protocol_version,
995 compiler_companion_build_id: sha256_file(&compiler_companion)?,
996 rustc_commit_hash: source_identity.rustc_commit_hash.clone(),
997 host_triple: source_identity.host_triple.clone(),
998 original_source_sha256: source_identity.original_source_sha256.clone(),
999 event_runtime_sha256: source_identity.event_runtime_sha256.clone(),
1000 patched_source_sha256: source_identity.patched_source_sha256.clone(),
1001 artifact_file,
1002 artifact_sha256: sha256_file(&artifact)?,
1003 };
1004 let path = rust_libtest_companion_bundle_path(&compiler_companion);
1005 let mut bytes = serde_json::to_vec_pretty(&bundle).map_err(|error| io_error(&path, error))?;
1006 bytes.push(b'\n');
1007 let partial = path.with_file_name(format!(
1008 ".{}.{}-{}.partial",
1009 path.file_name()
1010 .and_then(|value| value.to_str())
1011 .unwrap_or("libtest"),
1012 std::process::id(),
1013 SystemTime::now()
1014 .duration_since(UNIX_EPOCH)
1015 .map_err(|error| io_error(&path, error))?
1016 .as_nanos()
1017 ));
1018 let mut cleanup = RemoveFileOnDrop(Some(partial.clone()));
1019 let mut options = OpenOptions::new();
1020 options.write(true).create_new(true);
1021 #[cfg(unix)]
1022 {
1023 use std::os::unix::fs::OpenOptionsExt as _;
1024 options.mode(0o600);
1025 }
1026 let mut file = options
1027 .open(&partial)
1028 .map_err(|error| io_error(&partial, error))?;
1029 file.write_all(&bytes)
1030 .and_then(|()| file.sync_all())
1031 .map_err(|error| io_error(&partial, error))?;
1032 drop(file);
1033 if let Ok(metadata) = fs::symlink_metadata(&path)
1034 && !metadata.file_type().is_file()
1035 {
1036 return Err(RustLibtestCompanionError::UnsafeSource(path));
1037 }
1038 fs::rename(&partial, &path).map_err(|error| io_error(&path, error))?;
1039 sync_directory(directory)?;
1040 cleanup.0 = None;
1041 Ok(path)
1042}
1043
1044fn libtest_builder_lock_path(compiler_companion: &Path) -> PathBuf {
1045 let mut value = compiler_companion.as_os_str().to_owned();
1046 value.push(".libtest.lock");
1047 PathBuf::from(value)
1048}
1049
1050fn bundle_matches_source_identity(
1051 selected: &SelectedRustLibtestCompanion,
1052 identity: &RustLibtestCompanionSourceIdentity,
1053) -> Result<(), RustLibtestCompanionError> {
1054 if selected.bundle.event_protocol_version != identity.event_protocol_version
1055 || selected.bundle.rustc_commit_hash != identity.rustc_commit_hash
1056 || selected.bundle.host_triple != identity.host_triple
1057 || selected.bundle.original_source_sha256 != identity.original_source_sha256
1058 || selected.bundle.event_runtime_sha256 != identity.event_runtime_sha256
1059 || selected.bundle.patched_source_sha256 != identity.patched_source_sha256
1060 {
1061 return Err(RustLibtestCompanionError::BundleMismatch(
1062 "published bundle differs from the authenticated prepared source".into(),
1063 ));
1064 }
1065 Ok(())
1066}
1067
1068pub fn build_exact_rust_libtest_companion(
1076 source_root: &Path,
1077 work_root: &Path,
1078 rustc: &Path,
1079 compiler_companion: &Path,
1080) -> Result<SelectedRustLibtestCompanion, RustLibtestCompanionError> {
1081 let rustc = fs::canonicalize(rustc).map_err(|error| io_error(rustc, error))?;
1082 let compiler = probe_rustc_identity(&rustc).map_err(|error| {
1083 RustLibtestCompanionError::BundleMismatch(format!(
1084 "could not authenticate exact rustc: {error}"
1085 ))
1086 })?;
1087 let work_root = regular_directory(work_root)?;
1088 let compiler_companion = fs::canonicalize(compiler_companion)
1089 .map_err(|error| io_error(compiler_companion, error))?;
1090 if !fs::symlink_metadata(&compiler_companion)
1091 .is_ok_and(|metadata| metadata.file_type().is_file())
1092 {
1093 return Err(RustLibtestCompanionError::UnsafeSource(compiler_companion));
1094 }
1095 let artifact_directory = compiler_companion.parent().ok_or_else(|| {
1096 RustLibtestCompanionError::BundleMismatch(
1097 "compiler companion has no artifact directory".into(),
1098 )
1099 })?;
1100 let lock_path = libtest_builder_lock_path(&compiler_companion);
1101 let builder_lock = acquire_kernel_lock(&lock_path)?;
1102 let selection =
1103 select_rust_compiler_companion(&rustc, std::slice::from_ref(&compiler_companion), false)
1104 .map_err(|error| {
1105 RustLibtestCompanionError::BundleMismatch(format!(
1106 "could not authenticate compiler companion: {error}"
1107 ))
1108 })?;
1109
1110 let bundle_path = rust_libtest_companion_bundle_path(&compiler_companion);
1111 let bundle_name = bundle_path
1112 .file_name()
1113 .and_then(|name| name.to_str())
1114 .ok_or_else(|| RustLibtestCompanionError::UnsafeSource(bundle_path.clone()))?;
1115 remove_owned_partials(artifact_directory, &format!(".{bundle_name}."), false)?;
1116
1117 let patched_source = work_root.join("patched-libtest");
1118 let source_identity = prepare_exact_libtest_source(source_root, &patched_source, &compiler)?;
1119 match fs::symlink_metadata(&bundle_path) {
1120 Ok(metadata) if metadata.file_type().is_file() => {
1121 let selected = select_rust_libtest_companion(&selection)?;
1122 bundle_matches_source_identity(&selected, &source_identity)?;
1123 return Ok(selected);
1124 }
1125 Ok(_) => return Err(RustLibtestCompanionError::UnsafeSource(bundle_path)),
1126 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1127 Err(error) => return Err(io_error(&bundle_path, error)),
1128 }
1129
1130 let target_libdir_output = Command::new(&rustc)
1131 .args(["--print", "target-libdir"])
1132 .env_remove("RUSTC_WRAPPER")
1133 .env_remove("RUSTC_WORKSPACE_WRAPPER")
1134 .output()
1135 .map_err(|error| io_error(&rustc, error))?;
1136 if !target_libdir_output.status.success() || !target_libdir_output.stderr.is_empty() {
1137 return Err(RustLibtestCompanionError::BuildFailed {
1138 program: rustc,
1139 status: target_libdir_output.status.code(),
1140 stdout: String::from_utf8_lossy(&target_libdir_output.stdout).into_owned(),
1141 stderr: String::from_utf8_lossy(&target_libdir_output.stderr).into_owned(),
1142 });
1143 }
1144 let target_libdir = PathBuf::from(
1145 std::str::from_utf8(&target_libdir_output.stdout)
1146 .map_err(|_| {
1147 RustLibtestCompanionError::BundleMismatch("rustc target libdir is not UTF-8".into())
1148 })?
1149 .trim(),
1150 );
1151 let artifact_name = format!(
1152 "libtest-supercov-v{}-{}-{}.rlib",
1153 RUST_LIBTEST_COMPANION_BUNDLE_SCHEMA_VERSION,
1154 &source_identity.rustc_commit_hash[..12],
1155 &source_identity.patched_source_sha256[..12]
1156 );
1157 let artifact = artifact_directory.join(&artifact_name);
1158 remove_owned_partials(artifact_directory, &format!(".{artifact_name}."), false)?;
1159
1160 let build_output = work_root.join(&artifact_name);
1164 remove_owned_path(&build_output, false)?;
1165 let mut build_cleanup = RemoveFileOnDrop(Some(build_output.clone()));
1166 let plan = rust_libtest_companion_build_plan(&patched_source, &target_libdir, &build_output)?;
1167 let mut command = Command::new(&rustc);
1168 command
1169 .args(&plan.arguments)
1170 .env("RUSTC_BOOTSTRAP", &plan.rustc_bootstrap)
1171 .env_remove("RUSTC_WRAPPER")
1172 .env_remove("RUSTC_WORKSPACE_WRAPPER")
1173 .env_remove(crate::rust_compiler_orchestration::RUST_COMPILER_WRAPPER_CONFIG_ENV)
1174 .env_remove(crate::rust_compiler_orchestration::RUST_COMPILER_INNER_MODE_ENV);
1175 inherit_lock_through_exec(&mut command, &builder_lock);
1176 let output = command.output().map_err(|error| io_error(&rustc, error))?;
1177 if !output.status.success() {
1178 return Err(RustLibtestCompanionError::BuildFailed {
1179 program: rustc,
1180 status: output.status.code(),
1181 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
1182 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
1183 });
1184 }
1185 let compiled = read_regular_file(&build_output)?;
1186 let canonical = canonicalize_rust_libtest_rlib(&compiled, &compiler.host_triple)?;
1187 let partial = artifact_directory.join(format!(
1188 ".{artifact_name}.{}-{}.partial",
1189 std::process::id(),
1190 SystemTime::now()
1191 .duration_since(UNIX_EPOCH)
1192 .map_err(|error| io_error(&artifact, error))?
1193 .as_nanos()
1194 ));
1195 let mut partial_cleanup = RemoveFileOnDrop(Some(partial.clone()));
1196 let mut options = OpenOptions::new();
1197 options.write(true).create_new(true);
1198 #[cfg(unix)]
1199 {
1200 use std::os::unix::fs::OpenOptionsExt as _;
1201 options.mode(0o600);
1202 }
1203 let mut partial_file = options
1204 .open(&partial)
1205 .map_err(|error| io_error(&partial, error))?;
1206 partial_file
1207 .write_all(&canonical)
1208 .and_then(|()| partial_file.sync_all())
1209 .map_err(|error| io_error(&partial, error))?;
1210 drop(partial_file);
1211
1212 match fs::symlink_metadata(&artifact) {
1213 Ok(metadata) if metadata.file_type().is_file() => {
1214 if read_regular_file(&artifact)? != canonical {
1215 return Err(RustLibtestCompanionError::BundleMismatch(
1216 "the same exact libtest identity produced different artifact bytes".into(),
1217 ));
1218 }
1219 fs::remove_file(&partial).map_err(|error| io_error(&partial, error))?;
1220 partial_cleanup.0 = None;
1221 }
1222 Ok(_) => return Err(RustLibtestCompanionError::UnsafeSource(artifact)),
1223 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
1224 fs::rename(&partial, &artifact).map_err(|error| io_error(&artifact, error))?;
1225 sync_directory(artifact_directory)?;
1226 partial_cleanup.0 = None;
1227 }
1228 Err(error) => return Err(io_error(&artifact, error)),
1229 }
1230 fs::remove_file(&build_output).map_err(|error| io_error(&build_output, error))?;
1231 build_cleanup.0 = None;
1232
1233 let published_bundle =
1234 write_rust_libtest_companion_bundle(&compiler_companion, &source_identity, &artifact)?;
1235 let selected = select_rust_libtest_companion(&selection)?;
1236 bundle_matches_source_identity(&selected, &source_identity)?;
1237 if selected.bundle_path != published_bundle || selected.artifact_path != artifact {
1238 return Err(RustLibtestCompanionError::BundleMismatch(
1239 "published libtest companion did not reselect exactly".into(),
1240 ));
1241 }
1242 Ok(selected)
1243}
1244
1245#[cfg(test)]
1246mod tests {
1247 use std::{
1248 sync::atomic::{AtomicU64, Ordering},
1249 time::{SystemTime, UNIX_EPOCH},
1250 };
1251
1252 use super::*;
1253 use supercov_contracts::{
1254 EVIDENCE_ARCHIVE_SCHEMA_VERSION, RUST_COMPILER_COMPANION_PROTOCOL_VERSION,
1255 RustCompilerCompanionCapabilities, RustCompilerCompanionHandshake,
1256 };
1257
1258 fn compiler() -> RustCompilerIdentity {
1259 RustCompilerIdentity {
1260 rustc_commit_hash: "a".repeat(40),
1261 rustc_release: "1.95.0".into(),
1262 host_triple: "aarch64-apple-darwin".into(),
1263 rustc_driver_sha256: "b".repeat(64),
1264 }
1265 }
1266
1267 fn selection(
1268 compiler_companion: PathBuf,
1269 compiler: RustCompilerIdentity,
1270 ) -> SelectedRustCompilerCompanion {
1271 let build_id = sha256_file(&compiler_companion).unwrap();
1272 SelectedRustCompilerCompanion {
1273 rustc_path: compiler_companion.with_file_name("rustc"),
1274 compiler_library_directory: compiler_companion.parent().unwrap().to_path_buf(),
1275 companion_path: compiler_companion,
1276 compiler: compiler.clone(),
1277 handshake: RustCompilerCompanionHandshake {
1278 protocol_version: RUST_COMPILER_COMPANION_PROTOCOL_VERSION,
1279 frontend_id: "rust".into(),
1280 coverage_model_variant: "rust-source-v1".into(),
1281 evidence_schema_version: EVIDENCE_ARCHIVE_SCHEMA_VERSION,
1282 companion_build_id: build_id,
1283 compiler,
1284 capabilities: RustCompilerCompanionCapabilities {
1285 expanded_hir_provenance: true,
1286 runtime_mir_probe_insertion: true,
1287 generated_source_provenance: true,
1288 ctfe_path_tracing: true,
1289 rustdoc_doctest_tracing: true,
1290 exact_test_harness_attribution: true,
1291 },
1292 },
1293 }
1294 }
1295
1296 fn fixture() -> PathBuf {
1297 static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
1298 let nonce = SystemTime::now()
1299 .duration_since(UNIX_EPOCH)
1300 .unwrap()
1301 .as_nanos();
1302 let root = std::env::temp_dir().join(format!(
1303 "supercov-libtest-source-{}-{nonce}-{}",
1304 std::process::id(),
1305 NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed)
1306 ));
1307 fs::create_dir_all(root.join("source/src")).unwrap();
1308 fs::write(root.join("source/Cargo.toml"), b"[package]\nname='test'\n").unwrap();
1309 fs::write(root.join("source/src/lib.rs"), fixture_lib_source()).unwrap();
1310 fs::write(
1311 root.join("source/src/console.rs"),
1312 format!("use std::io;\n{CONSOLE_ANCHOR}\n }}\n}}\n{LISTING_ANCHOR}\n}}\n"),
1313 )
1314 .unwrap();
1315 root
1316 }
1317
1318 fn fixture_lib_source() -> String {
1319 format!(
1320 "#![feature(test)]\n{LIB_ANCHOR}\n{IN_PROCESS_ANCHOR} synthetic;\n{SPAWNED_PROCESS_ANCHOR};\n{BENCH_ANCHOR}(synthetic);\n"
1321 )
1322 }
1323
1324 #[test]
1325 fn patches_atomically_with_relocation_stable_identity() {
1326 let first = fixture();
1327 let second = fixture();
1328 let first_identity = prepare_exact_libtest_source(
1329 &first.join("source"),
1330 &first.join("patched"),
1331 &compiler(),
1332 )
1333 .unwrap();
1334 let second_identity = prepare_exact_libtest_source(
1335 &second.join("source"),
1336 &second.join("patched"),
1337 &compiler(),
1338 )
1339 .unwrap();
1340 assert_eq!(first_identity, second_identity);
1341 assert!(
1342 fs::read_to_string(first.join("patched/src/lib.rs"))
1343 .unwrap()
1344 .contains("mod supercov_events;")
1345 );
1346 assert_eq!(
1347 fs::read_to_string(first.join("patched/src/lib.rs"))
1348 .unwrap()
1349 .matches("supercov_events::enter_test")
1350 .count(),
1351 3
1352 );
1353 assert!(
1354 fs::read_to_string(first.join("patched/src/console.rs"))
1355 .unwrap()
1356 .contains("crate::supercov_events::emit(event)?;")
1357 );
1358 assert!(
1359 fs::read_to_string(first.join("patched/src/console.rs"))
1360 .unwrap()
1361 .contains("crate::supercov_events::emit_listing(")
1362 );
1363 assert_eq!(
1364 fs::read_to_string(first.join("source/src/lib.rs")).unwrap(),
1365 fixture_lib_source()
1366 );
1367 assert!(first.join("patched/supercov-libtest-source.json").is_file());
1368 assert_eq!(
1369 prepare_exact_libtest_source(
1370 &first.join("source"),
1371 &first.join("patched"),
1372 &compiler()
1373 )
1374 .unwrap(),
1375 first_identity
1376 );
1377 fs::remove_dir_all(first).unwrap();
1378 fs::remove_dir_all(second).unwrap();
1379 }
1380
1381 #[test]
1382 fn prepared_source_reuse_fails_closed_on_tree_or_identity_tampering() {
1383 let root = fixture();
1384 let source = root.join("source");
1385 let patched = root.join("patched");
1386 prepare_exact_libtest_source(&source, &patched, &compiler()).unwrap();
1387 fs::write(patched.join("src/lib.rs"), b"tampered\n").unwrap();
1388 assert!(matches!(
1389 prepare_exact_libtest_source(&source, &patched, &compiler()),
1390 Err(RustLibtestCompanionError::BundleMismatch(reason))
1391 if reason.contains("tree digest")
1392 ));
1393
1394 fs::remove_dir_all(&patched).unwrap();
1395 prepare_exact_libtest_source(&source, &patched, &compiler()).unwrap();
1396 let identity_path = patched.join(SOURCE_IDENTITY_FILE);
1397 let mut identity: serde_json::Value =
1398 serde_json::from_slice(&fs::read(&identity_path).unwrap()).unwrap();
1399 identity["unknown"] = serde_json::json!(true);
1400 fs::write(&identity_path, serde_json::to_vec(&identity).unwrap()).unwrap();
1401 assert!(matches!(
1402 prepare_exact_libtest_source(&source, &patched, &compiler()),
1403 Err(RustLibtestCompanionError::InvalidBundle { .. })
1404 ));
1405 fs::remove_dir_all(root).unwrap();
1406 }
1407
1408 #[test]
1409 fn concurrent_source_preparation_converges_without_partial_debris() {
1410 let root = fixture();
1411 let source = root.join("source");
1412 let patched = root.join("patched");
1413 let workers = (0..8)
1414 .map(|_| {
1415 let source = source.clone();
1416 let patched = patched.clone();
1417 std::thread::spawn(move || {
1418 prepare_exact_libtest_source(&source, &patched, &compiler()).unwrap()
1419 })
1420 })
1421 .collect::<Vec<_>>();
1422 let identities = workers
1423 .into_iter()
1424 .map(|worker| worker.join().unwrap())
1425 .collect::<Vec<_>>();
1426 assert!(identities.windows(2).all(|pair| pair[0] == pair[1]));
1427 assert!(fs::read_dir(&root).unwrap().all(|entry| {
1428 !entry
1429 .unwrap()
1430 .file_name()
1431 .to_string_lossy()
1432 .ends_with(".partial")
1433 }));
1434 fs::remove_dir_all(root).unwrap();
1435 }
1436
1437 #[cfg(unix)]
1438 #[test]
1439 fn libtest_builder_lock_holder_helper() {
1440 let Some(lock) = std::env::var_os("SUPERCOV_TEST_LIBTEST_LOCK") else {
1441 return;
1442 };
1443 let partial =
1444 PathBuf::from(std::env::var_os("SUPERCOV_TEST_LIBTEST_PARTIAL").expect("partial path"));
1445 let ready =
1446 PathBuf::from(std::env::var_os("SUPERCOV_TEST_LIBTEST_READY").expect("ready path"));
1447 let _lock = acquire_kernel_lock(Path::new(&lock)).unwrap();
1448 fs::write(&partial, b"incomplete\n").unwrap();
1449 fs::write(&ready, b"locked\n").unwrap();
1450 loop {
1451 std::thread::sleep(Duration::from_secs(1));
1452 }
1453 }
1454
1455 #[cfg(unix)]
1456 #[test]
1457 fn killed_builder_releases_lock_and_owned_partial_is_recoverable() {
1458 use std::process::Stdio;
1459
1460 let root = fixture();
1461 let lock = root.join("companion.libtest.lock");
1462 let partial = root.join(".artifact.123.partial");
1463 let ready = root.join("builder-ready");
1464 let mut child = Command::new(std::env::current_exe().unwrap())
1465 .args([
1466 "--exact",
1467 "rust_libtest_companion::tests::libtest_builder_lock_holder_helper",
1468 "--nocapture",
1469 ])
1470 .env("SUPERCOV_TEST_LIBTEST_LOCK", &lock)
1471 .env("SUPERCOV_TEST_LIBTEST_PARTIAL", &partial)
1472 .env("SUPERCOV_TEST_LIBTEST_READY", &ready)
1473 .stdin(Stdio::null())
1474 .stdout(Stdio::null())
1475 .stderr(Stdio::null())
1476 .spawn()
1477 .unwrap();
1478 let started = Instant::now();
1479 while !ready.is_file() {
1480 assert!(
1481 started.elapsed() < Duration::from_secs(10),
1482 "libtest builder helper did not acquire its kernel lock"
1483 );
1484 std::thread::sleep(Duration::from_millis(10));
1485 }
1486 assert_eq!(
1487 unsafe { libc::kill(child.id().try_into().unwrap(), libc::SIGKILL) },
1488 0
1489 );
1490 assert_eq!(child.wait().unwrap().code(), None);
1491 let recovery_started = Instant::now();
1492 let _lock = acquire_kernel_lock(&lock).unwrap();
1493 assert!(recovery_started.elapsed() < Duration::from_secs(5));
1494 remove_owned_partials(&root, ".artifact.", false).unwrap();
1495 assert!(!partial.exists());
1496 fs::remove_dir_all(root).unwrap();
1497 }
1498
1499 #[cfg(unix)]
1500 #[test]
1501 fn libtest_builder_child_lock_holder_helper() {
1502 let Some(lock) = std::env::var_os("SUPERCOV_TEST_LIBTEST_CHILD_LOCK") else {
1503 return;
1504 };
1505 let ready = PathBuf::from(
1506 std::env::var_os("SUPERCOV_TEST_LIBTEST_CHILD_READY").expect("ready path"),
1507 );
1508 let lock = acquire_kernel_lock(Path::new(&lock)).unwrap();
1509 let mut command = Command::new("/bin/sh");
1510 command.args(["-c", "sleep 2"]);
1511 inherit_lock_through_exec(&mut command, &lock);
1512 let child = command.spawn().unwrap();
1513 fs::write(&ready, format!("{}\n", child.id())).unwrap();
1514 drop(child);
1518 loop {
1519 std::thread::sleep(Duration::from_secs(1));
1520 }
1521 }
1522
1523 #[cfg(unix)]
1524 #[test]
1525 fn killed_builder_keeps_lock_until_its_compiler_child_exits() {
1526 use std::process::Stdio;
1527
1528 let root = fixture();
1529 let lock = root.join("companion-child.libtest.lock");
1530 let ready = root.join("compiler-ready");
1531 let mut builder = Command::new(std::env::current_exe().unwrap())
1532 .args([
1533 "--exact",
1534 "rust_libtest_companion::tests::libtest_builder_child_lock_holder_helper",
1535 "--nocapture",
1536 ])
1537 .env("SUPERCOV_TEST_LIBTEST_CHILD_LOCK", &lock)
1538 .env("SUPERCOV_TEST_LIBTEST_CHILD_READY", &ready)
1539 .stdin(Stdio::null())
1540 .stdout(Stdio::null())
1541 .stderr(Stdio::null())
1542 .spawn()
1543 .unwrap();
1544 let started = Instant::now();
1545 while !ready.is_file() {
1546 assert!(started.elapsed() < Duration::from_secs(10));
1547 std::thread::sleep(Duration::from_millis(10));
1548 }
1549 assert_eq!(
1550 unsafe { libc::kill(builder.id().try_into().unwrap(), libc::SIGKILL) },
1551 0
1552 );
1553 assert_eq!(builder.wait().unwrap().code(), None);
1554 let recovery_started = Instant::now();
1555 let _lock = acquire_kernel_lock(&lock).unwrap();
1556 assert!(
1557 recovery_started.elapsed() >= Duration::from_millis(500),
1558 "the compiler child did not retain the publication lock"
1559 );
1560 assert!(recovery_started.elapsed() < Duration::from_secs(5));
1561 fs::remove_dir_all(root).unwrap();
1562 }
1563
1564 #[test]
1565 fn rejects_unrecognized_or_unsafe_exact_source() {
1566 let root = fixture();
1567 fs::write(root.join("source/src/console.rs"), "not libtest\n").unwrap();
1568 assert!(matches!(
1569 prepare_exact_libtest_source(&root.join("source"), &root.join("patched"), &compiler()),
1570 Err(RustLibtestCompanionError::UnrecognizedSource { .. })
1571 ));
1572 assert!(!root.join("patched").exists());
1573 assert_eq!(
1574 fs::read_to_string(root.join("source/src/console.rs")).unwrap(),
1575 "not libtest\n"
1576 );
1577 fs::remove_dir_all(root).unwrap();
1578 }
1579
1580 #[cfg(unix)]
1581 #[test]
1582 fn rejects_symlinks_without_leaving_a_destination() {
1583 use std::os::unix::fs::symlink;
1584
1585 let root = fixture();
1586 symlink("lib.rs", root.join("source/src/alias.rs")).unwrap();
1587 assert!(matches!(
1588 prepare_exact_libtest_source(&root.join("source"), &root.join("patched"), &compiler()),
1589 Err(RustLibtestCompanionError::UnsafeSource(_))
1590 ));
1591 assert!(!root.join("patched").exists());
1592 fs::remove_dir_all(root).unwrap();
1593 }
1594
1595 #[test]
1596 fn build_plan_requires_exact_full_metadata() {
1597 let root = fixture();
1598 prepare_exact_libtest_source(&root.join("source"), &root.join("patched"), &compiler())
1599 .unwrap();
1600 fs::create_dir(root.join("libdir")).unwrap();
1601 fs::write(root.join("libdir/libgetopts-a.rmeta"), b"getopts").unwrap();
1602 fs::write(root.join("libdir/libgetopts-a.rlib"), b"getopts archive").unwrap();
1603 fs::write(root.join("libdir/liblibc-b.rmeta"), b"libc").unwrap();
1604 fs::write(root.join("libdir/liblibc-b.rlib"), b"libc archive").unwrap();
1605 let plan = rust_libtest_companion_build_plan(
1606 &root.join("patched"),
1607 &root.join("libdir"),
1608 &root.join("libtest-supercov.rlib"),
1609 )
1610 .unwrap();
1611 assert_eq!(
1612 plan.source,
1613 fs::canonicalize(root.join("patched/src/lib.rs")).unwrap()
1614 );
1615 assert!(
1616 plan.arguments
1617 .iter()
1618 .any(|value| value == "-Zcrate-attr=feature(rustc_private)")
1619 );
1620 assert!(
1621 !plan
1622 .arguments
1623 .iter()
1624 .any(|value| value.to_string_lossy().starts_with("-Cincremental"))
1625 );
1626 fs::write(root.join("libdir/liblibc-c.rmeta"), b"duplicate").unwrap();
1627 fs::write(root.join("libdir/liblibc-c.rlib"), b"duplicate archive").unwrap();
1628 assert!(
1629 rust_libtest_companion_build_plan(
1630 &root.join("patched"),
1631 &root.join("libdir"),
1632 &root.join("duplicate.rlib")
1633 )
1634 .is_err()
1635 );
1636 fs::remove_dir_all(root).unwrap();
1637 }
1638
1639 fn synthetic_rlib(object_suffix: &str, reverse: bool) -> Vec<u8> {
1640 let metadata = b"metadata".as_slice();
1641 let first = b"first object".as_slice();
1642 let second = b"second object".as_slice();
1643 let first_name = format!("test.alpha.{object_suffix}.rcgu.o");
1644 let second_name = format!("test.beta.{object_suffix}.rcgu.o");
1645 let mut owned = [
1646 ("lib.rmeta".to_owned(), metadata),
1647 (first_name, first),
1648 (second_name, second),
1649 ];
1650 if reverse {
1651 owned[1..].reverse();
1652 }
1653 let members = owned
1654 .iter()
1655 .map(|(name, data)| NewArchiveMember::new(data, &DEFAULT_OBJECT_READER, name.clone()))
1656 .collect::<Vec<_>>();
1657 let mut output = Cursor::new(Vec::new());
1658 write_archive_to_stream(
1659 &mut output,
1660 &members,
1661 WritableArchiveKind::Darwin,
1662 false,
1663 Some(false),
1664 )
1665 .unwrap();
1666 output.into_inner()
1667 }
1668
1669 #[test]
1670 fn canonical_rlib_ignores_session_names_and_member_order() {
1671 let first = canonicalize_rust_libtest_rlib(
1672 &synthetic_rlib("random-one", false),
1673 "aarch64-apple-darwin",
1674 )
1675 .unwrap();
1676 let second = canonicalize_rust_libtest_rlib(
1677 &synthetic_rlib("random-two", true),
1678 "aarch64-apple-darwin",
1679 )
1680 .unwrap();
1681 assert_eq!(first, second);
1682
1683 let archive = ArchiveFile::parse(first.as_slice()).unwrap();
1684 let names = archive
1685 .members()
1686 .map(|member| {
1687 std::str::from_utf8(member.unwrap().name())
1688 .unwrap()
1689 .to_owned()
1690 })
1691 .collect::<Vec<_>>();
1692 assert_eq!(names.first().map(String::as_str), Some("lib.rmeta"));
1693 assert!(
1694 names[1..]
1695 .iter()
1696 .all(|name| name.starts_with("supercov-libtest-") && name.ends_with(".rcgu.o"))
1697 );
1698 }
1699
1700 #[test]
1701 fn bundle_binds_exact_compiler_source_runtime_and_artifact_bytes() {
1702 let root = fixture();
1703 let compiler = compiler();
1704 let source_identity =
1705 prepare_exact_libtest_source(&root.join("source"), &root.join("patched"), &compiler)
1706 .unwrap();
1707 let compiler_companion = root.join("supercov-rustc-companion");
1708 let artifact = root.join("libtest-supercov.rlib");
1709 fs::write(&compiler_companion, b"compiler companion").unwrap();
1710 fs::write(&artifact, b"exact libtest rlib").unwrap();
1711 let selected = selection(
1712 fs::canonicalize(&compiler_companion).unwrap(),
1713 compiler.clone(),
1714 );
1715 let bundle_path =
1716 write_rust_libtest_companion_bundle(&compiler_companion, &source_identity, &artifact)
1717 .unwrap();
1718 let bound = select_rust_libtest_companion(&selected).unwrap();
1719 assert_eq!(bound.bundle_path, bundle_path);
1720 assert_eq!(bound.artifact_path, fs::canonicalize(&artifact).unwrap());
1721 assert_eq!(
1722 bound.bundle.compiler_companion_build_id,
1723 selected.handshake.companion_build_id
1724 );
1725 assert_eq!(
1726 bound.bundle.original_source_sha256,
1727 source_identity.original_source_sha256
1728 );
1729
1730 fs::write(&artifact, b"tampered").unwrap();
1731 assert!(matches!(
1732 select_rust_libtest_companion(&selected),
1733 Err(RustLibtestCompanionError::BundleMismatch(_))
1734 ));
1735 fs::remove_dir_all(root).unwrap();
1736 }
1737
1738 #[test]
1739 fn bundle_rejects_unknown_fields_unsafe_paths_and_companion_mismatch() {
1740 let root = fixture();
1741 let compiler = compiler();
1742 let source_identity =
1743 prepare_exact_libtest_source(&root.join("source"), &root.join("patched"), &compiler)
1744 .unwrap();
1745 let compiler_companion = root.join("supercov-rustc-companion");
1746 let artifact = root.join("libtest-supercov.rlib");
1747 fs::write(&compiler_companion, b"compiler companion").unwrap();
1748 fs::write(&artifact, b"exact libtest rlib").unwrap();
1749 let mut selected = selection(
1750 fs::canonicalize(&compiler_companion).unwrap(),
1751 compiler.clone(),
1752 );
1753 let bundle_path =
1754 write_rust_libtest_companion_bundle(&compiler_companion, &source_identity, &artifact)
1755 .unwrap();
1756 let mut value: serde_json::Value =
1757 serde_json::from_slice(&fs::read(&bundle_path).unwrap()).unwrap();
1758 value["unknown"] = serde_json::json!(true);
1759 fs::write(&bundle_path, serde_json::to_vec(&value).unwrap()).unwrap();
1760 assert!(matches!(
1761 select_rust_libtest_companion(&selected),
1762 Err(RustLibtestCompanionError::InvalidBundle { .. })
1763 ));
1764
1765 value.as_object_mut().unwrap().remove("unknown");
1766 value["artifactFile"] = serde_json::json!("../escaped.rlib");
1767 fs::write(&bundle_path, serde_json::to_vec(&value).unwrap()).unwrap();
1768 assert!(matches!(
1769 select_rust_libtest_companion(&selected),
1770 Err(RustLibtestCompanionError::InvalidBundle { .. })
1771 ));
1772
1773 value["artifactFile"] = serde_json::json!("libtest-supercov.rlib");
1774 fs::write(&bundle_path, serde_json::to_vec(&value).unwrap()).unwrap();
1775 selected.handshake.companion_build_id = "f".repeat(64);
1776 assert!(matches!(
1777 select_rust_libtest_companion(&selected),
1778 Err(RustLibtestCompanionError::BundleMismatch(_))
1779 ));
1780 fs::remove_dir_all(root).unwrap();
1781 }
1782}