fix(runner): preserve macOS verified runtime layout
This commit is contained in:
parent
b8d84471a3
commit
72fc40a21e
|
|
@ -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<TemporaryExecutable, LocalRunnerError> {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue