Avoid duplicate Darwin qualified executable materialization

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-02 06:41:10 -05:00
1 parent ad83416b03
commit ab368156f9
3 files changed
+673 -7

No files matched your search

@@ -32,7 +32,7 @@ use crate::provider_bridge::{
use crate::provider_events::{
project_acpx_state_event, AcpxEventProjectionContext, NormalizedProviderEvent,
};
use crate::qualified_launch::verify_launch_artifact;
use crate::qualified_launch::{verify_executable_launch_artifact, verify_launch_artifact};
fn is_reserved_terminal_operation_id(operation_id: &str) -> bool {
matches!(operation_id, "paperclip_finish" | "paperclip_block")
@@ -382,7 +382,11 @@ impl AcpxProviderDescriptor {
"ACPX runner launch profile repeats an artifact path",
));
}
let snapshot = verify_launch_artifact(artifact, "ACPX")?;
let snapshot = if artifact.path == launch_profile.command {
verify_executable_launch_artifact(artifact, "ACPX")?
} else {
verify_launch_artifact(artifact, "ACPX")?
};
verified.insert(artifact.path.clone(), snapshot);
}
let command = verified
@@ -3089,6 +3093,135 @@ mod tests {
fs::remove_dir_all(directory).unwrap();
}
#[cfg(target_os = "macos")]
mod darwin_named_transport_cleanup {
use super::*;
struct Fixture {
directory: PathBuf,
descriptor: AcpxProviderDescriptor,
profile: AcpxLaunchProfile,
}
impl Fixture {
fn new(label: &str) -> Self {
let directory = temporary_directory(label);
let command = directory.join("node");
let sidecar = directory.join("sidecar.cjs");
write_artifact(&command, b"qualified node", true);
write_artifact(&sidecar, b"qualified sidecar", false);
let args = vec![sidecar.to_string_lossy().into_owned()];
let profile = AcpxLaunchProfile {
authority_digest: format!("sha256:{}", "d".repeat(64)),
command: command.clone(),
args: args.clone(),
artifacts: vec![artifact(&command), artifact(&sidecar)],
};
let mut value = descriptor("codex");
value["sidecarCommand"] = json!(command);
value["sidecarArgs"] = json!(args);
Self {
directory,
descriptor: serde_json::from_value(value).unwrap(),
profile,
}
}
fn named_images(&self) -> usize {
fs::read_dir(&self.directory)
.unwrap()
.map(|entry| entry.unwrap())
.filter(|entry| {
entry
.file_name()
.to_string_lossy()
.starts_with(".paperclip-verified-executable-")
})
.count()
}
fn prove_transport_owns_image(&self) {
assert_eq!(self.named_images(), 0);
let transport = self
.descriptor
.verified_transport(Some(&self.profile))
.unwrap();
assert_eq!(
self.named_images(),
1,
"verification must own one named Node image"
);
drop(transport);
assert_eq!(
self.named_images(),
0,
"dropping unstarted transport must retire its image"
);
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.directory);
}
}
// File verification only: these tests never start a transport/process.
#[test]
fn second_artifact_failure_retires_named_executable() {
let fixture = Fixture::new("darwin-second-artifact-cleanup");
fixture.prove_transport_owns_image();
// The command remains valid and first in the profile; the sidecar
// fails only after the command's named snapshot has been admitted.
write_artifact(
&fixture.profile.artifacts[1].path,
b"tampered sidecar",
false,
);
let error = fixture
.descriptor
.verified_transport(Some(&fixture.profile))
.err()
.unwrap();
assert!(error
.to_string()
.contains("verified process artifact digest mismatch"));
assert_eq!(fixture.named_images(), 0);
assert_eq!(
fs::read(&fixture.profile.command).unwrap(),
b"qualified node"
);
}
#[test]
fn argv_construction_failure_retires_named_executable() {
let mut fixture = Fixture::new("darwin-argv-cleanup");
fixture.prove_transport_owns_image();
// Descriptor and profile agree, so this passes launch binding and
// artifact verification, then fails the absolute-argument mapping.
let unauthenticated = fixture
.directory
.join("unauthenticated.cjs")
.to_string_lossy()
.into_owned();
fixture.profile.args.push(unauthenticated.clone());
fixture.descriptor.sidecar_args.push(unauthenticated);
let error = fixture
.descriptor
.verified_transport(Some(&fixture.profile))
.err()
.unwrap();
assert!(error
.to_string()
.contains("does not authenticate an absolute argument"));
assert_eq!(fixture.named_images(), 0);
assert_eq!(
fs::read(&fixture.profile.command).unwrap(),
b"qualified node"
);
}
}
#[cfg(unix)]
#[test]
fn rejects_symlinked_launch_artifacts() {
@@ -40,6 +40,8 @@ pub(crate) fn is_node_interpreter(path: &Path) -> bool {
pub struct VerifiedProcessArtifact {
display_path: PathBuf,
file: Arc<File>,
#[cfg(target_os = "macos")]
executable: Option<Arc<NamedExecutableSnapshot>>,
}
impl VerifiedProcessArtifact {
@@ -65,8 +67,246 @@ impl VerifiedProcessArtifact {
Ok(Self {
display_path,
file: Arc::new(file),
#[cfg(target_os = "macos")]
executable: None,
})
}
/// Executable snapshots need a named image on Darwin. Construct that image
/// once, beside the source runtime for loader-relative shared libraries.
pub fn snapshot_verified_executable(
display_path: PathBuf,
file: File,
expected_sha256: &str,
) -> Result<Self, LocalRunnerError> {
#[cfg(target_os = "macos")]
{
let mut file = file;
file.seek(SeekFrom::Start(0))
.map_err(|error| snapshot_error(&display_path, error))?;
let executable = Arc::new(NamedExecutableSnapshot::create(
&display_path,
&mut file,
expected_sha256,
)?);
Ok(Self {
display_path,
file: executable.file.clone(),
executable: Some(executable),
})
}
#[cfg(not(target_os = "macos"))]
Self::snapshot_verified(display_path, file, expected_sha256)
}
}
#[cfg(target_os = "macos")]
#[derive(Debug)]
struct NamedExecutablePath {
parent: File,
path: PathBuf,
basename: std::ffi::OsString,
device: u64,
inode: u64,
}
#[cfg(target_os = "macos")]
impl NamedExecutablePath {
fn open_image(&self) -> Result<File, LocalRunnerError> {
use rustix::fs::{openat, Mode, OFlags};
let fd = openat(
&self.parent,
&self.basename,
OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
Mode::empty(),
)
.map_err(|error| snapshot_error(&self.path, error))?;
Ok(File::from(fd))
}
fn validate(&self) -> Result<File, LocalRunnerError> {
let parent_path = self.path.parent().expect("named image has a parent");
let visible_parent =
fs::symlink_metadata(parent_path).map_err(|error| snapshot_error(&self.path, error))?;
let held_parent = self
.parent
.metadata()
.map_err(|error| snapshot_error(&self.path, error))?;
if !visible_parent.is_dir()
|| visible_parent.dev() != held_parent.dev()
|| visible_parent.ino() != held_parent.ino()
|| visible_parent.permissions().mode() & 0o022 != 0
{
return Err(snapshot_error(&self.path, "executable parent changed"));
}
let image = self.open_image()?;
let metadata = image
.metadata()
.map_err(|error| snapshot_error(&self.path, error))?;
if !metadata.is_file()
|| metadata.dev() != self.device
|| metadata.ino() != self.inode
|| metadata.permissions().mode() & 0o777 != 0o500
{
return Err(snapshot_error(&self.path, "executable image changed"));
}
Ok(image)
}
}
#[cfg(target_os = "macos")]
impl Drop for NamedExecutablePath {
fn drop(&mut self) {
// Resolve relative to the retained directory, never through a replaced
// parent pathname. A substituted image does not belong to this guard.
if let Ok(file) = self.open_image() {
if let Ok(metadata) = file.metadata() {
if metadata.is_file()
&& metadata.dev() == self.device
&& metadata.ino() == self.inode
{
let _ = rustix::fs::unlinkat(
&self.parent,
&self.basename,
rustix::fs::AtFlags::empty(),
);
}
}
}
}
}
#[cfg(target_os = "macos")]
#[derive(Debug)]
struct NamedExecutableSnapshot {
image: NamedExecutablePath,
file: Arc<File>,
expected_sha256: String,
expected_len: u64,
}
#[cfg(target_os = "macos")]
impl NamedExecutableSnapshot {
fn create(
display_path: &Path,
source: &mut File,
expected_sha256: &str,
) -> Result<Self, LocalRunnerError> {
use rustix::fs::{open, openat, Mode, OFlags};
let parent_path = display_path
.parent()
.ok_or_else(|| snapshot_error(display_path, "executable has no parent"))?;
let parent = File::from(
open(
parent_path,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
)
.map_err(|error| snapshot_error(display_path, error))?,
);
let parent_metadata = parent
.metadata()
.map_err(|error| snapshot_error(display_path, error))?;
if parent_metadata.permissions().mode() & 0o022 != 0 {
return Err(snapshot_error(
display_path,
"executable parent is group- or world-writable",
));
}
let basename = std::ffi::OsString::from(format!(
".paperclip-verified-executable-{}",
Uuid::new_v4().simple()
));
let path = parent_path.join(&basename);
let mut writable = File::from(
openat(
&parent,
&basename,
OFlags::RDWR | OFlags::CREATE | OFlags::EXCL | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::RUSR | Mode::WUSR | Mode::XUSR,
)
.map_err(|error| snapshot_error(display_path, error))?,
);
// If fstat itself fails, ownership cannot be proven. Fail closed and
// leave the randomized entry rather than unlink a possibly substituted
// name without its inode identity.
let metadata = writable
.metadata()
.map_err(|error| snapshot_error(display_path, error))?;
let image = NamedExecutablePath {
parent,
path,
basename,
device: metadata.dev(),
inode: metadata.ino(),
};
copy_verified(source, &mut writable, display_path, expected_sha256)?;
writable
.sync_all()
.map_err(|error| snapshot_error(display_path, error))?;
rustix::fs::fchmod(&writable, Mode::RUSR | Mode::XUSR)
.map_err(|error| snapshot_error(display_path, error))?;
let file = Arc::new(image.validate()?);
drop(writable);
let expected_len = file
.metadata()
.map_err(|error| snapshot_error(display_path, error))?
.len();
Ok(Self {
image,
file,
expected_sha256: expected_sha256.to_owned(),
expected_len,
})
}
fn validate_before_spawn(&self) -> Result<(), LocalRunnerError> {
self.image.validate()?;
let before = self
.file
.metadata()
.map_err(|error| snapshot_error(&self.image.path, error))?;
if before.len() != self.expected_len {
return Err(snapshot_error(
&self.image.path,
"executable length changed",
));
}
let mut digest = Sha256::new();
let mut buffer = [0_u8; 64 * 1024];
let mut offset = 0;
while offset < self.expected_len {
let remaining = (self.expected_len - offset).min(buffer.len() as u64) as usize;
let count = self
.file
.read_at(&mut buffer[..remaining], offset)
.map_err(|error| snapshot_error(&self.image.path, error))?;
if count == 0 {
break;
}
digest.update(&buffer[..count]);
offset += count as u64;
}
let after = self
.file
.metadata()
.map_err(|error| snapshot_error(&self.image.path, error))?;
if format!("sha256:{:x}", digest.finalize()) != self.expected_sha256
|| before.len() != after.len()
|| offset != after.len()
|| before.mtime() != after.mtime()
|| before.mtime_nsec() != after.mtime_nsec()
|| before.ctime() != after.ctime()
|| before.ctime_nsec() != after.ctime_nsec()
{
return Err(snapshot_error(
&self.image.path,
"executable digest or metadata changed",
));
}
self.image.validate()?;
Ok(())
}
}
#[cfg(target_os = "linux")]
@@ -232,11 +472,18 @@ impl VerifiedProcessLaunch {
#[cfg(target_os = "linux")]
inherited.push(program_fd);
#[cfg(target_os = "macos")]
let program_snapshot = materialize_executable(&self.program)?;
let named_executable = self.program.executable.clone();
#[cfg(target_os = "macos")]
let program = program_snapshot.path.clone();
let mut temporary_executables = Vec::new();
#[cfg(target_os = "macos")]
let mut temporary_executables = vec![program_snapshot];
let program = if let Some(executable) = &named_executable {
executable.image.path.clone()
} else {
let snapshot = materialize_executable(&self.program)?;
let path = snapshot.path.clone();
temporary_executables.push(snapshot);
path
};
let mut args = Vec::with_capacity(self.args.len());
for argument in &self.args {
match argument {
@@ -269,12 +516,18 @@ impl VerifiedProcessLaunch {
}
}
}
#[cfg(target_os = "macos")]
if let Some(executable) = &named_executable {
executable.validate_before_spawn()?;
}
Ok(InheritedCommand {
program,
args,
_inherited: inherited,
#[cfg(target_os = "macos")]
temporary_executables,
#[cfg(target_os = "macos")]
named_executable,
})
}
}
@@ -286,6 +539,8 @@ struct InheritedCommand {
_inherited: Vec<rustix::fd::OwnedFd>,
#[cfg(target_os = "macos")]
temporary_executables: Vec<TemporaryExecutable>,
#[cfg(target_os = "macos")]
named_executable: Option<Arc<NamedExecutableSnapshot>>,
}
#[cfg(target_os = "macos")]
@@ -641,6 +896,8 @@ pub struct SupervisedProcess {
finished: bool,
#[cfg(target_os = "macos")]
_temporary_executables: Vec<TemporaryExecutable>,
#[cfg(target_os = "macos")]
_named_executable: Option<Arc<NamedExecutableSnapshot>>,
}
impl SupervisedProcess {
@@ -722,6 +979,7 @@ impl SupervisedProcess {
if let Ok(process) = result.as_mut() {
process._temporary_executables =
std::mem::take(&mut inherited.temporary_executables);
process._named_executable = inherited.named_executable.take();
}
drop(inherited);
result
@@ -824,6 +1082,8 @@ impl SupervisedProcess {
finished: false,
#[cfg(target_os = "macos")]
_temporary_executables: Vec::new(),
#[cfg(target_os = "macos")]
_named_executable: None,
})
}
@@ -1098,6 +1358,260 @@ mod tests {
assert!(!process.stdout_drained());
}
// These tests exercise file admission and ownership only. They never launch
// a native runner, shell, Node executable, provider, or child process.
#[cfg(target_os = "macos")]
mod darwin_named_snapshot {
use super::*;
use std::os::unix::fs::symlink;
struct Fixture {
directory: PathBuf,
}
impl Fixture {
fn new() -> Self {
let directory = std::env::temp_dir().join(format!(
"paperclip-darwin-snapshot-{}",
Uuid::new_v4().simple()
));
fs::create_dir(&directory).unwrap();
fs::set_permissions(&directory, fs::Permissions::from_mode(0o700)).unwrap();
Self {
directory: fs::canonicalize(directory).unwrap(),
}
}
fn artifact(&self) -> VerifiedProcessArtifact {
let path = self.directory.join("node");
fs::write(&path, b"authenticated executable bytes").unwrap();
VerifiedProcessArtifact::snapshot_verified_executable(
path.clone(),
File::open(path).unwrap(),
&format!(
"sha256:{:x}",
Sha256::digest(b"authenticated executable bytes")
),
)
.unwrap()
}
fn image_count(&self) -> usize {
fs::read_dir(&self.directory)
.unwrap()
.filter_map(Result::ok)
.filter(|entry| {
entry
.file_name()
.to_string_lossy()
.starts_with(".paperclip-verified-executable-")
})
.count()
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.directory);
}
}
#[test]
fn source_changes_and_closed_source_do_not_change_single_image() {
let fixture = Fixture::new();
let artifact = fixture.artifact();
let executable = artifact.executable.as_ref().unwrap();
let source = fixture.directory.join("node");
fs::write(&source, b"overwrite original inode").unwrap();
fs::rename(&source, fixture.directory.join("retired-node")).unwrap();
fs::write(&source, b"replacement source inode").unwrap();
fs::remove_file(&source).unwrap();
executable.validate_before_spawn().unwrap();
assert_eq!(
executable.image.path.parent(),
Some(fixture.directory.as_path())
);
assert_eq!(fixture.image_count(), 1);
let launch = VerifiedProcessLaunch::new(artifact, vec![]);
let inherited = launch.inherited_command().unwrap();
assert_eq!(
fixture.image_count(),
1,
"admission must not materialize another image"
);
assert_eq!(
fs::read(&inherited.program).unwrap(),
b"authenticated executable bytes"
);
}
#[test]
fn same_inode_mutation_is_rejected_even_after_restoring_readonly_mode() {
let fixture = Fixture::new();
let artifact = fixture.artifact();
let executable = artifact.executable.as_ref().unwrap();
fs::set_permissions(&executable.image.path, fs::Permissions::from_mode(0o700)).unwrap();
fs::write(
&executable.image.path,
vec![b'X'; executable.expected_len as usize],
)
.unwrap();
fs::set_permissions(&executable.image.path, fs::Permissions::from_mode(0o500)).unwrap();
assert!(executable.validate_before_spawn().is_err());
}
#[test]
fn replacement_image_is_rejected_and_not_removed_by_drop() {
let fixture = Fixture::new();
let artifact = fixture.artifact();
let path = artifact.executable.as_ref().unwrap().image.path.clone();
fs::rename(&path, fixture.directory.join("retired-image")).unwrap();
fs::write(&path, b"replacement").unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o500)).unwrap();
assert!(artifact
.executable
.as_ref()
.unwrap()
.validate_before_spawn()
.is_err());
drop(artifact);
assert_eq!(fs::read(&path).unwrap(), b"replacement");
}
#[test]
fn symlink_image_is_rejected_and_its_target_and_link_survive_drop() {
let fixture = Fixture::new();
let artifact = fixture.artifact();
let path = artifact.executable.as_ref().unwrap().image.path.clone();
fs::remove_file(&path).unwrap();
let target = fixture.directory.join("unrelated");
fs::write(&target, b"unrelated").unwrap();
symlink(&target, &path).unwrap();
assert!(artifact
.executable
.as_ref()
.unwrap()
.validate_before_spawn()
.is_err());
drop(artifact);
assert!(fs::symlink_metadata(&path)
.unwrap()
.file_type()
.is_symlink());
assert_eq!(fs::read(&target).unwrap(), b"unrelated");
}
#[test]
fn directory_image_is_rejected_and_survives_drop() {
let fixture = Fixture::new();
let artifact = fixture.artifact();
let executable = artifact.executable.as_ref().unwrap();
let path = executable.image.path.clone();
fs::remove_file(&path).unwrap();
// rustix does not expose safe FIFO creation on Darwin. This tests
// nonregular-entry rejection; FIFO-specific calibration remains
// separate. Production opens retain NONBLOCK for FIFO substitution.
fs::create_dir(&path).unwrap();
assert!(executable.validate_before_spawn().is_err());
drop(artifact);
assert!(fs::symlink_metadata(&path).unwrap().is_dir());
}
#[test]
fn writable_mode_is_rejected_even_with_original_bytes() {
let fixture = Fixture::new();
let artifact = fixture.artifact();
let executable = artifact.executable.as_ref().unwrap();
fs::set_permissions(&executable.image.path, fs::Permissions::from_mode(0o700)).unwrap();
assert!(executable.validate_before_spawn().is_err());
fs::set_permissions(&executable.image.path, fs::Permissions::from_mode(0o500)).unwrap();
executable.validate_before_spawn().unwrap();
}
#[test]
fn replaced_parent_fails_admission_and_cleanup_uses_held_parent() {
let fixture = Fixture::new();
let parent = fixture.directory.join("runtime");
fs::create_dir(&parent).unwrap();
let source = parent.join("node");
fs::write(&source, b"original").unwrap();
let artifact = VerifiedProcessArtifact::snapshot_verified_executable(
source.clone(),
File::open(&source).unwrap(),
&format!("sha256:{:x}", Sha256::digest(b"original")),
)
.unwrap();
let path = artifact.executable.as_ref().unwrap().image.path.clone();
let retired = fixture.directory.join("retired-runtime");
fs::rename(&parent, &retired).unwrap();
fs::create_dir(&parent).unwrap();
fs::write(&path, b"replacement parent image").unwrap();
assert!(artifact
.executable
.as_ref()
.unwrap()
.validate_before_spawn()
.is_err());
drop(artifact);
assert_eq!(fs::read(&path).unwrap(), b"replacement parent image");
assert!(!retired.join(path.file_name().unwrap()).exists());
}
#[test]
fn launch_clones_and_inherited_owner_keep_image_until_last_drop() {
let fixture = Fixture::new();
let launch = VerifiedProcessLaunch::new(fixture.artifact(), vec![]);
let clone = launch.clone();
let mut inherited = launch.inherited_command().unwrap();
let path = inherited.program.clone();
// This is the same ownership transfer performed after spawn succeeds.
let process_owner = inherited.named_executable.take();
drop(inherited);
drop(launch);
assert!(path.exists());
drop(clone);
assert!(
path.exists(),
"descendant runtime pathname must remain live"
);
process_owner
.as_ref()
.unwrap()
.validate_before_spawn()
.unwrap();
drop(process_owner);
assert!(!path.exists());
}
#[test]
fn digest_failure_and_late_abort_remove_owned_image() {
let fixture = Fixture::new();
let source = fixture.directory.join("node");
fs::write(&source, b"original").unwrap();
assert!(VerifiedProcessArtifact::snapshot_verified_executable(
source.clone(),
File::open(source).unwrap(),
"sha256:wrong"
)
.is_err());
assert_eq!(fixture.image_count(), 0);
let launch = VerifiedProcessLaunch::new(fixture.artifact(), vec![]);
let inherited = launch.inherited_command().unwrap();
let path = inherited.program.clone();
drop(launch);
assert!(path.exists());
drop(inherited); // Includes the spawn-error/late-abort ownership path.
assert!(!path.exists());
}
#[test]
fn commonjs_snapshots_stay_anonymous() {
let fixture = Fixture::new();
let script = verified_artifact(
&fixture.directory.join("sidecar.cjs"),
b"module.exports = {};\n",
);
assert!(script.executable.is_none());
assert_eq!(script.file.metadata().unwrap().nlink(), 0);
}
}
fn verified_artifact(path: &Path, bytes: &[u8]) -> VerifiedProcessArtifact {
fs::write(path, bytes).unwrap();
let digest = format!("sha256:{:x}", Sha256::digest(bytes));
@@ -9,6 +9,21 @@ use crate::process_supervisor::VerifiedProcessArtifact;
pub fn verify_launch_artifact(
artifact: &QualifiedLaunchArtifact,
label: &str,
) -> Result<VerifiedProcessArtifact, DurableRunnerError> {
verify_launch_artifact_with_role(artifact, label, false)
}
pub fn verify_executable_launch_artifact(
artifact: &QualifiedLaunchArtifact,
label: &str,
) -> Result<VerifiedProcessArtifact, DurableRunnerError> {
verify_launch_artifact_with_role(artifact, label, true)
}
fn verify_launch_artifact_with_role(
artifact: &QualifiedLaunchArtifact,
label: &str,
executable: bool,
) -> Result<VerifiedProcessArtifact, DurableRunnerError> {
let source_metadata = fs::symlink_metadata(&artifact.path).map_err(|error| {
DurableRunnerError::invalid(format!("failed to inspect qualified {label}: {error}"))
@@ -49,8 +64,12 @@ pub fn verify_launch_artifact(
"qualified {label} changed while it was opened"
)));
}
VerifiedProcessArtifact::snapshot_verified(canonical, file, &artifact.sha256)
.map_err(|error| DurableRunnerError::invalid(error.to_string()))
let snapshot = if executable {
VerifiedProcessArtifact::snapshot_verified_executable(canonical, file, &artifact.sha256)
} else {
VerifiedProcessArtifact::snapshot_verified(canonical, file, &artifact.sha256)
};
snapshot.map_err(|error| DurableRunnerError::invalid(error.to_string()))
}
#[cfg(unix)]