use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Mutex, OnceLock};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
const MAX_MODEL_ID_BYTES: usize = 4 * 1024;
const MAX_STATE_BYTES: u64 = 64 * 1024;
#[derive(Debug, thiserror::Error)]
pub enum ModelManagementError {
#[error("model-management I/O failed at {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("invalid model-management state at {path}: {message}")]
InvalidState { path: PathBuf, message: String },
#[error("model-management identity mismatch at {path}: expected {expected}, found {actual}")]
IdentityMismatch {
path: PathBuf,
expected: String,
actual: String,
},
#[error("no CAR install receipt exists for {model_id}")]
MissingReceipt { model_id: String },
#[error("unsafe CAR-managed path for {model_id}: {path} ({reason})")]
UnsafeManagedPath {
model_id: String,
path: PathBuf,
reason: String,
},
#[error("local model {model_id} is in use")]
ModelInUse { model_id: String },
#[error("local model-management operation is already active for {model_id}")]
MutationInProgress { model_id: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ManagedArtifactKind {
Symlink,
Directory,
File,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InstallReceipt {
pub model_id: String,
pub managed_path: PathBuf,
pub artifact_kind: ManagedArtifactKind,
pub source_model_id: String,
pub source_revision: Option<String>,
pub creation_generation: u64,
pub shared_cache_references: Vec<PathBuf>,
pub adopted: bool,
}
#[derive(Debug, Clone)]
pub struct ModelManagementStore {
models_dir: PathBuf,
management_state_dir: PathBuf,
receipts_root: PathBuf,
tombstones_root: PathBuf,
installs_root: PathBuf,
removals_root: PathBuf,
leases_root: PathBuf,
mutation_locks_root: PathBuf,
}
impl ModelManagementStore {
pub fn new(state_root: PathBuf, models_dir: PathBuf) -> Self {
let state_root = crate::resource_policy::normalized_state_root_key(&state_root);
let management_state_dir = state_root.join("model-management");
let models_dir = crate::resource_policy::normalized_state_root_key(&models_dir);
let shared_coordination_root = models_dir
.parent()
.unwrap_or(&models_dir)
.join(".car-model-management");
let store = Self {
models_dir,
receipts_root: management_state_dir.join("receipts"),
tombstones_root: management_state_dir.join("tombstones"),
installs_root: management_state_dir.join("installs"),
removals_root: management_state_dir.join("removals"),
leases_root: shared_coordination_root.join("activity"),
mutation_locks_root: shared_coordination_root.join("mutation"),
management_state_dir,
};
store.resume_pending_quarantines();
store
}
pub fn models_dir(&self) -> &Path {
&self.models_dir
}
pub fn management_state_dir(&self) -> &Path {
&self.management_state_dir
}
pub fn receipts_root(&self) -> &Path {
&self.receipts_root
}
pub fn receipt_path(&self, model_id: &str) -> PathBuf {
self.receipts_root.join(hashed_filename(model_id))
}
pub fn tombstones_root(&self) -> &Path {
&self.tombstones_root
}
pub fn tombstone_path(&self, model_id: &str) -> PathBuf {
self.tombstones_root.join(hashed_filename(model_id))
}
fn removal_journal_path(&self, model_id: &str) -> PathBuf {
self.removals_root.join(hashed_filename(model_id))
}
fn install_journal_path(&self, model_id: &str) -> PathBuf {
self.installs_root.join(hashed_filename(model_id))
}
pub fn leases_root(&self) -> &Path {
&self.leases_root
}
pub fn lease_path(&self, model_id: &str) -> PathBuf {
self.leases_root.join(hashed_filename(model_id))
}
pub fn mutation_lock_path(&self, model_id: &str) -> PathBuf {
self.mutation_locks_root.join(hashed_filename(model_id))
}
pub fn load_receipt(
&self,
model_id: &str,
) -> Result<Option<InstallReceipt>, ModelManagementError> {
validate_model_id(model_id)?;
let path = self.receipt_path(model_id);
let bytes = match read_private_state(&path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(source) => return Err(ModelManagementError::Io { path, source }),
};
let receipt: InstallReceipt =
serde_json::from_slice(&bytes).map_err(|error| ModelManagementError::InvalidState {
path: path.clone(),
message: error.to_string(),
})?;
ensure_identity(&path, model_id, &receipt.model_id)?;
Ok(Some(receipt))
}
pub(crate) fn write_receipt(
&self,
receipt: &InstallReceipt,
) -> Result<(), ModelManagementError> {
validate_model_id(&receipt.model_id)?;
if let Some(existing) = self.load_receipt(&receipt.model_id)? {
ensure_identity(
&self.receipt_path(&receipt.model_id),
&receipt.model_id,
&existing.model_id,
)?;
}
write_private_json(&self.receipt_path(&receipt.model_id), receipt)
}
pub fn can_remove(&self, model_id: &str) -> Result<bool, ModelManagementError> {
let Some(receipt) = self.load_receipt(model_id)? else {
return Ok(false);
};
self.validate_managed_artifact(&receipt)?;
Ok(receipt.artifact_kind != ManagedArtifactKind::Directory)
}
pub fn acquire_lease(&self, model_id: &str) -> Result<ModelLease, ModelManagementError> {
validate_model_id(model_id)?;
let mut mutation = self.open_identity_lock(&self.mutation_lock_path(model_id), model_id)?;
mutation
.lock_shared()
.map_err(|source| ModelManagementError::Io {
path: self.mutation_lock_path(model_id),
source,
})?;
validate_lock_identity(&mut mutation, &self.mutation_lock_path(model_id), model_id)?;
let mut file = self.open_identity_lock(&self.lease_path(model_id), model_id)?;
file.lock_shared()
.map_err(|source| ModelManagementError::Io {
path: self.lease_path(model_id),
source,
})?;
validate_lock_identity(&mut file, &self.lease_path(model_id), model_id)?;
drop(mutation);
Ok(ModelLease { _file: file })
}
pub(crate) fn begin_mutation(
&self,
model_id: &str,
) -> Result<ModelMutationGuard, ModelManagementError> {
validate_model_id(model_id)?;
let mut file = self.open_identity_lock(&self.mutation_lock_path(model_id), model_id)?;
validate_lock_identity(&mut file, &self.mutation_lock_path(model_id), model_id)?;
match file.try_lock() {
Ok(()) => Ok(ModelMutationGuard {
store: self.clone(),
model_id: model_id.to_string(),
_file: file,
}),
Err(std::fs::TryLockError::WouldBlock) => {
Err(ModelManagementError::MutationInProgress {
model_id: model_id.to_string(),
})
}
Err(std::fs::TryLockError::Error(source)) => Err(ModelManagementError::Io {
path: self.mutation_lock_path(model_id),
source,
}),
}
}
pub fn model_in_use(&self, model_id: &str) -> Result<bool, ModelManagementError> {
validate_model_id(model_id)?;
let mut file = self.open_identity_lock(&self.lease_path(model_id), model_id)?;
validate_lock_identity(&mut file, &self.lease_path(model_id), model_id)?;
match file.try_lock() {
Ok(()) => Ok(false),
Err(std::fs::TryLockError::WouldBlock) => Ok(true),
Err(std::fs::TryLockError::Error(source)) => Err(ModelManagementError::Io {
path: self.lease_path(model_id),
source,
}),
}
}
pub fn car_enabled(&self, model_id: &str) -> Result<bool, ModelManagementError> {
Ok(self.load_tombstone(model_id)?.is_none())
}
fn load_tombstone(
&self,
model_id: &str,
) -> Result<Option<RemovalTombstone>, ModelManagementError> {
validate_model_id(model_id)?;
let path = self.tombstone_path(model_id);
let bytes = match read_private_state(&path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(source) => return Err(ModelManagementError::Io { path, source }),
};
let tombstone: RemovalTombstone =
serde_json::from_slice(&bytes).map_err(|error| ModelManagementError::InvalidState {
path: path.clone(),
message: error.to_string(),
})?;
ensure_identity(&path, model_id, &tombstone.model_id)?;
Ok(Some(tombstone))
}
pub(crate) fn clear_tombstone(&self, model_id: &str) -> Result<(), ModelManagementError> {
validate_model_id(model_id)?;
let _ = self.car_enabled(model_id)?;
let path = self.tombstone_path(model_id);
match std::fs::remove_file(&path) {
Ok(()) => sync_directory(path.parent().expect("tombstone path has parent"))
.map_err(|source| ModelManagementError::Io { path, source }),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(source) => Err(ModelManagementError::Io { path, source }),
}
}
pub(crate) fn record_managed_artifact(
&self,
model_id: &str,
source_model_id: &str,
source_revision: Option<String>,
creation_generation: u64,
adopted: bool,
managed_path: PathBuf,
) -> Result<InstallReceipt, ModelManagementError> {
validate_model_id(model_id)?;
let artifact_kind = artifact_kind(&managed_path, model_id)?;
let shared_cache_references = collect_shared_references(&managed_path, model_id)?;
let receipt = InstallReceipt {
model_id: model_id.to_string(),
managed_path,
artifact_kind,
source_model_id: source_model_id.to_string(),
source_revision,
creation_generation,
shared_cache_references,
adopted,
};
self.validate_managed_artifact(&receipt)?;
self.write_receipt(&receipt)?;
self.clear_tombstone(model_id)?;
Ok(receipt)
}
pub(crate) fn begin_install_intent(
&self,
receipt: InstallReceipt,
staging_path: Option<&Path>,
) -> Result<(), ModelManagementError> {
validate_model_id(&receipt.model_id)?;
validate_direct_child(&self.models_dir, &receipt.managed_path, &receipt.model_id)?;
let staging_path = staging_path.map(Path::to_path_buf);
let staging_identity = staging_path
.as_deref()
.map(|path| artifact_identity(path, &receipt.model_id))
.transpose()?;
if let Some(path) = staging_path.as_deref() {
validate_direct_child(&self.models_dir, path, &receipt.model_id)?;
}
let intent = InstallJournal {
model_id: receipt.model_id.clone(),
receipt,
staging_path,
staging_identity,
};
let path = self.install_journal_path(&intent.model_id);
if let Some(existing) = self.load_install_intent(&intent.model_id)? {
if existing == intent {
return Ok(());
}
return Err(ModelManagementError::InvalidState {
path,
message: "a different CAR install intent already exists for this model".into(),
});
}
if std::fs::symlink_metadata(&intent.receipt.managed_path).is_ok() {
return Err(ModelManagementError::UnsafeManagedPath {
model_id: intent.model_id,
path: intent.receipt.managed_path,
reason: "pre-existing unreceipted leaf cannot become CAR-owned through an install intent"
.into(),
});
}
write_private_json(&path, &intent)
}
pub(crate) fn install_receipt_for_publication(
&self,
model_id: &str,
source_model_id: &str,
creation_generation: u64,
adopted: bool,
managed_path: PathBuf,
artifact_kind: ManagedArtifactKind,
artifact_source: &Path,
) -> Result<InstallReceipt, ModelManagementError> {
validate_model_id(model_id)?;
let shared_cache_references = match artifact_kind {
ManagedArtifactKind::Symlink => {
vec![artifact_source
.canonicalize()
.map_err(|source| ModelManagementError::Io {
path: artifact_source.to_path_buf(),
source,
})?]
}
ManagedArtifactKind::Directory => collect_shared_references(artifact_source, model_id)?,
ManagedArtifactKind::File => Vec::new(),
};
Ok(InstallReceipt {
model_id: model_id.to_string(),
managed_path,
artifact_kind,
source_model_id: source_model_id.to_string(),
source_revision: None,
creation_generation,
shared_cache_references,
adopted,
})
}
pub(crate) fn resume_install_intent(
&self,
model_id: &str,
) -> Result<Option<InstallReceipt>, ModelManagementError> {
let Some(intent) = self.load_install_intent(model_id)? else {
return Ok(None);
};
if std::fs::symlink_metadata(&intent.receipt.managed_path).is_err() {
if let (Some(staging), Some(identity)) = (
intent.staging_path.as_deref(),
intent.staging_identity.as_ref(),
) {
if std::fs::symlink_metadata(staging).is_ok() {
ensure_artifact_identity(staging, model_id, identity)?;
atomic_rename_noreplace(staging, &intent.receipt.managed_path).map_err(
|source| ModelManagementError::Io {
path: intent.receipt.managed_path.clone(),
source,
},
)?;
sync_directory(&self.models_dir).map_err(|source| {
ModelManagementError::Io {
path: self.models_dir.clone(),
source,
}
})?;
} else {
self.clear_install_intent(model_id)?;
return Ok(None);
}
} else {
self.clear_install_intent(model_id)?;
return Ok(None);
}
}
if let Some(identity) = intent.staging_identity.as_ref() {
ensure_artifact_identity(&intent.receipt.managed_path, model_id, identity)?;
}
self.validate_managed_artifact(&intent.receipt)?;
self.write_receipt(&intent.receipt)?;
self.clear_install_intent(model_id)?;
self.clear_tombstone(model_id)?;
Ok(Some(intent.receipt))
}
fn load_install_intent(
&self,
model_id: &str,
) -> Result<Option<InstallJournal>, ModelManagementError> {
let path = self.install_journal_path(model_id);
let bytes = match read_private_state(&path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(source) => return Err(ModelManagementError::Io { path, source }),
};
let intent: InstallJournal =
serde_json::from_slice(&bytes).map_err(|error| ModelManagementError::InvalidState {
path: path.clone(),
message: error.to_string(),
})?;
ensure_identity(&path, model_id, &intent.model_id)?;
ensure_identity(&path, model_id, &intent.receipt.model_id)?;
validate_direct_child(&self.models_dir, &intent.receipt.managed_path, model_id)?;
if let Some(staging) = intent.staging_path.as_deref() {
validate_direct_child(&self.models_dir, staging, model_id)?;
if !staging
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with(".car-install-"))
{
return Err(ModelManagementError::UnsafeManagedPath {
model_id: model_id.to_string(),
path: staging.to_path_buf(),
reason: "install journal staging path lacks CAR staging identity".into(),
});
}
}
Ok(Some(intent))
}
fn clear_install_intent(&self, model_id: &str) -> Result<(), ModelManagementError> {
let path = self.install_journal_path(model_id);
match std::fs::remove_file(&path) {
Ok(()) => sync_directory(&self.installs_root)
.map_err(|source| ModelManagementError::Io { path, source }),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(source) => Err(ModelManagementError::Io { path, source }),
}
}
pub(crate) fn materialize_managed_projection(
&self,
managed_leaf: &str,
source: &Path,
) -> Result<PathBuf, ModelManagementError> {
validate_managed_leaf(managed_leaf, &self.models_dir)?;
create_private_dir(&self.models_dir)?;
let models_root = canonical_directory(&self.models_dir)?;
let source_parent =
source
.parent()
.ok_or_else(|| ModelManagementError::UnsafeManagedPath {
model_id: managed_leaf.to_string(),
path: source.to_path_buf(),
reason: "source has no parent".into(),
})?;
if canonical_directory(source_parent)? == models_root
&& source == self.models_dir.join(managed_leaf)
{
return Err(ModelManagementError::UnsafeManagedPath {
model_id: managed_leaf.to_string(),
path: source.to_path_buf(),
reason: "pre-existing unreceipted managed projection cannot become CAR-owned"
.into(),
});
}
let target = source
.canonicalize()
.map_err(|source_error| ModelManagementError::Io {
path: source.to_path_buf(),
source: source_error,
})?;
let managed = self.models_dir.join(managed_leaf);
if std::fs::symlink_metadata(&managed).is_ok() {
return Err(ModelManagementError::UnsafeManagedPath {
model_id: managed_leaf.to_string(),
path: managed,
reason: "pre-existing managed projection has no CAR creation receipt".into(),
});
}
static NEXT_PROJECTION: AtomicU64 = AtomicU64::new(1);
let tmp = self.models_dir.join(format!(
".managed-projection.{}.{}",
std::process::id(),
NEXT_PROJECTION.fetch_add(1, Ordering::Relaxed)
));
create_symlink(&target, &tmp)?;
let result = atomic_rename_noreplace(&tmp, &managed).map_err(|source| {
ModelManagementError::UnsafeManagedPath {
model_id: managed_leaf.to_string(),
path: managed.clone(),
reason: format!("managed projection publication raced or failed: {source}"),
}
});
if result.is_err() {
let _ = std::fs::remove_file(&tmp);
}
result?;
sync_directory(&self.models_dir).map_err(|source| ModelManagementError::Io {
path: self.models_dir.clone(),
source,
})?;
Ok(managed)
}
pub(crate) fn create_install_staging(
&self,
model_id: &str,
) -> Result<PathBuf, ModelManagementError> {
validate_model_id(model_id)?;
create_private_dir(&self.models_dir)?;
let digest = hex::encode(Sha256::digest(model_id.as_bytes()));
for _ in 0..16 {
let candidate = self.models_dir.join(format!(
".car-install-{}-{:032x}",
&digest[..16],
rand::random::<u128>()
));
match std::fs::create_dir(&candidate) {
Ok(()) => {
harden_private_directory(&candidate)?;
return Ok(candidate);
}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(source) => {
return Err(ModelManagementError::Io {
path: candidate,
source,
});
}
}
}
Err(ModelManagementError::InvalidState {
path: self.models_dir.clone(),
message: "could not allocate a collision-resistant install staging directory".into(),
})
}
pub(crate) fn publish_install_staging(
&self,
model_id: &str,
staging: &Path,
managed_leaf: &str,
) -> Result<PathBuf, ModelManagementError> {
validate_model_id(model_id)?;
validate_direct_child(&self.models_dir, staging, model_id)?;
if !staging
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with(".car-install-"))
{
return Err(ModelManagementError::UnsafeManagedPath {
model_id: model_id.to_string(),
path: staging.to_path_buf(),
reason: "install staging path lacks CAR staging identity".into(),
});
}
validate_managed_leaf(managed_leaf, &self.models_dir)?;
let managed = self.models_dir.join(managed_leaf);
atomic_rename_noreplace(staging, &managed).map_err(|source| {
ModelManagementError::UnsafeManagedPath {
model_id: model_id.to_string(),
path: managed.clone(),
reason: format!("could not atomically publish CAR-owned install: {source}"),
}
})?;
sync_directory(&self.models_dir).map_err(|source| ModelManagementError::Io {
path: self.models_dir.clone(),
source,
})?;
Ok(managed)
}
pub(crate) fn discard_install_staging(&self, staging: &Path) {
let _ = staging;
}
pub(crate) fn materialize_adopted_projection(
&self,
model_id: &str,
source: &Path,
) -> Result<PathBuf, ModelManagementError> {
validate_model_id(model_id)?;
let managed = self.adopted_projection_path(model_id)?;
let leaf = managed
.file_name()
.and_then(|name| name.to_str())
.expect("adopted projection is an UTF-8 direct child");
self.materialize_managed_projection(leaf, source)
}
pub(crate) fn adopted_projection_path(
&self,
model_id: &str,
) -> Result<PathBuf, ModelManagementError> {
validate_model_id(model_id)?;
let digest = hex::encode(Sha256::digest(model_id.as_bytes()));
Ok(self.models_dir.join(format!(".car-adopted-{digest}")))
}
fn remove_with_mutation(
&self,
model_id: &str,
removal_generation: u64,
) -> Result<RemoveFromCarResult, ModelManagementError> {
let _activity = self.lock_idle_activity(model_id)?;
if let Some(journal) = self.load_removal_journal(model_id)? {
write_private_json(
&self.tombstone_path(model_id),
&RemovalTombstone {
model_id: model_id.to_string(),
removal_generation: journal.removal_generation,
artifact_kind: Some(journal.receipt.artifact_kind),
preserved_shared_cache_references: journal
.receipt
.shared_cache_references
.clone(),
},
)?;
return self.resume_removal_journal(journal);
}
let Some(receipt) = self.load_receipt(model_id)? else {
if let Some(tombstone) = self.load_tombstone(model_id)? {
if let Some(artifact_kind) = tombstone.artifact_kind {
return Ok(RemoveFromCarResult {
model_id: model_id.to_string(),
artifact_kind,
preserved_shared_cache_references: tombstone
.preserved_shared_cache_references,
});
}
}
return Err(ModelManagementError::MissingReceipt {
model_id: model_id.to_string(),
});
};
self.validate_managed_artifact(&receipt)?;
if receipt.artifact_kind == ManagedArtifactKind::Directory {
return Err(ModelManagementError::UnsafeManagedPath {
model_id: model_id.to_string(),
path: receipt.managed_path,
reason: "recursive directory removal is unsupported without object-bound traversal"
.into(),
});
}
let artifact_identity = artifact_identity(&receipt.managed_path, model_id)?;
let quarantine = self.allocate_quarantine(model_id)?;
write_private_json(
&self.tombstone_path(model_id),
&RemovalTombstone {
model_id: model_id.to_string(),
removal_generation,
artifact_kind: Some(receipt.artifact_kind),
preserved_shared_cache_references: receipt.shared_cache_references.clone(),
},
)?;
let journal = RemovalJournal {
model_id: model_id.to_string(),
removal_generation,
receipt,
quarantine_path: quarantine,
artifact_identity,
};
write_private_json(&self.removal_journal_path(model_id), &journal)?;
self.resume_removal_journal(journal)
}
fn lock_idle_activity(&self, model_id: &str) -> Result<File, ModelManagementError> {
let mut activity = self.open_identity_lock(&self.lease_path(model_id), model_id)?;
validate_lock_identity(&mut activity, &self.lease_path(model_id), model_id)?;
match activity.try_lock() {
Ok(()) => Ok(activity),
Err(std::fs::TryLockError::WouldBlock) => Err(ModelManagementError::ModelInUse {
model_id: model_id.to_string(),
}),
Err(std::fs::TryLockError::Error(source)) => Err(ModelManagementError::Io {
path: self.lease_path(model_id),
source,
}),
}
}
fn load_removal_journal(
&self,
model_id: &str,
) -> Result<Option<RemovalJournal>, ModelManagementError> {
let path = self.removal_journal_path(model_id);
let bytes = match read_private_state(&path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(source) => return Err(ModelManagementError::Io { path, source }),
};
let journal: RemovalJournal =
serde_json::from_slice(&bytes).map_err(|error| ModelManagementError::InvalidState {
path: path.clone(),
message: error.to_string(),
})?;
ensure_identity(&path, model_id, &journal.model_id)?;
ensure_identity(&path, model_id, &journal.receipt.model_id)?;
validate_direct_child(&self.models_dir, &journal.receipt.managed_path, model_id)?;
if journal.receipt.artifact_kind == ManagedArtifactKind::Directory {
return Err(ModelManagementError::UnsafeManagedPath {
model_id: model_id.to_string(),
path: journal.receipt.managed_path,
reason: "recursive directory removal journal is unsupported".into(),
});
}
validate_direct_child(&self.models_dir, &journal.quarantine_path, model_id)?;
if !journal
.quarantine_path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with(".car-remove-"))
{
return Err(ModelManagementError::UnsafeManagedPath {
model_id: model_id.to_string(),
path: journal.quarantine_path,
reason: "removal journal has an invalid quarantine path".into(),
});
}
Ok(Some(journal))
}
fn allocate_quarantine(&self, model_id: &str) -> Result<PathBuf, ModelManagementError> {
let digest = hex::encode(Sha256::digest(model_id.as_bytes()));
for _ in 0..16 {
let candidate = self.models_dir.join(format!(
".car-remove-{}-{:032x}",
&digest[..16],
rand::random::<u128>()
));
if std::fs::symlink_metadata(&candidate)
.is_err_and(|error| error.kind() == std::io::ErrorKind::NotFound)
{
return Ok(candidate);
}
}
Err(ModelManagementError::InvalidState {
path: self.models_dir.clone(),
message: "could not allocate a collision-resistant removal quarantine".into(),
})
}
fn resume_removal_journal(
&self,
journal: RemovalJournal,
) -> Result<RemoveFromCarResult, ModelManagementError> {
let model_id = &journal.model_id;
if journal.receipt.artifact_kind == ManagedArtifactKind::Directory {
return Err(ModelManagementError::UnsafeManagedPath {
model_id: model_id.clone(),
path: journal.receipt.managed_path,
reason: "recursive directory removal is unsupported without object-bound traversal"
.into(),
});
}
let original_exists = std::fs::symlink_metadata(&journal.receipt.managed_path).is_ok();
let quarantine_exists = std::fs::symlink_metadata(&journal.quarantine_path).is_ok();
if original_exists && quarantine_exists {
return Err(ModelManagementError::UnsafeManagedPath {
model_id: model_id.clone(),
path: journal.receipt.managed_path.clone(),
reason: "both managed artifact and removal quarantine exist".into(),
});
}
if original_exists {
self.validate_managed_artifact(&journal.receipt)?;
ensure_artifact_identity(
&journal.receipt.managed_path,
model_id,
&journal.artifact_identity,
)?;
validate_direct_child(&self.models_dir, &journal.receipt.managed_path, model_id)?;
ensure_artifact_identity(
&journal.receipt.managed_path,
model_id,
&journal.artifact_identity,
)?;
atomic_rename_noreplace(&journal.receipt.managed_path, &journal.quarantine_path)
.map_err(|source| ModelManagementError::Io {
path: journal.receipt.managed_path.clone(),
source,
})?;
sync_directory(&self.models_dir).map_err(|source| ModelManagementError::Io {
path: self.models_dir.clone(),
source,
})?;
}
if std::fs::symlink_metadata(&journal.quarantine_path).is_ok() {
ensure_artifact_identity(
&journal.quarantine_path,
model_id,
&journal.artifact_identity,
)?;
std::fs::remove_file(&journal.quarantine_path).map_err(|source| {
ModelManagementError::Io {
path: journal.quarantine_path.clone(),
source,
}
})?;
sync_directory(&self.models_dir).map_err(|source| ModelManagementError::Io {
path: self.models_dir.clone(),
source,
})?;
}
let receipt_path = self.receipt_path(model_id);
match std::fs::remove_file(&receipt_path) {
Ok(()) => {
sync_directory(&self.receipts_root).map_err(|source| ModelManagementError::Io {
path: self.receipts_root.clone(),
source,
})?
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(source) => {
return Err(ModelManagementError::Io {
path: receipt_path,
source,
});
}
}
let journal_path = self.removal_journal_path(model_id);
match std::fs::remove_file(&journal_path) {
Ok(()) => {
sync_directory(&self.removals_root).map_err(|source| ModelManagementError::Io {
path: self.removals_root.clone(),
source,
})?
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(source) => {
return Err(ModelManagementError::Io {
path: journal_path,
source,
});
}
}
Ok(RemoveFromCarResult {
model_id: model_id.clone(),
artifact_kind: journal.receipt.artifact_kind,
preserved_shared_cache_references: journal.receipt.shared_cache_references,
})
}
fn resume_pending_quarantines(&self) {
let Ok(entries) = std::fs::read_dir(&self.removals_root) else {
return;
};
for entry in entries.flatten() {
if !is_hashed_state_filename(&entry.file_name()) {
continue;
}
let Ok(bytes) = read_private_state(&entry.path()) else {
continue;
};
let Ok(candidate) = serde_json::from_slice::<RemovalJournal>(&bytes) else {
continue;
};
if entry.path() != self.removal_journal_path(&candidate.model_id) {
continue;
}
let Ok(guard) = self.begin_mutation(&candidate.model_id) else {
continue;
};
let Ok(Some(journal)) = self.load_removal_journal(&candidate.model_id) else {
continue;
};
if std::fs::symlink_metadata(&journal.receipt.managed_path).is_ok() {
continue;
}
let _ = guard.resume_existing(journal);
}
}
fn open_identity_lock(
&self,
path: &Path,
model_id: &str,
) -> Result<File, ModelManagementError> {
let parent = path.parent().expect("lock path has parent");
create_private_dir(parent)?;
match open_private_new(path) {
Ok(mut file) => {
file.lock().map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})?;
let bytes = serde_json::to_vec(&IdentityRecord {
model_id: model_id.to_string(),
})
.map_err(|error| ModelManagementError::InvalidState {
path: path.to_path_buf(),
message: error.to_string(),
})?;
file.write_all(&bytes)
.and_then(|_| file.sync_all())
.map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})?;
file.unlock().map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})?;
return Ok(file);
}
Err(ModelManagementError::Io { source, .. })
if source.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(error) => return Err(error),
}
open_existing_identity_lock(path)
}
fn validate_managed_artifact(
&self,
receipt: &InstallReceipt,
) -> Result<(), ModelManagementError> {
let models_root = validate_models_root(&self.models_dir, &receipt.model_id)?;
if let Some(hf_root) = shared_hugging_face_root() {
let hf_root = crate::resource_policy::normalized_state_root_key(&hf_root);
if models_root.starts_with(&hf_root) || hf_root.starts_with(&models_root) {
return Err(ModelManagementError::UnsafeManagedPath {
model_id: receipt.model_id.clone(),
path: receipt.managed_path.clone(),
reason: "managed models root overlaps the shared Hugging Face cache".into(),
});
}
}
let parent = receipt.managed_path.parent().ok_or_else(|| {
ModelManagementError::UnsafeManagedPath {
model_id: receipt.model_id.clone(),
path: receipt.managed_path.clone(),
reason: "managed path has no parent".into(),
}
})?;
let canonical_parent = canonical_directory(parent)?;
if canonical_parent != models_root || receipt.managed_path == models_root {
return Err(ModelManagementError::UnsafeManagedPath {
model_id: receipt.model_id.clone(),
path: receipt.managed_path.clone(),
reason: "artifact is not a direct child of the managed models root".into(),
});
}
let actual = artifact_kind(&receipt.managed_path, &receipt.model_id)?;
if actual != receipt.artifact_kind {
return Err(ModelManagementError::UnsafeManagedPath {
model_id: receipt.model_id.clone(),
path: receipt.managed_path.clone(),
reason: format!(
"receipt says {:?}, filesystem is {:?}",
receipt.artifact_kind, actual
),
});
}
if actual == ManagedArtifactKind::Symlink {
let target =
receipt
.managed_path
.canonicalize()
.map_err(|source| ModelManagementError::Io {
path: receipt.managed_path.clone(),
source,
})?;
if !receipt
.shared_cache_references
.iter()
.any(|path| path == &target)
{
return Err(ModelManagementError::UnsafeManagedPath {
model_id: receipt.model_id.clone(),
path: receipt.managed_path.clone(),
reason: "symlink target does not match a preserved shared-cache reference"
.into(),
});
}
} else {
let current_references =
collect_shared_references(&receipt.managed_path, &receipt.model_id)?;
if current_references != receipt.shared_cache_references {
return Err(ModelManagementError::UnsafeManagedPath {
model_id: receipt.model_id.clone(),
path: receipt.managed_path.clone(),
reason: "shared-cache references no longer match the install receipt".into(),
});
}
}
Ok(())
}
}
pub(crate) struct ModelMutationGuard {
store: ModelManagementStore,
model_id: String,
_file: File,
}
impl ModelMutationGuard {
pub(crate) fn remove(
self,
removal_generation: u64,
) -> Result<RemoveFromCarResult, ModelManagementError> {
self.store
.remove_with_mutation(&self.model_id, removal_generation)
}
fn resume_existing(
self,
journal: RemovalJournal,
) -> Result<RemoveFromCarResult, ModelManagementError> {
if journal.model_id != self.model_id {
return Err(ModelManagementError::IdentityMismatch {
path: self.store.removal_journal_path(&self.model_id),
expected: self.model_id,
actual: journal.model_id,
});
}
let _activity = self.store.lock_idle_activity(&self.model_id)?;
write_private_json(
&self.store.tombstone_path(&self.model_id),
&RemovalTombstone {
model_id: self.model_id.clone(),
removal_generation: journal.removal_generation,
artifact_kind: Some(journal.receipt.artifact_kind),
preserved_shared_cache_references: journal.receipt.shared_cache_references.clone(),
},
)?;
self.store.resume_removal_journal(journal)
}
}
pub struct ModelLease {
_file: File,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoveFromCarResult {
pub model_id: String,
pub artifact_kind: ManagedArtifactKind,
pub preserved_shared_cache_references: Vec<PathBuf>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct IdentityRecord {
model_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct RemovalTombstone {
model_id: String,
removal_generation: u64,
#[serde(default)]
artifact_kind: Option<ManagedArtifactKind>,
#[serde(default)]
preserved_shared_cache_references: Vec<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct ArtifactIdentity {
kind: ManagedArtifactKind,
#[serde(default)]
device: u64,
#[serde(default)]
inode: u64,
#[serde(default)]
file_len: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct RemovalJournal {
model_id: String,
removal_generation: u64,
receipt: InstallReceipt,
quarantine_path: PathBuf,
artifact_identity: ArtifactIdentity,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct InstallJournal {
model_id: String,
receipt: InstallReceipt,
#[serde(default)]
staging_path: Option<PathBuf>,
#[serde(default)]
staging_identity: Option<ArtifactIdentity>,
}
fn hashed_filename(model_id: &str) -> String {
let digest = Sha256::digest(model_id.as_bytes());
format!("{}.json", hex::encode(digest))
}
fn is_hashed_state_filename(name: &std::ffi::OsStr) -> bool {
let Some(name) = name.to_str() else {
return false;
};
let Some(hex) = name.strip_suffix(".json") else {
return false;
};
hex.len() == 64
&& hex
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
}
fn validate_model_id(model_id: &str) -> Result<(), ModelManagementError> {
if !model_id.is_empty() && model_id.len() <= MAX_MODEL_ID_BYTES {
return Ok(());
}
Err(ModelManagementError::InvalidState {
path: PathBuf::new(),
message: "model id must contain 1..=4096 UTF-8 bytes".into(),
})
}
fn validate_managed_leaf(
managed_leaf: &str,
models_dir: &Path,
) -> Result<(), ModelManagementError> {
if Path::new(managed_leaf).components().count() == 1
&& matches!(
Path::new(managed_leaf).components().next(),
Some(std::path::Component::Normal(_))
)
{
return Ok(());
}
Err(ModelManagementError::UnsafeManagedPath {
model_id: managed_leaf.to_string(),
path: models_dir.join(managed_leaf),
reason: "managed model name must be one ordinary path component".into(),
})
}
fn validate_direct_child(
root: &Path,
candidate: &Path,
model_id: &str,
) -> Result<(), ModelManagementError> {
let canonical_root = validate_models_root(root, model_id)?;
let parent = candidate
.parent()
.ok_or_else(|| ModelManagementError::UnsafeManagedPath {
model_id: model_id.to_string(),
path: candidate.to_path_buf(),
reason: "managed path has no parent".into(),
})?;
if canonical_directory(parent)? != canonical_root {
return Err(ModelManagementError::UnsafeManagedPath {
model_id: model_id.to_string(),
path: candidate.to_path_buf(),
reason: "managed path is not a direct child of the models root".into(),
});
}
Ok(())
}
fn validate_models_root(root: &Path, model_id: &str) -> Result<PathBuf, ModelManagementError> {
let metadata = std::fs::symlink_metadata(root).map_err(|source| ModelManagementError::Io {
path: root.to_path_buf(),
source,
})?;
if metadata.file_type().is_symlink() {
return Err(ModelManagementError::UnsafeManagedPath {
model_id: model_id.to_string(),
path: root.to_path_buf(),
reason: "managed models root must not be a symlink".into(),
});
}
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(ModelManagementError::UnsafeManagedPath {
model_id: model_id.to_string(),
path: root.to_path_buf(),
reason: "managed models root must not be a reparse point".into(),
});
}
}
if !metadata.is_dir() {
return Err(ModelManagementError::UnsafeManagedPath {
model_id: model_id.to_string(),
path: root.to_path_buf(),
reason: "managed models root is not a directory".into(),
});
}
canonical_directory(root)
}
fn ensure_identity(path: &Path, expected: &str, actual: &str) -> Result<(), ModelManagementError> {
if expected == actual {
return Ok(());
}
Err(ModelManagementError::IdentityMismatch {
path: path.to_path_buf(),
expected: expected.to_string(),
actual: actual.to_string(),
})
}
fn validate_lock_identity(
file: &mut File,
path: &Path,
model_id: &str,
) -> Result<(), ModelManagementError> {
file.rewind().map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})?;
let mut bytes = Vec::new();
file.take(16 * 1024)
.read_to_end(&mut bytes)
.map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})?;
let record: IdentityRecord =
serde_json::from_slice(&bytes).map_err(|error| ModelManagementError::InvalidState {
path: path.to_path_buf(),
message: error.to_string(),
})?;
ensure_identity(path, model_id, &record.model_id)
}
fn canonical_directory(path: &Path) -> Result<PathBuf, ModelManagementError> {
path.canonicalize()
.map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})
}
fn shared_hugging_face_root() -> Option<PathBuf> {
std::env::var_os("HF_HOME").map(PathBuf::from).or_else(|| {
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(|home| PathBuf::from(home).join(".cache/huggingface"))
})
}
fn artifact_identity(
path: &Path,
model_id: &str,
) -> Result<ArtifactIdentity, ModelManagementError> {
let metadata = std::fs::symlink_metadata(path).map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})?;
reject_windows_reparse(&metadata, path, model_id)?;
let kind = if metadata.file_type().is_symlink() {
ManagedArtifactKind::Symlink
} else if metadata.is_dir() {
ManagedArtifactKind::Directory
} else if metadata.is_file() {
ManagedArtifactKind::File
} else {
return Err(ModelManagementError::UnsafeManagedPath {
model_id: model_id.to_string(),
path: path.to_path_buf(),
reason: "unsupported artifact type".into(),
});
};
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
Ok(ArtifactIdentity {
kind,
device: metadata.dev(),
inode: metadata.ino(),
file_len: metadata.len(),
})
}
#[cfg(not(unix))]
{
Ok(ArtifactIdentity {
kind,
device: 0,
inode: 0,
file_len: metadata.len(),
})
}
}
fn ensure_artifact_identity(
path: &Path,
model_id: &str,
expected: &ArtifactIdentity,
) -> Result<(), ModelManagementError> {
let actual = artifact_identity(path, model_id)?;
if &actual == expected {
return Ok(());
}
Err(ModelManagementError::UnsafeManagedPath {
model_id: model_id.to_string(),
path: path.to_path_buf(),
reason: "managed artifact identity changed during removal".into(),
})
}
fn artifact_kind(path: &Path, model_id: &str) -> Result<ManagedArtifactKind, ModelManagementError> {
let metadata = std::fs::symlink_metadata(path).map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})?;
reject_windows_reparse(&metadata, path, model_id)?;
if metadata.file_type().is_symlink() {
Ok(ManagedArtifactKind::Symlink)
} else if metadata.is_dir() {
Ok(ManagedArtifactKind::Directory)
} else if metadata.is_file() {
Ok(ManagedArtifactKind::File)
} else {
Err(ModelManagementError::UnsafeManagedPath {
model_id: model_id.to_string(),
path: path.to_path_buf(),
reason: "unsupported artifact type".into(),
})
}
}
fn collect_shared_references(
path: &Path,
model_id: &str,
) -> Result<Vec<PathBuf>, ModelManagementError> {
let metadata = std::fs::symlink_metadata(path).map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})?;
if metadata.file_type().is_symlink() {
return path
.canonicalize()
.map(|target| vec![target])
.map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
});
}
if !metadata.is_dir() {
return Ok(Vec::new());
}
let mut references = Vec::new();
for entry in std::fs::read_dir(path).map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})? {
let entry = entry.map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})?;
let entry_path = entry.path();
let entry_metadata =
std::fs::symlink_metadata(&entry_path).map_err(|source| ModelManagementError::Io {
path: entry_path.clone(),
source,
})?;
if entry_metadata.file_type().is_symlink() {
let target = entry_path
.canonicalize()
.map_err(|source| ModelManagementError::Io {
path: entry_path.clone(),
source,
})?;
if std::fs::metadata(&target)
.map_err(|source| ModelManagementError::Io {
path: target.clone(),
source,
})?
.is_dir()
{
return Err(ModelManagementError::UnsafeManagedPath {
model_id: model_id.to_string(),
path: entry_path,
reason: "managed directory contains a directory symlink".into(),
});
}
references.push(target);
} else if entry_metadata.is_dir() {
references.extend(collect_shared_references(&entry_path, model_id)?);
}
}
references.sort();
references.dedup();
Ok(references)
}
#[cfg(unix)]
fn create_symlink(target: &Path, link: &Path) -> Result<(), ModelManagementError> {
std::os::unix::fs::symlink(target, link).map_err(|source| ModelManagementError::Io {
path: link.to_path_buf(),
source,
})
}
#[cfg(windows)]
fn create_symlink(target: &Path, link: &Path) -> Result<(), ModelManagementError> {
let result = if target.is_dir() {
std::os::windows::fs::symlink_dir(target, link)
} else {
std::os::windows::fs::symlink_file(target, link)
};
result.map_err(|source| ModelManagementError::Io {
path: link.to_path_buf(),
source,
})
}
fn create_private_dir(path: &Path) -> Result<(), ModelManagementError> {
std::fs::create_dir_all(path).map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})?;
harden_private_directory(path)?;
Ok(())
}
fn harden_private_directory(path: &Path) -> Result<(), ModelManagementError> {
let metadata = std::fs::symlink_metadata(path).map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})?;
if metadata.file_type().is_symlink() {
return Err(ModelManagementError::UnsafeManagedPath {
model_id: "model-management-state".into(),
path: path.to_path_buf(),
reason: "private model-management directory must not be a symlink".into(),
});
}
reject_windows_reparse(&metadata, path, "model-management-state")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700)).map_err(
|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
},
)?;
}
Ok(())
}
fn reject_windows_reparse(
metadata: &std::fs::Metadata,
path: &Path,
model_id: &str,
) -> Result<(), ModelManagementError> {
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(ModelManagementError::UnsafeManagedPath {
model_id: model_id.to_string(),
path: path.to_path_buf(),
reason: "model-management path is a Windows reparse point".into(),
});
}
}
#[cfg(not(windows))]
let _ = (metadata, path, model_id);
Ok(())
}
fn open_private_new(path: &Path) -> Result<File, ModelManagementError> {
let mut options = OpenOptions::new();
options.read(true).write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
options
.open(path)
.map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})
}
fn open_existing_identity_lock(path: &Path) -> Result<File, ModelManagementError> {
let metadata = std::fs::symlink_metadata(path).map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})?;
if metadata.file_type().is_symlink() {
return Err(ModelManagementError::InvalidState {
path: path.to_path_buf(),
message: "model-management lock sentinel must not be a symlink".into(),
});
}
reject_windows_reparse(&metadata, path, "model-management-lock")?;
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if metadata.nlink() != 1 {
return Err(ModelManagementError::InvalidState {
path: path.to_path_buf(),
message: "model-management lock sentinel must not be hard-linked".into(),
});
}
}
let mut options = OpenOptions::new();
options.read(true).write(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(libc::O_NOFOLLOW);
}
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
}
let file = options
.open(path)
.map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})?;
let opened_metadata = file.metadata().map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})?;
if !opened_metadata.is_file() {
return Err(ModelManagementError::InvalidState {
path: path.to_path_buf(),
message: "model-management lock sentinel is not a regular file".into(),
});
}
reject_windows_reparse(&opened_metadata, path, "model-management-lock")?;
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if opened_metadata.nlink() != 1 {
return Err(ModelManagementError::InvalidState {
path: path.to_path_buf(),
message: "opened model-management lock sentinel is hard-linked".into(),
});
}
}
Ok(file)
}
fn read_private_state(path: &Path) -> std::io::Result<Vec<u8>> {
let mut options = OpenOptions::new();
options.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(libc::O_NOFOLLOW);
}
let file = options.open(path)?;
let metadata = file.metadata()?;
if !metadata.is_file() || metadata.len() > MAX_STATE_BYTES {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"model-management state must be a bounded regular file",
));
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if metadata.nlink() != 1 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"model-management state must not be hard-linked",
));
}
}
let mut bytes = Vec::with_capacity(metadata.len() as usize);
file.take(MAX_STATE_BYTES + 1).read_to_end(&mut bytes)?;
if bytes.len() as u64 > MAX_STATE_BYTES {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"model-management state exceeds the size limit",
));
}
Ok(bytes)
}
fn write_private_json<T: Serialize>(path: &Path, value: &T) -> Result<(), ModelManagementError> {
let _guard = state_mutation_lock()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let parent = path
.parent()
.expect("model-management record path has a parent");
create_private_dir(parent)?;
let tmp = (0..16)
.map(|_| {
parent.join(format!(
".{}.{}-{:032x}.tmp",
std::process::id(),
path.file_name()
.and_then(|name| name.to_str())
.unwrap_or("state"),
rand::random::<u128>()
))
})
.find(|candidate| {
std::fs::symlink_metadata(candidate)
.is_err_and(|error| error.kind() == std::io::ErrorKind::NotFound)
})
.ok_or_else(|| ModelManagementError::InvalidState {
path: parent.to_path_buf(),
message: "could not allocate collision-resistant state staging file".into(),
})?;
let bytes =
serde_json::to_vec_pretty(value).map_err(|error| ModelManagementError::InvalidState {
path: path.to_path_buf(),
message: error.to_string(),
})?;
let result = (|| {
let mut file = open_private_new(&tmp)?;
file.write_all(&bytes)
.and_then(|_| file.sync_all())
.map_err(|source| ModelManagementError::Io {
path: tmp.clone(),
source,
})?;
atomic_replace(&tmp, path).map_err(|source| ModelManagementError::Io {
path: path.to_path_buf(),
source,
})?;
car_secrets::harden_owner_only(path);
sync_directory(parent).map_err(|source| ModelManagementError::Io {
path: parent.to_path_buf(),
source,
})
})();
if result.is_err() {
let _ = std::fs::remove_file(&tmp);
}
result
}
fn state_mutation_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
#[cfg(not(windows))]
fn atomic_replace(source: &Path, destination: &Path) -> std::io::Result<()> {
std::fs::rename(source, destination)
}
#[cfg(target_os = "macos")]
fn atomic_rename_noreplace(source: &Path, destination: &Path) -> std::io::Result<()> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let source = CString::new(source.as_os_str().as_bytes())
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidInput, error))?;
let destination = CString::new(destination.as_os_str().as_bytes())
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidInput, error))?;
let result =
unsafe { libc::renamex_np(source.as_ptr(), destination.as_ptr(), libc::RENAME_EXCL) };
if result == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
#[cfg(target_os = "linux")]
fn atomic_rename_noreplace(source: &Path, destination: &Path) -> std::io::Result<()> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let source = CString::new(source.as_os_str().as_bytes())
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidInput, error))?;
let destination = CString::new(destination.as_os_str().as_bytes())
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidInput, error))?;
let result = unsafe {
libc::renameat2(
libc::AT_FDCWD,
source.as_ptr(),
libc::AT_FDCWD,
destination.as_ptr(),
libc::RENAME_NOREPLACE,
)
};
if result == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
#[cfg(windows)]
fn atomic_rename_noreplace(source: &Path, destination: &Path) -> std::io::Result<()> {
use std::os::windows::ffi::OsStrExt;
const MOVEFILE_WRITE_THROUGH: u32 = 0x8;
#[link(name = "kernel32")]
unsafe extern "system" {
fn MoveFileExW(existing: *const u16, new: *const u16, flags: u32) -> i32;
}
let source = source
.as_os_str()
.encode_wide()
.chain(Some(0))
.collect::<Vec<_>>();
let destination = destination
.as_os_str()
.encode_wide()
.chain(Some(0))
.collect::<Vec<_>>();
let moved = unsafe {
MoveFileExW(
source.as_ptr(),
destination.as_ptr(),
MOVEFILE_WRITE_THROUGH,
)
};
if moved == 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(())
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
fn atomic_rename_noreplace(_source: &Path, _destination: &Path) -> std::io::Result<()> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"atomic no-replace publication is unavailable on this platform",
))
}
#[cfg(windows)]
fn atomic_replace(source: &Path, destination: &Path) -> std::io::Result<()> {
use std::os::windows::ffi::OsStrExt;
const MOVEFILE_REPLACE_EXISTING: u32 = 0x1;
const MOVEFILE_WRITE_THROUGH: u32 = 0x8;
#[link(name = "kernel32")]
unsafe extern "system" {
fn MoveFileExW(existing: *const u16, new: *const u16, flags: u32) -> i32;
}
let source = source
.as_os_str()
.encode_wide()
.chain(Some(0))
.collect::<Vec<_>>();
let destination = destination
.as_os_str()
.encode_wide()
.chain(Some(0))
.collect::<Vec<_>>();
let replaced = unsafe {
MoveFileExW(
source.as_ptr(),
destination.as_ptr(),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
)
};
if replaced == 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(())
}
}
#[cfg(unix)]
fn sync_directory(path: &Path) -> std::io::Result<()> {
File::open(path)?.sync_all()
}
#[cfg(test)]
mod tests {
use super::*;
fn directory_receipt(model_id: &str, managed_path: PathBuf) -> InstallReceipt {
InstallReceipt {
model_id: model_id.into(),
managed_path,
artifact_kind: ManagedArtifactKind::Directory,
source_model_id: model_id.into(),
source_revision: None,
creation_generation: 1,
shared_cache_references: vec![],
adopted: false,
}
}
#[test]
fn raw_receipt_identity_is_private_and_collision_fails_closed() {
let root = tempfile::tempdir().unwrap();
let models = root.path().join("models");
std::fs::create_dir_all(&models).unwrap();
let store = ModelManagementStore::new(root.path().join("state"), models.clone());
let receipt = directory_receipt("org/model", models.join("Model"));
store.write_receipt(&receipt).unwrap();
assert_eq!(store.load_receipt("org/model").unwrap(), Some(receipt));
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(
std::fs::metadata(store.receipt_path("org/model"))
.unwrap()
.permissions()
.mode()
& 0o077,
0
);
}
let collision = directory_receipt("different/model", models.join("Other"));
std::fs::write(
store.receipt_path("org/model"),
serde_json::to_vec(&collision).unwrap(),
)
.unwrap();
assert!(matches!(
store.load_receipt("org/model"),
Err(ModelManagementError::IdentityMismatch { .. })
));
}
#[test]
fn journaled_removal_rejects_leaf_identity_swap() {
let root = tempfile::tempdir().unwrap();
let models = root.path().join("models");
std::fs::create_dir_all(&models).unwrap();
let store = ModelManagementStore::new(root.path().join("state"), models.clone());
let managed = models.join("Managed");
std::fs::create_dir_all(&managed).unwrap();
std::fs::write(managed.join("weights"), b"original").unwrap();
let receipt = directory_receipt("org/model", managed.clone());
store.write_receipt(&receipt).unwrap();
let journal = RemovalJournal {
model_id: "org/model".into(),
removal_generation: 2,
artifact_identity: artifact_identity(&managed, "org/model").unwrap(),
receipt,
quarantine_path: store.allocate_quarantine("org/model").unwrap(),
};
write_private_json(&store.removal_journal_path("org/model"), &journal).unwrap();
let displaced = models.join("displaced-original");
std::fs::rename(&managed, &displaced).unwrap();
std::fs::create_dir_all(&managed).unwrap();
std::fs::write(managed.join("sentinel"), b"replacement").unwrap();
assert!(matches!(
store.begin_mutation("org/model").unwrap().remove(2),
Err(ModelManagementError::UnsafeManagedPath { .. })
));
assert_eq!(
std::fs::read(managed.join("sentinel")).unwrap(),
b"replacement"
);
}
#[test]
fn journaled_removal_rejects_models_parent_swap() {
let root = tempfile::tempdir().unwrap();
let models = root.path().join("models");
std::fs::create_dir_all(&models).unwrap();
let store = ModelManagementStore::new(root.path().join("state"), models.clone());
let managed = models.join("Managed");
std::fs::create_dir_all(&managed).unwrap();
std::fs::write(managed.join("weights"), b"original").unwrap();
let receipt = directory_receipt("org/model", managed.clone());
store.write_receipt(&receipt).unwrap();
let journal = RemovalJournal {
model_id: "org/model".into(),
removal_generation: 2,
artifact_identity: artifact_identity(&managed, "org/model").unwrap(),
receipt,
quarantine_path: store.allocate_quarantine("org/model").unwrap(),
};
write_private_json(&store.removal_journal_path("org/model"), &journal).unwrap();
std::fs::rename(&models, root.path().join("models-original")).unwrap();
std::fs::create_dir_all(&managed).unwrap();
std::fs::write(managed.join("sentinel"), b"replacement-parent").unwrap();
assert!(matches!(
store.begin_mutation("org/model").unwrap().remove(2),
Err(ModelManagementError::UnsafeManagedPath { .. })
));
assert_eq!(
std::fs::read(managed.join("sentinel")).unwrap(),
b"replacement-parent"
);
}
#[test]
fn directory_receipts_fail_closed_without_recursive_cleanup() {
let root = tempfile::tempdir().unwrap();
let models = root.path().join("models");
std::fs::create_dir_all(&models).unwrap();
let store = ModelManagementStore::new(root.path().join("state"), models.clone());
let managed = models.join("Managed");
std::fs::create_dir_all(&managed).unwrap();
std::fs::write(managed.join("shared-sentinel"), b"preserve").unwrap();
store
.write_receipt(&directory_receipt("org/model", managed.clone()))
.unwrap();
assert!(!store.can_remove("org/model").unwrap());
let error = store
.begin_mutation("org/model")
.unwrap()
.remove(2)
.unwrap_err();
assert!(matches!(
error,
ModelManagementError::UnsafeManagedPath { .. }
));
assert_eq!(
std::fs::read(managed.join("shared-sentinel")).unwrap(),
b"preserve"
);
}
#[test]
fn failed_directory_staging_is_left_for_object_bound_cleanup() {
let root = tempfile::tempdir().unwrap();
let models = root.path().join("models");
std::fs::create_dir_all(&models).unwrap();
let store = ModelManagementStore::new(root.path().join("state"), models);
let staging = store.create_install_staging("org/model").unwrap();
std::fs::write(staging.join("shared-sentinel"), b"preserve").unwrap();
store.discard_install_staging(&staging);
assert_eq!(
std::fs::read(staging.join("shared-sentinel")).unwrap(),
b"preserve"
);
}
#[cfg(unix)]
#[test]
fn completed_symlink_removal_retry_is_idempotent() {
use std::os::unix::fs::symlink;
let root = tempfile::tempdir().unwrap();
let models = root.path().join("models");
let shared = root.path().join("shared");
std::fs::create_dir_all(&models).unwrap();
std::fs::create_dir_all(&shared).unwrap();
std::fs::write(shared.join("sentinel"), b"preserve").unwrap();
let managed = models.join("Managed");
symlink(&shared, &managed).unwrap();
let store = ModelManagementStore::new(root.path().join("state"), models);
let receipt = InstallReceipt {
model_id: "org/model".into(),
managed_path: managed,
artifact_kind: ManagedArtifactKind::Symlink,
source_model_id: "org/model".into(),
source_revision: None,
creation_generation: 1,
shared_cache_references: vec![shared.canonicalize().unwrap()],
adopted: false,
};
store.write_receipt(&receipt).unwrap();
let first = store
.begin_mutation("org/model")
.unwrap()
.remove(2)
.unwrap();
let retry = store
.begin_mutation("org/model")
.unwrap()
.remove(3)
.unwrap();
assert_eq!(retry, first);
assert!(shared.join("sentinel").exists());
}
#[test]
fn pulled_directory_publish_before_receipt_resumes_from_install_intent() {
let root = tempfile::tempdir().unwrap();
let models = root.path().join("models");
std::fs::create_dir_all(&models).unwrap();
let store = ModelManagementStore::new(root.path().join("state"), models.clone());
let staging = store.create_install_staging("org/model").unwrap();
std::fs::write(staging.join("weights"), b"owned").unwrap();
let receipt = store
.install_receipt_for_publication(
"org/model",
"org/model",
1,
false,
models.join("Managed"),
ManagedArtifactKind::Directory,
&staging,
)
.unwrap();
store.begin_install_intent(receipt, Some(&staging)).unwrap();
store
.publish_install_staging("org/model", &staging, "Managed")
.unwrap();
assert!(store.load_receipt("org/model").unwrap().is_none());
let resumed = store.resume_install_intent("org/model").unwrap().unwrap();
assert_eq!(resumed.managed_path, models.join("Managed"));
assert!(store.load_receipt("org/model").unwrap().is_some());
assert!(!store.can_remove("org/model").unwrap());
}
#[cfg(unix)]
#[test]
fn adopted_projection_publish_before_receipt_resumes_from_install_intent() {
let root = tempfile::tempdir().unwrap();
let models = root.path().join("models");
let source = root.path().join("hand-installed");
std::fs::create_dir_all(&models).unwrap();
std::fs::create_dir_all(&source).unwrap();
std::fs::write(source.join("sentinel"), b"preserve").unwrap();
let store = ModelManagementStore::new(root.path().join("state"), models);
let managed = store.adopted_projection_path("org/model").unwrap();
let receipt = store
.install_receipt_for_publication(
"org/model",
"org/model",
1,
true,
managed.clone(),
ManagedArtifactKind::Symlink,
&source,
)
.unwrap();
store.begin_install_intent(receipt, None).unwrap();
store
.materialize_adopted_projection("org/model", &source)
.unwrap();
assert!(store.load_receipt("org/model").unwrap().is_none());
let resumed = store.resume_install_intent("org/model").unwrap().unwrap();
assert_eq!(resumed.managed_path, managed);
assert!(store.can_remove("org/model").unwrap());
assert!(source.join("sentinel").exists());
}
#[cfg(unix)]
#[test]
fn install_intent_never_claims_a_preexisting_unreceipted_leaf() {
use std::os::unix::fs::symlink;
let root = tempfile::tempdir().unwrap();
let models = root.path().join("models");
let source = root.path().join("shared");
std::fs::create_dir_all(&models).unwrap();
std::fs::create_dir_all(&source).unwrap();
let managed = models.join("Managed");
symlink(&source, &managed).unwrap();
let store = ModelManagementStore::new(root.path().join("state"), models);
let receipt = store
.install_receipt_for_publication(
"org/model",
"org/model",
1,
false,
managed.clone(),
ManagedArtifactKind::Symlink,
&source,
)
.unwrap();
let error = store.begin_install_intent(receipt, None).unwrap_err();
assert!(matches!(
error,
ModelManagementError::UnsafeManagedPath { .. }
));
assert!(managed.exists());
assert!(store.load_receipt("org/model").unwrap().is_none());
}
#[test]
fn forged_install_journal_paths_fail_before_filesystem_mutation() {
let root = tempfile::tempdir().unwrap();
let models = root.path().join("models");
std::fs::create_dir_all(&models).unwrap();
let outside = root.path().join("outside");
std::fs::create_dir_all(&outside).unwrap();
std::fs::write(outside.join("sentinel"), b"preserve").unwrap();
let store = ModelManagementStore::new(root.path().join("state"), models.clone());
let forged_managed = InstallJournal {
model_id: "org/model".into(),
receipt: directory_receipt("org/model", outside.clone()),
staging_path: None,
staging_identity: None,
};
write_private_json(&store.install_journal_path("org/model"), &forged_managed).unwrap();
assert!(matches!(
store.resume_install_intent("org/model"),
Err(ModelManagementError::UnsafeManagedPath { .. })
));
assert!(outside.join("sentinel").exists());
let forged_staging = InstallJournal {
model_id: "org/other".into(),
receipt: directory_receipt("org/other", models.join("Other")),
staging_path: Some(outside.clone()),
staging_identity: Some(artifact_identity(&outside, "org/other").unwrap()),
};
write_private_json(&store.install_journal_path("org/other"), &forged_staging).unwrap();
assert!(matches!(
store.resume_install_intent("org/other"),
Err(ModelManagementError::UnsafeManagedPath { .. })
));
assert!(outside.join("sentinel").exists());
}
#[cfg(unix)]
#[test]
fn startup_ignores_noncanonical_removal_journal_without_touching_valid_model() {
use std::os::unix::fs::symlink;
let root = tempfile::tempdir().unwrap();
let state = root.path().join("state");
let models = root.path().join("models");
let shared = root.path().join("shared");
let outside = root.path().join("outside");
std::fs::create_dir_all(&models).unwrap();
std::fs::create_dir_all(&shared).unwrap();
std::fs::create_dir_all(&outside).unwrap();
std::fs::write(shared.join("sentinel"), b"shared").unwrap();
std::fs::write(outside.join("sentinel"), b"outside").unwrap();
let managed = models.join("Managed");
symlink(&shared, &managed).unwrap();
let store = ModelManagementStore::new(state.clone(), models.clone());
let legitimate = InstallReceipt {
model_id: "org/model".into(),
managed_path: managed.clone(),
artifact_kind: ManagedArtifactKind::Symlink,
source_model_id: "org/model".into(),
source_revision: None,
creation_generation: 1,
shared_cache_references: vec![shared.canonicalize().unwrap()],
adopted: false,
};
store.write_receipt(&legitimate).unwrap();
create_private_dir(&store.removals_root).unwrap();
let forged = RemovalJournal {
model_id: "org/model".into(),
removal_generation: 99,
receipt: InstallReceipt {
managed_path: outside.join("missing-leaf"),
..legitimate
},
quarantine_path: models.join(".car-remove-forged"),
artifact_identity: ArtifactIdentity {
kind: ManagedArtifactKind::Symlink,
device: 1,
inode: 1,
file_len: 0,
},
};
write_private_json(&store.removals_root.join("junk.json"), &forged).unwrap();
drop(store);
let recovered = ModelManagementStore::new(state, models);
assert!(std::fs::symlink_metadata(&managed).is_ok());
assert!(recovered.car_enabled("org/model").unwrap());
assert_eq!(std::fs::read(shared.join("sentinel")).unwrap(), b"shared");
assert_eq!(std::fs::read(outside.join("sentinel")).unwrap(), b"outside");
}
}
#[cfg(not(unix))]
fn sync_directory(_path: &Path) -> std::io::Result<()> {
Ok(())
}