diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs b/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs index 2d22ca9ff7..6ac380da5d 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs @@ -15,7 +15,7 @@ use std::os::unix::process::CommandExt; use std::os::fd::AsRawFd; #[cfg(target_os = "macos")] -use std::os::unix::fs::{DirBuilderExt, FileExt, MetadataExt, OpenOptionsExt, PermissionsExt}; +use std::os::unix::fs::{FileExt, MetadataExt, OpenOptionsExt, PermissionsExt}; #[cfg(target_os = "macos")] use uuid::Uuid; @@ -290,7 +290,6 @@ struct InheritedCommand { #[cfg(target_os = "macos")] struct TemporaryExecutable { path: PathBuf, - directory: PathBuf, _file: File, } @@ -298,7 +297,6 @@ struct TemporaryExecutable { impl Drop for TemporaryExecutable { fn drop(&mut self) { let _ = fs::remove_file(&self.path); - let _ = fs::remove_dir(&self.directory); } } @@ -306,29 +304,31 @@ impl Drop for TemporaryExecutable { fn materialize_executable( artifact: &VerifiedProcessArtifact, ) -> Result { - let directory = std::env::temp_dir().join(format!( + let directory = artifact.display_path.parent().ok_or_else(|| { + LocalRunnerError::invalid(format!( + "verified process artifact {} has no parent directory", + artifact.display_path.display() + )) + })?; + let directory_metadata = fs::symlink_metadata(directory) + .map_err(|error| snapshot_error(&artifact.display_path, error))?; + if !directory_metadata.is_dir() || directory_metadata.permissions().mode() & 0o022 != 0 { + return Err(LocalRunnerError::invalid(format!( + "verified process artifact directory {} must be a directory that is not group- or world-writable", + directory.display() + ))); + } + let path = directory.join(format!( ".paperclip-verified-executable-{}", Uuid::new_v4().simple() )); - let mut directory_builder = fs::DirBuilder::new(); - directory_builder.mode(0o700); - directory_builder - .create(&directory) - .map_err(|error| snapshot_error(&artifact.display_path, error))?; - let path = directory.join("launch"); - let writable = OpenOptions::new() + let mut writable = OpenOptions::new() .read(true) .write(true) .create_new(true) .mode(0o700) - .open(&path); - let mut writable = match writable { - Ok(file) => file, - Err(error) => { - let _ = fs::remove_dir(&directory); - return Err(snapshot_error(&artifact.display_path, error)); - } - }; + .open(&path) + .map_err(|error| snapshot_error(&artifact.display_path, error))?; let result = (|| { let length = artifact .file @@ -375,13 +375,11 @@ fn materialize_executable( drop(writable); Ok(TemporaryExecutable { path: path.clone(), - directory: directory.clone(), _file: file, }) })(); if result.is_err() { let _ = fs::remove_file(path); - let _ = fs::remove_dir(directory); } result } diff --git a/packages/paperclip-runner/runner/crates/runner-core/tests/process_supervisor.rs b/packages/paperclip-runner/runner/crates/runner-core/tests/process_supervisor.rs index b871c7bfae..d1fac57753 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/tests/process_supervisor.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/tests/process_supervisor.rs @@ -1,6 +1,8 @@ #![cfg(unix)] use std::fs::{self, File}; +#[cfg(target_os = "macos")] +use std::io::Read; use std::io::Write; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; @@ -35,6 +37,21 @@ fn sha256(contents: &str) -> String { format!("sha256:{:x}", Sha256::digest(contents.as_bytes())) } +#[cfg(target_os = "macos")] +fn sha256_file(path: &Path) -> String { + let mut file = File::open(path).unwrap(); + let mut digest = Sha256::new(); + let mut buffer = [0_u8; 64 * 1024]; + loop { + let count = file.read(&mut buffer).unwrap(); + if count == 0 { + break; + } + digest.update(&buffer[..count]); + } + format!("sha256:{:x}", digest.finalize()) +} + #[test] fn verified_launch_uses_open_command_and_script_after_atomic_path_replacement() { let directory = std::env::temp_dir().join(format!( @@ -105,7 +122,14 @@ fn verified_launch_uses_open_command_and_script_after_atomic_path_replacement() #[cfg(target_os = "linux")] assert!(inherited_runtime.starts_with("/proc/self/fd/")); #[cfg(target_os = "macos")] - assert!(inherited_runtime.contains(".paperclip-verified-executable-")); + { + assert!(inherited_runtime.contains(".paperclip-verified-executable-")); + assert_eq!( + Path::new(&inherited_runtime).parent(), + command.parent(), + "macOS verified launches must preserve loader-relative runtime layout" + ); + } assert_eq!( process .receive_stdout_line(Duration::from_secs(1)) @@ -117,6 +141,55 @@ fn verified_launch_uses_open_command_and_script_after_atomic_path_replacement() fs::remove_dir_all(directory).unwrap(); } +#[cfg(target_os = "macos")] +#[test] +fn verified_launch_preserves_homebrew_node_loader_layout() { + let resolved = Command::new("/usr/bin/which") + .arg("node") + .output() + .expect("node lookup should run"); + assert!( + resolved.status.success(), + "node should be available on PATH" + ); + let node = fs::canonicalize( + String::from_utf8(resolved.stdout) + .expect("node path should be UTF-8") + .trim(), + ) + .expect("node path should resolve"); + let launch = VerifiedProcessLaunch::new( + VerifiedProcessArtifact::snapshot_verified( + node.clone(), + File::open(&node).unwrap(), + &sha256_file(&node), + ) + .unwrap(), + vec![ + VerifiedProcessArgument::Literal("--eval".to_owned()), + VerifiedProcessArgument::Literal("console.log(process.execPath)".to_owned()), + ], + ); + + let mut process = SupervisedProcess::spawn_verified_with_environment_keys( + &launch, + Duration::from_millis(50), + 1024, + &[], + ) + .expect("verified Node should start with its loader-relative libraries"); + let executed_node = process + .receive_stdout_line(Duration::from_secs(2)) + .unwrap() + .expect("Node should report its executable path"); + assert_eq!( + Path::new(&executed_node).parent(), + node.parent(), + "verified Node must execute beside the authenticated runtime" + ); + process.wait().unwrap(); +} + fn spawn_linger_process() -> (SupervisedProcess, u32, u64) { let harness = PathBuf::from(env!("CARGO_BIN_EXE_fake-harness")); let script = PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/packages/paperclip-runner/scripts/run-local-provider-smoke.mjs b/packages/paperclip-runner/scripts/run-local-provider-smoke.mjs index cc856be840..a45dbcf80b 100644 --- a/packages/paperclip-runner/scripts/run-local-provider-smoke.mjs +++ b/packages/paperclip-runner/scripts/run-local-provider-smoke.mjs @@ -175,7 +175,7 @@ const rustSources = await filesUnder( path.endsWith(".rs") || path.endsWith("Cargo.toml") || path.endsWith("Cargo.lock"), - (path) => path.endsWith("/target"), + (path) => /\/(?:target|tests|benches|examples)$/u.test(path), ); await assertArtifactsFresh("runnerd", rustSources, [runnerd]);