use std::{ ffi::CString, fs::File, io::{self, Read, Write}, os::{ fd::{AsRawFd, FromRawFd, OwnedFd}, unix::ffi::OsStrExt, }, path::{Component, Path}, sync::atomic::{AtomicU64, Ordering}, }; use rand::random; use sha2::{Digest, Sha256}; use crate::{ArtifactError, ArtifactRef, MAX_ARTIFACT_BYTES, RegisteredArtifact, StoredArtifact}; const SHA_DIR: &[u8] = b"sha256"; pub(crate) const QUARANTINE_DIR: &[u8] = b"quarantine"; const TEMP_PREFIX: &str = ".crank-artifact-tmp-v1-"; const RESOLVE_NO_SYMLINKS: u64 = 0x04; static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0); #[derive(Debug)] pub(crate) struct Root { pub(crate) fd: OwnedFd, pub(crate) dev: u64, pub(crate) ino: u64, } #[derive(Debug)] struct LegacyRoot { root: Root, configured_path: std::path::PathBuf, } /// Immutable content-addressed store anchored to one trusted directory inode. #[derive(Debug)] pub struct ArtifactStore { root: Result, legacy: Option>, nonce: u128, } impl Clone for ArtifactStore { fn clone(&self) -> Self { // A fresh open file description makes flock coordination independent // between clones while retaining the original directory authority. let root = self .root .as_ref() .map_err(|error| *error) .and_then(|root| duplicate_root(root).map_err(|_| ArtifactError::Storage)); let legacy = self.legacy.as_ref().map(|legacy| { legacy.as_ref().map_err(|error| *error).and_then(|legacy| { duplicate_root(&legacy.root) .map(|root| LegacyRoot { root, configured_path: legacy.configured_path.clone(), }) .map_err(|_| ArtifactError::Storage) }) }); Self { root, legacy, nonce: self.nonce, } } } impl ArtifactStore { /// Opens an already provisioned, private artifact root. The root is never /// reopened by pathname after this point. pub fn open(root: impl AsRef) -> Result { Ok(Self { root: Ok(open_trusted_root(root.as_ref())?), legacy: None, nonce: random(), }) } /// Compatibility constructor. I/O returns the opening error fail-closed. pub fn new(root: impl AsRef) -> Self { Self { root: open_trusted_root(root.as_ref()), legacy: None, nonce: random(), } } /// Enables the explicit, read-only compatibility reader for one pinned /// legacy root. It never migrates, rewrites, or deletes legacy files. pub fn with_legacy_root(mut self, root: impl AsRef) -> Self { let configured_path = normalize_absolute_path(root.as_ref()); self.legacy = Some(configured_path.and_then(|configured_path| { open_trusted_root(root.as_ref()).map(|root_fd| LegacyRoot { root: root_fd, configured_path, }) })); self } /// Stores a complete bounded byte slice durably and returns its SHA-256 ref. pub fn put(&self, bytes: &[u8]) -> Result { self.put_reader(&mut io::Cursor::new(bytes)) } /// Stores bytes and returns a non-forgeable capability suitable for /// creating authoritative registry metadata. pub fn put_registered(&self, bytes: &[u8]) -> Result { let stored = self.put(bytes)?; Ok(RegisteredArtifact { artifact_ref: stored.artifact_ref, size_bytes: stored.size_bytes, }) } /// Historical spelling retained for source compatibility; bytes are not /// restricted to UTF-8 at this filesystem boundary. pub fn put_utf8(&self, bytes: &[u8]) -> Result { self.put(bytes) } /// Streams at most [`MAX_ARTIFACT_BYTES`] from `reader`, detecting an /// oversized source before publication even with short or interrupted reads. pub fn put_reader(&self, reader: &mut impl Read) -> Result { let mut bytes = Vec::with_capacity(8 * 1024); let mut hasher = Sha256::new(); let mut buffer = [0_u8; 8192]; loop { match reader.read(&mut buffer) { Ok(0) => break, Ok(count) => { if bytes .len() .checked_add(count) .ok_or(ArtifactError::SourceTooLarge)? > MAX_ARTIFACT_BYTES { return Err(ArtifactError::SourceTooLarge); } hasher.update(&buffer[..count]); bytes.extend_from_slice(&buffer[..count]); } Err(error) if error.kind() == io::ErrorKind::Interrupted => continue, Err(_) => return Err(ArtifactError::Storage), } } if bytes.is_empty() { return Err(ArtifactError::EmptySource); } let artifact_ref = ArtifactRef::from_digest_hex(&format!("{:x}", hasher.finalize()))?; let root = self.root()?; let _lock = RootLock::shared(root)?; ensure_root_unchanged(root)?; let sha = open_or_create_dir(root.fd.as_raw_fd(), SHA_DIR)?; let shard = open_or_create_dir(sha.as_raw_fd(), artifact_ref.shard().as_bytes())?; self.publish(root, shard.as_raw_fd(), &artifact_ref, &bytes)?; Ok(StoredArtifact { artifact_ref, size_bytes: bytes.len(), }) } /// Reads a blob only after validating its inode properties, bounded length, /// and complete SHA-256 digest. pub fn read(&self, artifact_ref: &ArtifactRef) -> Result, ArtifactError> { let root = self.root()?; let _lock = RootLock::shared(root)?; ensure_root_unchanged(root)?; let sha = open_existing_dir(root.fd.as_raw_fd(), SHA_DIR)?; let shard = open_existing_dir(sha.as_raw_fd(), artifact_ref.shard().as_bytes())?; let fd = open_existing_file(shard.as_raw_fd(), artifact_ref.digest_hex().as_bytes())?; read_verified(&fd, artifact_ref) } /// Digest-verified `file://` compatibility read below the explicitly /// configured legacy root. A URL cannot select another filesystem root. pub fn read_legacy_file_url( &self, value: &str, expected: &ArtifactRef, ) -> Result, ArtifactError> { let legacy = self .legacy .as_ref() .ok_or(ArtifactError::UnsafeRoot)? .as_ref() .map_err(|error| *error)?; let _lock = RootLock::shared(&legacy.root)?; ensure_root_unchanged(&legacy.root)?; let url = url::Url::parse(value).map_err(|_| ArtifactError::InvalidReference)?; if url.scheme() != "file" || url.host_str().is_some() { return Err(ArtifactError::InvalidReference); } let candidate = url .to_file_path() .map_err(|_| ArtifactError::InvalidReference)?; let relative = candidate .strip_prefix(&legacy.configured_path) .map_err(|_| ArtifactError::UnsafeRoot)?; let mut fd = duplicate_fd(legacy.root.fd.as_raw_fd())?; let mut components = relative.components().peekable(); while let Some(component) = components.next() { let Component::Normal(name) = component else { return Err(ArtifactError::UnsafeRoot); }; fd = if components.peek().is_some() { open_legacy_dir(fd.as_raw_fd(), name.as_bytes())? } else { open_legacy_file(fd.as_raw_fd(), name.as_bytes())? }; } read_legacy_verified(&fd, expected) } fn publish( &self, root: &Root, shard_fd: i32, artifact_ref: &ArtifactRef, bytes: &[u8], ) -> Result<(), ArtifactError> { let final_name = artifact_ref.digest_hex().as_bytes(); match open_existing_file(shard_fd, final_name) { Ok(file) => { let existing = read_verified(&file, artifact_ref)?; if existing == bytes { checkpointed("dedupe_directory_fsync", || fsync_fd(shard_fd))?; return Ok(()); } return Err(ArtifactError::Integrity); } Err(ArtifactError::NotFound) => {} Err(error) => return Err(error), } let temp_name = self.temp_name(); let temp = create_temp(shard_fd, temp_name.as_bytes())?; let result = (|| { checkpointed("write", || write_all(&temp, bytes))?; checkpointed("file_fsync", || fsync_fd(temp.as_raw_fd()))?; chmod_read_only(temp.as_raw_fd())?; fsync_fd(temp.as_raw_fd())?; ensure_root_unchanged(root)?; match checkpointed("publish", || { rename_no_replace(shard_fd, temp_name.as_bytes(), final_name) })? { true => checkpointed("directory_fsync", || fsync_fd(shard_fd)), false => { let file = open_existing_file(shard_fd, final_name)?; let existing = read_verified(&file, artifact_ref)?; if existing == bytes { checkpointed("dedupe_directory_fsync", || fsync_fd(shard_fd)) } else { Err(ArtifactError::Integrity) } } } })(); let _ = unlinkat(shard_fd, temp_name.as_bytes()); result } pub(crate) fn root(&self) -> Result<&Root, ArtifactError> { self.root.as_ref().map_err(|error| *error) } pub(crate) fn temp_prefix() -> &'static str { TEMP_PREFIX } pub(crate) fn temp_name(&self) -> String { format!( "{TEMP_PREFIX}{:032x}-{}-{}", self.nonce, std::process::id(), TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed) ) } } fn normalize_absolute_path(path: &Path) -> Result { if !path.is_absolute() { return Err(ArtifactError::UnsafeRoot); } let mut normalized = std::path::PathBuf::from("/"); for component in path.components() { match component { Component::RootDir | Component::CurDir => {} Component::Normal(part) => normalized.push(part), Component::ParentDir => { if !normalized.pop() { return Err(ArtifactError::UnsafeRoot); } } Component::Prefix(_) => return Err(ArtifactError::UnsafeRoot), } } Ok(normalized) } pub(crate) fn c_name(value: &[u8]) -> Result { CString::new(value).map_err(|_| ArtifactError::InvalidReference) } pub(crate) fn duplicate_fd(fd: i32) -> Result { let duplicate = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) }; if duplicate < 0 { Err(ArtifactError::Storage) } else { Ok(unsafe { OwnedFd::from_raw_fd(duplicate) }) } } fn duplicate_root(root: &Root) -> Result { let dot = c_name(b".")?; let raw = unsafe { libc::openat( root.fd.as_raw_fd(), dot.as_ptr(), libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW, ) }; if raw < 0 { return Err(classify_errno()); } let fd = unsafe { OwnedFd::from_raw_fd(raw) }; Ok(Root { fd, dev: root.dev, ino: root.ino, }) } fn open_trusted_root(path: &Path) -> Result { let path = CString::new(path.as_os_str().as_bytes()).map_err(|_| ArtifactError::UnsafeRoot)?; let how = OpenHow { flags: (libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW) as u64, mode: 0, resolve: RESOLVE_NO_SYMLINKS, }; let fd = unsafe { libc::syscall( libc::SYS_openat2, libc::AT_FDCWD, path.as_ptr(), &how, std::mem::size_of::(), ) as i32 }; if fd < 0 { return Err(classify_errno()); } let fd = unsafe { OwnedFd::from_raw_fd(fd) }; let stat = stat_fd(fd.as_raw_fd())?; check_private_dir(&stat)?; Ok(Root { fd, dev: stat.st_dev, ino: stat.st_ino, }) } #[repr(C)] struct OpenHow { flags: u64, mode: u64, resolve: u64, } pub(crate) fn ensure_root_unchanged(root: &Root) -> Result<(), ArtifactError> { let stat = stat_fd(root.fd.as_raw_fd())?; if stat.st_dev != root.dev || stat.st_ino != root.ino { return Err(ArtifactError::UnsafeRoot); } check_private_dir(&stat) } fn check_private_dir(stat: &libc::stat) -> Result<(), ArtifactError> { if stat.st_uid != unsafe { libc::geteuid() } || (stat.st_mode & libc::S_IFMT) != libc::S_IFDIR || (stat.st_mode & 0o777) != 0o700 { return Err(ArtifactError::UnsafeRoot); } Ok(()) } pub(crate) fn stat_fd(fd: i32) -> Result { let mut stat = unsafe { std::mem::zeroed() }; if unsafe { libc::fstat(fd, &mut stat) } != 0 { Err(ArtifactError::Storage) } else { Ok(stat) } } pub(crate) fn check_file(stat: &libc::stat) -> Result<(), ArtifactError> { if (stat.st_mode & libc::S_IFMT) != libc::S_IFREG || stat.st_uid != unsafe { libc::geteuid() } || stat.st_nlink != 1 || (stat.st_mode & 0o222) != 0 { return Err(ArtifactError::Integrity); } Ok(()) } pub(crate) fn open_or_create_dir(parent: i32, name: &[u8]) -> Result { let fd = match open_existing_dir(parent, name) { Ok(fd) => fd, Err(ArtifactError::NotFound) => { let name = c_name(name)?; let code = unsafe { libc::mkdirat(parent, name.as_ptr(), 0o700) }; if code != 0 && unsafe { *libc::__errno_location() } != libc::EEXIST { return Err(ArtifactError::Storage); } open_existing_dir(parent, name.as_bytes())? } Err(error) => return Err(error), }; checkpointed("mkdir_parent_fsync", || fsync_fd(parent))?; Ok(fd) } pub(crate) fn open_existing_dir(parent: i32, name: &[u8]) -> Result { let name = c_name(name)?; let fd = unsafe { libc::openat( parent, name.as_ptr(), libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW, ) }; if fd < 0 { return Err(classify_errno()); } let fd = unsafe { OwnedFd::from_raw_fd(fd) }; check_private_dir(&stat_fd(fd.as_raw_fd())?)?; Ok(fd) } pub(crate) fn open_existing_file(parent: i32, name: &[u8]) -> Result { let name = c_name(name)?; let fd = unsafe { libc::openat( parent, name.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NONBLOCK, ) }; if fd < 0 { return Err(classify_errno()); } let fd = unsafe { OwnedFd::from_raw_fd(fd) }; check_file(&stat_fd(fd.as_raw_fd())?)?; Ok(fd) } fn open_legacy_dir(parent: i32, name: &[u8]) -> Result { let name = c_name(name)?; let raw = unsafe { libc::openat( parent, name.as_ptr(), libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW, ) }; if raw < 0 { return Err(classify_errno()); } let fd = unsafe { OwnedFd::from_raw_fd(raw) }; if (stat_fd(fd.as_raw_fd())?.st_mode & libc::S_IFMT) != libc::S_IFDIR { return Err(ArtifactError::UnsafeRoot); } Ok(fd) } fn open_legacy_file(parent: i32, name: &[u8]) -> Result { let name = c_name(name)?; let raw = unsafe { libc::openat( parent, name.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NONBLOCK, ) }; if raw < 0 { return Err(classify_errno()); } let fd = unsafe { OwnedFd::from_raw_fd(raw) }; if (stat_fd(fd.as_raw_fd())?.st_mode & libc::S_IFMT) != libc::S_IFREG { return Err(ArtifactError::Integrity); } Ok(fd) } fn create_temp(parent: i32, name: &[u8]) -> Result { let name = c_name(name)?; let fd = unsafe { libc::openat( parent, name.as_ptr(), libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC | libc::O_NOFOLLOW, 0o600, ) }; if fd < 0 { return Err(classify_errno()); } Ok(unsafe { OwnedFd::from_raw_fd(fd) }) } fn write_all(fd: &OwnedFd, bytes: &[u8]) -> Result<(), ArtifactError> { let mut file = File::from(duplicate_fd(fd.as_raw_fd())?); let result = file.write_all(bytes).map_err(|_| ArtifactError::Storage); drop(file); result } /// Debug/test-only fault seam coupled to one syscall wrapper. `Fail` models a /// syscall that did not run; crash/hold actions happen only after success. pub(crate) fn checkpointed( stage: &str, operation: impl FnOnce() -> Result, ) -> Result { #[cfg(debug_assertions)] { if crate::test_support::action(stage) == Some(crate::test_support::FaultAction::Fail) { return Err(ArtifactError::Storage); } } let result = operation()?; #[cfg(debug_assertions)] { match crate::test_support::checkpoint(stage) { Some(crate::test_support::FaultAction::Exit) => { // Do not run Rust destructors: this models abrupt process // death, rather than an ordinary in-process error path. unsafe { libc::_exit(86) }; } Some(crate::test_support::FaultAction::Fail) => unreachable!("handled before syscall"), Some(crate::test_support::FaultAction::Hold) => {} None => {} } } let _ = stage; Ok(result) } pub(crate) fn read_verified( fd: &OwnedFd, expected: &ArtifactRef, ) -> Result, ArtifactError> { check_file(&stat_fd(fd.as_raw_fd())?)?; let length = usize::try_from(stat_fd(fd.as_raw_fd())?.st_size).map_err(|_| ArtifactError::Integrity)?; if length == 0 || length > MAX_ARTIFACT_BYTES { return Err(ArtifactError::Integrity); } let mut file = File::from(duplicate_fd(fd.as_raw_fd())?); let mut bytes = Vec::with_capacity(length); let result = Read::take(&mut file, (MAX_ARTIFACT_BYTES + 1) as u64) .read_to_end(&mut bytes) .map_err(|_| ArtifactError::Storage); drop(file); result?; if bytes.len() > MAX_ARTIFACT_BYTES || bytes.len() != length || format!("{:x}", Sha256::digest(&bytes)) != expected.digest_hex() { return Err(ArtifactError::Integrity); } Ok(bytes) } fn read_legacy_verified(fd: &OwnedFd, expected: &ArtifactRef) -> Result, ArtifactError> { let stat = stat_fd(fd.as_raw_fd())?; if (stat.st_mode & libc::S_IFMT) != libc::S_IFREG { return Err(ArtifactError::Integrity); } read_bounded_digest(fd, expected, stat.st_size) } fn read_bounded_digest( fd: &OwnedFd, expected: &ArtifactRef, raw_length: libc::off_t, ) -> Result, ArtifactError> { let length = usize::try_from(raw_length).map_err(|_| ArtifactError::Integrity)?; if length == 0 || length > MAX_ARTIFACT_BYTES { return Err(ArtifactError::Integrity); } let mut file = File::from(duplicate_fd(fd.as_raw_fd())?); let mut bytes = Vec::with_capacity(length); let result = Read::take(&mut file, (MAX_ARTIFACT_BYTES + 1) as u64) .read_to_end(&mut bytes) .map_err(|_| ArtifactError::Storage); drop(file); result?; if bytes.len() > MAX_ARTIFACT_BYTES || bytes.len() != length || format!("{:x}", Sha256::digest(&bytes)) != expected.digest_hex() { return Err(ArtifactError::Integrity); } Ok(bytes) } pub(crate) fn fsync_fd(fd: i32) -> Result<(), ArtifactError> { if unsafe { libc::fsync(fd) } == 0 { Ok(()) } else { Err(ArtifactError::Storage) } } fn chmod_read_only(fd: i32) -> Result<(), ArtifactError> { if unsafe { libc::fchmod(fd, 0o400) } == 0 { Ok(()) } else { Err(ArtifactError::Storage) } } fn rename_no_replace(parent: i32, old: &[u8], new: &[u8]) -> Result { rename_no_replace_at(parent, old, parent, new) } pub(crate) fn rename_no_replace_at( old_parent: i32, old: &[u8], new_parent: i32, new: &[u8], ) -> Result { let old = c_name(old)?; let new = c_name(new)?; let result = unsafe { libc::syscall( libc::SYS_renameat2, old_parent, old.as_ptr(), new_parent, new.as_ptr(), libc::RENAME_NOREPLACE, ) }; if result == 0 { Ok(true) } else if unsafe { *libc::__errno_location() } == libc::EEXIST { Ok(false) } else { Err(ArtifactError::Storage) } } pub(crate) fn unlinkat(parent: i32, name: &[u8]) -> Result<(), ArtifactError> { let name = c_name(name)?; if unsafe { libc::unlinkat(parent, name.as_ptr(), 0) } == 0 { Ok(()) } else { Err(classify_errno()) } } pub(crate) fn classify_errno() -> ArtifactError { match unsafe { *libc::__errno_location() } { libc::ENOENT => ArtifactError::NotFound, libc::ELOOP | libc::ENOTDIR => ArtifactError::UnsafeRoot, _ => ArtifactError::Storage, } } pub(crate) struct RootLock { fd: OwnedFd, } impl RootLock { pub(crate) fn shared(root: &Root) -> Result { let fd = duplicate_root(root)?.fd; if unsafe { libc::flock(fd.as_raw_fd(), libc::LOCK_SH) } == 0 { Ok(Self { fd }) } else { Err(ArtifactError::Storage) } } pub(crate) fn exclusive(root: &Root) -> Result { let fd = duplicate_root(root)?.fd; if unsafe { libc::flock(fd.as_raw_fd(), libc::LOCK_EX) } == 0 { Ok(Self { fd }) } else { Err(ArtifactError::Storage) } } } impl Drop for RootLock { fn drop(&mut self) { unsafe { libc::flock(self.fd.as_raw_fd(), libc::LOCK_UN); } } }