//! Self-replacement always publishes a complete, transparently compressed file.
//! The mapped carrier is never edited, and the current process loads a
//! different inode/path: dlopen may otherwise return the already-loaded carrier
//! recursively.
use std::path::Path;
use crate::{cache::Materialized, format::Payload, Result};
/// Try the optional filesystem optimization. Errors leave the carrier intact
/// (except a directory-flush error after a completed atomic replacement) and
/// should be reported as diagnostics before falling back to the verified cache.
pub fn try_replace(payload: &Payload<'_>, carrier: &Path) -> Result<Option<Materialized>> {
#[cfg(any(target_os = "linux", target_os = "macos"))]
return supported::replace(payload, carrier);
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let _ = (payload, carrier);
// In particular, never attempt to replace a mapped Windows DLL.
Ok(None)
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
mod supported {
use std::{
fs::{self, File},
io::{self, Read},
os::{fd::AsRawFd, unix::fs::MetadataExt},
path::Path,
};
use tempfile::Builder;
use crate::{
cache,
format::{Payload, MAX_SIZE},
platform, Error, ErrorKind, Result,
};
const HEADER_SEARCH_CHUNK_SIZE: usize = 64 * 1024;
fn fail(operation: &str, error: io::Error) -> Error {
Error::io(ErrorKind::Cache, operation, error)
}
pub fn replace(payload: &Payload<'_>, carrier: &Path) -> Result<Option<cache::Materialized>> {
let mut original =
File::open(carrier).map_err(|e| fail("open carrier for replacement", e))?;
if !supported_filesystem(&original)? {
return Ok(None);
}
let metadata = original
.metadata()
.map_err(|e| fail("inspect carrier", e))?;
if !metadata.is_file()
|| metadata.nlink() != 1
|| metadata.uid() != unsafe { libc::geteuid() }
{
return Ok(None);
}
// Lock the original inode. Competing first loads holding that inode must
// recheck the pathname after acquiring the lock; only one can replace it.
platform::lock_exclusive(&original).map_err(|e| fail("lock carrier", e))?;
if !same_file(carrier, &metadata)? {
return Ok(None);
}
#[cfg(target_os = "macos")]
if platform::has_extended_acl(&original).map_err(|e| fail("inspect carrier ACL", e))? {
// The decoded staging file would not inherit an explicit Darwin
// ACL. Preserve installation access controls by using the cache.
return Ok(None);
}
// A process may reach registration after another process has already
// replaced the path. Require this payload's header in the source image
// before attempting replacement, never overwrite a different raw addon.
if metadata.len() > MAX_SIZE {
return Ok(None);
}
let header = payload.header.encode();
if !contains_header(&mut original, &header, HEADER_SEARCH_CHUNK_SIZE)
.map_err(|e| fail("inspect carrier payload", e))?
{
return Ok(None);
}
let parent = carrier
.parent()
.ok_or_else(|| Error::new(ErrorKind::Cache, "carrier has no parent directory"))?;
let mut stage = Builder::new()
.prefix(".swc-replace-")
.suffix(".node")
.tempfile_in(parent)
.map_err(|e| fail("stage carrier replacement", e))?;
#[cfg(target_os = "macos")]
if platform::has_extended_acl(stage.as_file())
.map_err(|e| fail("inspect replacement staging ACL", e))?
{
// The parent may contribute an inheritable ACL even when the
// carrier has none. Publishing it would silently alter the
// carrier's access controls, so retain the carrier and use cache.
return Ok(None);
}
payload.decode_into(stage.as_file_mut())?;
platform::copy_metadata(&original, stage.as_file())
.map_err(|e| fail("preserve carrier metadata", e))?;
stage
.as_file()
.sync_all()
.map_err(|e| fail("flush replacement", e))?;
let candidate = compress(stage)?;
let mut verified =
File::open(&candidate).map_err(|e| fail("open compressed replacement", e))?;
payload.verify_image(&mut verified)?;
verified
.sync_all()
.map_err(|e| fail("flush compressed replacement", e))?;
// Materialize the current process's distinct image before publishing.
// Failure here leaves the original carrier available for every process.
let temporary = cache::temporary(payload)?;
if !same_file(carrier, &metadata)? {
return Ok(None);
}
fs::rename(&candidate, carrier)
.map_err(|e| fail("atomically replace native carrier", e))?;
platform::sync_directory(parent).map_err(|e| fail("flush carrier directory", e))?;
Ok(Some(temporary))
}
fn same_file(path: &Path, original: &fs::Metadata) -> Result<bool> {
let current =
fs::symlink_metadata(path).map_err(|e| fail("recheck carrier identity", e))?;
Ok(current.is_file()
&& current.dev() == original.dev()
&& current.ino() == original.ino()
&& current.len() == original.len()
&& current.mtime() == original.mtime()
&& current.mtime_nsec() == original.mtime_nsec())
}
/// Search a carrier without retaining its potentially multi-gigabyte image.
/// The overlap retains headers split between successive reads.
fn contains_header(
input: &mut impl Read,
header: &[u8],
chunk_size: usize,
) -> io::Result<bool> {
debug_assert!(!header.is_empty());
debug_assert_ne!(chunk_size, 0);
let overlap = header.len() - 1;
let mut bytes = vec![0; overlap + chunk_size];
loop {
let read = input.read(&mut bytes[overlap..])?;
if read == 0 {
return Ok(false);
}
let len = overlap + read;
if bytes[..len]
.windows(header.len())
.any(|window| window == header)
{
return Ok(true);
}
if overlap != 0 {
bytes.copy_within(len - overlap..len, 0);
}
}
}
fn supported_filesystem(file: &File) -> Result<bool> {
let mut status: libc::statfs = unsafe { std::mem::zeroed() };
if unsafe { libc::fstatfs(file.as_raw_fd(), &mut status) } != 0 {
return Err(fail(
"identify carrier filesystem",
io::Error::last_os_error(),
));
}
#[cfg(target_os = "linux")]
return Ok(status.f_type as u64 == 0x9123_683e); // BTRFS_SUPER_MAGIC
#[cfg(target_os = "macos")]
return Ok(
unsafe { std::ffi::CStr::from_ptr(status.f_fstypename.as_ptr()) }.to_bytes() == b"apfs",
);
}
#[cfg(target_os = "linux")]
fn compress(stage: tempfile::NamedTempFile) -> Result<tempfile::TempPath> {
// These ioctl encodings are shared by the supported 64-bit Linux ABIs.
// Set the inode policy AND rewrite already-written extents: setting only
// FS_COMPR_FL does not retroactively compress the decoded file.
#[repr(C)]
struct DefragRange {
start: u64,
len: u64,
flags: u64,
extent_thresh: u32,
compress_type: u32,
unused: [u32; 4],
}
let fd = stage.as_file().as_raw_fd();
let mut flags: libc::c_long = 0;
if unsafe { libc::ioctl(fd, 0x8008_6601_u32 as _, &mut flags) } != 0 {
return Err(fail("read btrfs inode flags", io::Error::last_os_error()));
}
flags = (flags | 0x4) & !0x400; // FS_COMPR_FL, clear FS_NOCOMP_FL
if unsafe { libc::ioctl(fd, 0x4008_6602, &flags) } != 0 {
return Err(fail("enable btrfs compression", io::Error::last_os_error()));
}
let range = DefragRange {
start: 0,
len: u64::MAX,
flags: 3,
extent_thresh: 1,
compress_type: 1,
unused: [0; 4], // zlib works on all supported btrfs kernels
};
if unsafe { libc::ioctl(fd, 0x4030_9410, &range) } != 0 {
return Err(fail("compress btrfs extents", io::Error::last_os_error()));
}
stage
.as_file()
.sync_all()
.map_err(|e| fail("flush btrfs extents", e))?;
Ok(stage.into_temp_path())
}
#[cfg(target_os = "macos")]
fn compress(stage: tempfile::NamedTempFile) -> Result<tempfile::TempPath> {
use std::{os::macos::fs::MetadataExt as MacMetadataExt, process::Command};
let parent = stage.path().parent().unwrap();
let candidate = Builder::new()
.prefix(".swc-compressed-")
.suffix(".node")
.tempfile_in(parent)
.map_err(|e| fail("stage APFS compression", e))?
.into_temp_path();
// Apple's installed copier maintains the decmpfs resource-fork format.
// Only a private staging file is passed to it, never the mapped carrier.
let output = Command::new("/usr/bin/ditto")
.args(["--hfsCompression", "--rsrc", "--extattr", "--acl", "--qtn"])
.arg(stage.path())
.arg(&candidate)
.env_remove("DITTOABORT")
.env_remove("DITTONORSRC")
.env_remove("DITTOKEEPBINARIESPATTERN")
.env_remove("DITTOKEEPBINARIESDIR")
.env_remove("DITTO_TEST_OPTIONS")
.output()
.map_err(|e| fail("run APFS compression", e))?;
if !output.status.success() {
return Err(Error::new(
ErrorKind::Cache,
format!(
"APFS compression failed: {}",
String::from_utf8_lossy(&output.stderr)
),
));
}
let metadata = fs::metadata(&candidate).map_err(|e| fail("inspect APFS compression", e))?;
if MacMetadataExt::st_flags(&metadata) & 0x20 == 0 {
// UF_COMPRESSED
return Err(Error::new(
ErrorKind::Cache,
"APFS declined transparent compression",
));
}
Ok(candidate)
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use super::contains_header;
#[test]
fn finds_embedded_header_across_read_boundaries() {
let mut input = Cursor::new(b"prefix-embedded-header-suffix");
assert!(contains_header(&mut input, b"embedded-header", 8).unwrap());
}
#[test]
fn rejects_carrier_without_embedded_header() {
let mut input = Cursor::new(b"unrelated carrier bytes");
assert!(!contains_header(&mut input, b"embedded-header", 8).unwrap());
}
}
}