Confine image preparation beneath retained storage authority

This commit is contained in:
ms2k 2026-07-16 20:41:26 +09:00
commit c0b139256e

View file

@ -3,20 +3,27 @@
use std::fmt;
use std::fs::{File, OpenOptions};
use std::io;
use std::os::fd::AsFd;
use std::os::unix::ffi::OsStringExt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use lazaret_core::codec::{self, CodecError};
use lazaret_core::identity::{PrepareOperationId, PreparedArtifactId};
use lazaret_core::configuration::ConfinedPathPlan;
use lazaret_core::identity::{MountIdentity, PrepareOperationId, PreparedArtifactId};
use lazaret_core::records::{
ArtifactLease, ArtifactState, PrepareOperation, PrepareStageState, PreparedArtifact,
StorageCatalog, StorageCatalogMetadata,
};
use lazaret_linux::filesystem::{ExclusiveFileLock, try_exclusive_flock};
use lazaret_linux::filesystem::{
ExclusiveFileLock, mount_identity, require_mount_identity, try_exclusive_flock,
};
use redb::{
Database, ReadTransaction, ReadableDatabase, ReadableTable, TableDefinition, WriteTransaction,
};
use rustix::fs::{Mode, OFlags, ResolveFlags, openat2};
const METADATA: TableDefinition<u8, &[u8]> = TableDefinition::new("catalog_metadata");
const ARTIFACTS: TableDefinition<&[u8], &[u8]> = TableDefinition::new("prepared_artifacts");
@ -26,23 +33,77 @@ const METADATA_KEY: u8 = 0;
const DEFAULT_LOCK_WAIT: Duration = Duration::from_secs(5);
const LOCK_RETRY_INTERVAL: Duration = Duration::from_millis(2);
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct Catalog {
database_path: PathBuf,
lock_path: PathBuf,
location: CatalogLocation,
lock_wait: Duration,
}
#[derive(Clone)]
enum CatalogLocation {
Ambient {
database_path: PathBuf,
lock_path: PathBuf,
},
StorageRoot(Arc<StorageRootCatalog>),
}
struct StorageRootCatalog {
root: File,
root_label: PathBuf,
identity: MountIdentity,
}
impl fmt::Debug for Catalog {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let location = match &self.location {
CatalogLocation::Ambient { .. } => "ambient",
CatalogLocation::StorageRoot(_) => "storage-root",
};
formatter
.debug_struct("Catalog")
.field("location", &location)
.field("lock_wait", &self.lock_wait)
.finish()
}
}
impl Catalog {
#[must_use]
pub fn new(database_path: impl Into<PathBuf>, lock_path: impl Into<PathBuf>) -> Self {
Self {
database_path: database_path.into(),
lock_path: lock_path.into(),
location: CatalogLocation::Ambient {
database_path: database_path.into(),
lock_path: lock_path.into(),
},
lock_wait: DEFAULT_LOCK_WAIT,
}
}
/// Binds the fixed catalog children to the validated Storage Root authority.
///
/// # Errors
/// Returns an error when the exact Storage Root cannot be retained and identified.
pub fn under_storage_root(storage_root: &ConfinedPathPlan) -> Result<Self, CatalogError> {
let root = File::from(
storage_root
.open_authority()
.map_err(|error| CatalogError::Io(io::Error::other(error.to_string())))?,
);
let identity = mount_identity(&root).map_err(CatalogError::Io)?;
let root_label = PathBuf::from(std::ffi::OsString::from_vec(
storage_root.canonical_label().as_bytes().to_vec(),
));
Ok(Self {
location: CatalogLocation::StorageRoot(Arc::new(StorageRootCatalog {
root,
root_label,
identity,
})),
lock_wait: DEFAULT_LOCK_WAIT,
})
}
#[must_use]
pub fn with_lock_wait(mut self, lock_wait: Duration) -> Self {
self.lock_wait = lock_wait;
@ -56,10 +117,11 @@ impl Catalog {
/// Returns an error if locking, creation, or the initialization transaction fails.
pub fn initialize(&self) -> Result<(), CatalogError> {
self.with_lock(|| {
if self.database_path.exists() {
if self.database_exists()? {
return Err(CatalogError::AlreadyInitialized);
}
let database = Database::create(&self.database_path).map_err(CatalogError::redb)?;
let database_path = self.database_path_for_handoff()?;
let database = Database::create(database_path).map_err(CatalogError::redb)?;
let transaction = database.begin_write().map_err(CatalogError::redb)?;
{
let mut metadata = transaction
@ -538,7 +600,8 @@ impl Catalog {
operation: impl FnOnce(&ReadTransaction) -> Result<T, CatalogError>,
) -> Result<T, CatalogError> {
self.with_lock(|| {
let database = Database::open(&self.database_path).map_err(CatalogError::redb)?;
let database_path = self.database_path_for_handoff()?;
let database = Database::open(database_path).map_err(CatalogError::redb)?;
let transaction = database.begin_read().map_err(CatalogError::redb)?;
validate_read_metadata(&transaction)?;
operation(&transaction)
@ -550,7 +613,8 @@ impl Catalog {
operation: impl FnOnce(&WriteTransaction) -> Result<T, CatalogError>,
) -> Result<T, CatalogError> {
self.with_lock(|| {
let database = Database::open(&self.database_path).map_err(CatalogError::redb)?;
let database_path = self.database_path_for_handoff()?;
let database = Database::open(database_path).map_err(CatalogError::redb)?;
let transaction = database.begin_write().map_err(CatalogError::redb)?;
validate_write_metadata(&transaction)?;
let result = operation(&transaction)?;
@ -563,9 +627,43 @@ impl Catalog {
&self,
operation: impl FnOnce() -> Result<T, CatalogError>,
) -> Result<T, CatalogError> {
let _lock = acquire_lock(&self.lock_path, self.lock_wait)?;
let _lock = match &self.location {
CatalogLocation::Ambient { lock_path, .. } => acquire_lock(lock_path, self.lock_wait)?,
CatalogLocation::StorageRoot(location) => {
acquire_storage_root_lock(location, self.lock_wait)?
}
};
operation()
}
fn database_exists(&self) -> Result<bool, CatalogError> {
match &self.location {
CatalogLocation::Ambient { database_path, .. } => Ok(database_path.exists()),
CatalogLocation::StorageRoot(location) => match open_storage_root_child(
location,
"catalog.redb",
OFlags::RDONLY | OFlags::NOFOLLOW,
Mode::empty(),
) {
Ok(_) => Ok(true),
Err(CatalogError::Io(error)) if error.kind() == io::ErrorKind::NotFound => {
Ok(false)
}
Err(error) => Err(error),
},
}
}
fn database_path_for_handoff(&self) -> Result<PathBuf, CatalogError> {
match &self.location {
CatalogLocation::Ambient { database_path, .. } => Ok(database_path.clone()),
CatalogLocation::StorageRoot(location) => {
require_mount_identity(&location.root_label, location.identity)
.map_err(CatalogError::Io)?;
Ok(location.root_label.join("catalog.redb"))
}
}
}
}
fn acquire_lock(path: &Path, wait: Duration) -> Result<ExclusiveFileLock, CatalogError> {
@ -586,6 +684,56 @@ fn acquire_lock(path: &Path, wait: Duration) -> Result<ExclusiveFileLock, Catalo
}
}
fn acquire_storage_root_lock(
location: &StorageRootCatalog,
wait: Duration,
) -> Result<ExclusiveFileLock, CatalogError> {
let deadline = Instant::now() + wait;
loop {
let file = File::from(open_storage_root_child(
location,
"storage-catalog.lock",
OFlags::CREATE | OFlags::RDWR | OFlags::NOFOLLOW,
Mode::from_raw_mode(0o600),
)?);
match try_exclusive_flock(file) {
Ok(lock) => return Ok(lock),
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
let now = Instant::now();
if now >= deadline {
return Err(CatalogError::LockTimeout);
}
thread::sleep(LOCK_RETRY_INTERVAL.min(deadline.saturating_duration_since(now)));
}
Err(error) => return Err(CatalogError::Io(error)),
}
}
}
fn open_storage_root_child(
location: &StorageRootCatalog,
name: &str,
flags: OFlags,
mode: Mode,
) -> Result<std::os::fd::OwnedFd, CatalogError> {
if mount_identity(&location.root).map_err(CatalogError::Io)? != location.identity {
return Err(CatalogError::Io(io::Error::other(
"retained Storage Root identity changed",
)));
}
openat2(
location.root.as_fd(),
name,
flags | OFlags::CLOEXEC,
mode,
ResolveFlags::BENEATH
| ResolveFlags::NO_MAGICLINKS
| ResolveFlags::NO_SYMLINKS
| ResolveFlags::NO_XDEV,
)
.map_err(|error| CatalogError::Io(error.into()))
}
fn open_lock(path: &Path) -> Result<File, CatalogError> {
OpenOptions::new()
.create(true)