857 lines
30 KiB
Rust
857 lines
30 KiB
Rust
use std::{
|
|
fmt,
|
|
os::fd::{AsRawFd, FromRawFd, OwnedFd},
|
|
};
|
|
|
|
use crate::temp_scan::valid_temp_name;
|
|
use crate::{
|
|
ArtifactError, ArtifactStore, ReconciliationMutation, ReconciliationNamespace,
|
|
ReconciliationScan, ReconciliationScanStop,
|
|
recovery::{FINAL_NAMESPACE, namespace_name, namespace_number},
|
|
store::{
|
|
QUARANTINE_DIR, RootLock, check_file, checkpointed, ensure_root_unchanged, fsync_fd,
|
|
open_existing_dir, open_or_create_dir, rename_no_replace_at, stat_fd, unlinkat,
|
|
},
|
|
};
|
|
|
|
mod stale_temp;
|
|
|
|
pub use stale_temp::{StaleTemp, TempScan, TempScanCursor};
|
|
|
|
/// Opaque bounded-scan continuation, valid only for an unchanged namespace.
|
|
/// Discard it after any put, quarantine, delete, or external mutation.
|
|
pub struct ReconciliationCursor {
|
|
root_dev: u64,
|
|
root_ino: u64,
|
|
namespace_start: u8,
|
|
namespace: u8,
|
|
namespace_end: u8,
|
|
shard: u8,
|
|
state: ReconciliationCursorState,
|
|
}
|
|
|
|
enum ReconciliationCursorState {
|
|
OpenNamespace,
|
|
ValidateNamespace(OwnedFd),
|
|
OpenShard(OwnedFd),
|
|
ValidateShard {
|
|
namespace: OwnedFd,
|
|
shard: OwnedFd,
|
|
},
|
|
ReadShard {
|
|
namespace: OwnedFd,
|
|
shard: OwnedFd,
|
|
shard_dev: u64,
|
|
shard_ino: u64,
|
|
buffer: Vec<u8>,
|
|
offset: usize,
|
|
filled: usize,
|
|
cookie: i64,
|
|
},
|
|
SeekShard {
|
|
namespace: OwnedFd,
|
|
shard: OwnedFd,
|
|
shard_dev: u64,
|
|
shard_ino: u64,
|
|
buffer: Vec<u8>,
|
|
cookie: i64,
|
|
},
|
|
Transition,
|
|
}
|
|
|
|
struct ShardIdentity(u64, u64);
|
|
|
|
impl fmt::Debug for ReconciliationCursor {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
formatter.write_str("ReconciliationCursor(..)")
|
|
}
|
|
}
|
|
|
|
/// Opaque inode-bound evidence that exposes neither digest nor filesystem path.
|
|
#[derive(Clone)]
|
|
pub struct ReconciliationCandidate {
|
|
pub(crate) root_dev: u64,
|
|
pub(crate) root_ino: u64,
|
|
pub(crate) namespace: ReconciliationNamespace,
|
|
pub(crate) shard: String,
|
|
pub(crate) shard_dev: u64,
|
|
pub(crate) shard_ino: u64,
|
|
pub(crate) name: String,
|
|
pub(crate) dev: u64,
|
|
pub(crate) ino: u64,
|
|
pub(crate) modified_seconds: i64,
|
|
pub(crate) modified_nanoseconds: i64,
|
|
}
|
|
|
|
impl ArtifactStore {
|
|
pub(crate) fn scan_reconciliation_bounded(
|
|
&self,
|
|
start_namespace: ReconciliationNamespace,
|
|
end_namespace: ReconciliationNamespace,
|
|
continuation: Option<ReconciliationCursor>,
|
|
scan_budget: usize,
|
|
result_limit: usize,
|
|
) -> Result<(ReconciliationScan, Vec<ReconciliationCandidate>), ArtifactError> {
|
|
let root = self.root()?;
|
|
let namespace = namespace_number(start_namespace);
|
|
let namespace_end = namespace_number(end_namespace);
|
|
let mut cursor = match continuation {
|
|
Some(cursor) => {
|
|
if cursor.root_dev != root.dev
|
|
|| cursor.root_ino != root.ino
|
|
|| cursor.namespace_start != namespace
|
|
|| cursor.namespace < namespace
|
|
|| cursor.namespace > cursor.namespace_end
|
|
|| cursor.namespace_end != namespace_end
|
|
{
|
|
return Err(ArtifactError::UnsafeRoot);
|
|
}
|
|
cursor
|
|
}
|
|
None => reconciliation_cursor(root.dev, root.ino, namespace, namespace_end),
|
|
};
|
|
let report = ReconciliationScan::empty(None);
|
|
let _lock = match RootLock::shared(root) {
|
|
Ok(lock) => lock,
|
|
Err(ArtifactError::Storage) => {
|
|
return Ok(reconciliation_page(
|
|
report,
|
|
Vec::new(),
|
|
cursor,
|
|
ReconciliationScanStop::Retryable,
|
|
));
|
|
}
|
|
Err(error) => return Err(error),
|
|
};
|
|
match ensure_root_unchanged(root) {
|
|
Ok(()) => {}
|
|
Err(ArtifactError::Storage) => {
|
|
return Ok(reconciliation_page(
|
|
report,
|
|
Vec::new(),
|
|
cursor,
|
|
ReconciliationScanStop::Retryable,
|
|
));
|
|
}
|
|
Err(error) => return Err(error),
|
|
}
|
|
let mut report = report;
|
|
if scan_budget == 0 {
|
|
return Ok(reconciliation_page(
|
|
report,
|
|
Vec::new(),
|
|
cursor,
|
|
ReconciliationScanStop::ScanBudget,
|
|
));
|
|
}
|
|
if result_limit == 0 {
|
|
return Ok(reconciliation_page(
|
|
report,
|
|
Vec::new(),
|
|
cursor,
|
|
ReconciliationScanStop::ResultLimit,
|
|
));
|
|
}
|
|
|
|
let mut entries = Vec::new();
|
|
while cursor.namespace <= cursor.namespace_end {
|
|
if entries.len() == result_limit {
|
|
return Ok(reconciliation_page(
|
|
report,
|
|
entries,
|
|
cursor,
|
|
ReconciliationScanStop::ResultLimit,
|
|
));
|
|
}
|
|
if report.traversal_syscalls == scan_budget {
|
|
return Ok(reconciliation_page(
|
|
report,
|
|
entries,
|
|
cursor,
|
|
ReconciliationScanStop::ScanBudget,
|
|
));
|
|
}
|
|
let state = std::mem::replace(&mut cursor.state, ReconciliationCursorState::Transition);
|
|
match reconciliation_step(root, &mut cursor, state, &mut report, &mut entries) {
|
|
Ok(()) => {}
|
|
Err((state, ArtifactError::Storage)) => {
|
|
cursor.state = state;
|
|
return Ok(reconciliation_page(
|
|
report,
|
|
entries,
|
|
cursor,
|
|
ReconciliationScanStop::Retryable,
|
|
));
|
|
}
|
|
Err((_, error)) => return Err(error),
|
|
}
|
|
}
|
|
report.stop = ReconciliationScanStop::Complete;
|
|
Ok((report, entries))
|
|
}
|
|
|
|
/// Moves a revalidated final inode to quarantine without replacing an inode.
|
|
pub fn quarantine_reconciliation(
|
|
&self,
|
|
candidate: ReconciliationCandidate,
|
|
) -> Result<ReconciliationMutation, ArtifactError> {
|
|
if candidate.namespace != ReconciliationNamespace::Final {
|
|
return Err(ArtifactError::UnsafeRoot);
|
|
}
|
|
let root = self.root()?;
|
|
let _lock = RootLock::exclusive(root)?;
|
|
ensure_reconciliation_root(root, &candidate)?;
|
|
let final_root = open_existing_dir(root.fd.as_raw_fd(), namespace_name(FINAL_NAMESPACE))?;
|
|
let final_shard = open_existing_dir(final_root.as_raw_fd(), candidate.shard.as_bytes())?;
|
|
match revalidate_candidate(&candidate, final_shard.as_raw_fd()) {
|
|
Ok(()) => {}
|
|
Err(ArtifactError::NotFound) => {
|
|
return self.recover_quarantine(root, &candidate);
|
|
}
|
|
Err(ArtifactError::UnsafeRoot) => {
|
|
return match self.recover_quarantine(root, &candidate) {
|
|
Ok(outcome) => Ok(outcome),
|
|
Err(ArtifactError::NotFound) => Err(ArtifactError::UnsafeRoot),
|
|
Err(error) => Err(error),
|
|
};
|
|
}
|
|
Err(error) => return Err(error),
|
|
}
|
|
let quarantine_root = open_or_create_dir(root.fd.as_raw_fd(), QUARANTINE_DIR)?;
|
|
let quarantine_shard =
|
|
open_or_create_dir(quarantine_root.as_raw_fd(), candidate.shard.as_bytes())?;
|
|
match checkpointed("reconciliation_quarantine_rename", || {
|
|
rename_no_replace_at(
|
|
final_shard.as_raw_fd(),
|
|
candidate.name.as_bytes(),
|
|
quarantine_shard.as_raw_fd(),
|
|
candidate.name.as_bytes(),
|
|
)
|
|
})? {
|
|
true => match fsync_quarantine_dirs(&final_shard, &quarantine_shard) {
|
|
Ok(()) => Ok(ReconciliationMutation::Quarantined),
|
|
Err(_) => Ok(ReconciliationMutation::Retryable),
|
|
},
|
|
false => self.recover_quarantine(root, &candidate),
|
|
}
|
|
}
|
|
|
|
/// Idempotently removes a revalidated quarantined inode and fsyncs its directory.
|
|
pub fn delete_quarantined_reconciliation(
|
|
&self,
|
|
candidate: ReconciliationCandidate,
|
|
) -> Result<ReconciliationMutation, ArtifactError> {
|
|
if candidate.namespace != ReconciliationNamespace::Quarantine {
|
|
return Err(ArtifactError::UnsafeRoot);
|
|
}
|
|
let root = self.root()?;
|
|
let _lock = RootLock::exclusive(root)?;
|
|
ensure_reconciliation_root(root, &candidate)?;
|
|
let quarantine_root = match open_existing_dir(root.fd.as_raw_fd(), QUARANTINE_DIR) {
|
|
Ok(fd) => fd,
|
|
Err(ArtifactError::NotFound) => return Ok(ReconciliationMutation::AlreadyAbsent),
|
|
Err(error) => return Err(error),
|
|
};
|
|
let shard = match open_existing_dir(quarantine_root.as_raw_fd(), candidate.shard.as_bytes())
|
|
{
|
|
Ok(fd) => fd,
|
|
Err(ArtifactError::NotFound) => return Ok(ReconciliationMutation::AlreadyAbsent),
|
|
Err(error) => return Err(error),
|
|
};
|
|
match revalidate_candidate(&candidate, shard.as_raw_fd()) {
|
|
Ok(()) => {}
|
|
Err(ArtifactError::NotFound) => {
|
|
return fsync_mutation(&shard, ReconciliationMutation::AlreadyAbsent);
|
|
}
|
|
Err(error) => return Err(error),
|
|
}
|
|
match checkpointed("reconciliation_delete_unlink", || {
|
|
unlinkat(shard.as_raw_fd(), candidate.name.as_bytes())
|
|
}) {
|
|
Ok(()) => fsync_mutation(&shard, ReconciliationMutation::Deleted),
|
|
Err(ArtifactError::NotFound) => {
|
|
fsync_mutation(&shard, ReconciliationMutation::AlreadyAbsent)
|
|
}
|
|
Err(error) => Err(error),
|
|
}
|
|
}
|
|
|
|
fn recover_quarantine(
|
|
&self,
|
|
root: &crate::store::Root,
|
|
candidate: &ReconciliationCandidate,
|
|
) -> Result<ReconciliationMutation, ArtifactError> {
|
|
let final_root = open_existing_dir(root.fd.as_raw_fd(), namespace_name(FINAL_NAMESPACE))?;
|
|
let final_shard = open_existing_dir(final_root.as_raw_fd(), candidate.shard.as_bytes())?;
|
|
let quarantine_root = match open_existing_dir(root.fd.as_raw_fd(), QUARANTINE_DIR) {
|
|
Ok(fd) => fd,
|
|
Err(ArtifactError::NotFound) => return Err(ArtifactError::NotFound),
|
|
Err(error) => return Err(error),
|
|
};
|
|
let shard = match open_existing_dir(quarantine_root.as_raw_fd(), candidate.shard.as_bytes())
|
|
{
|
|
Ok(fd) => fd,
|
|
Err(ArtifactError::NotFound) => return Err(ArtifactError::NotFound),
|
|
Err(error) => return Err(error),
|
|
};
|
|
revalidate_moved_candidate(candidate, shard.as_raw_fd())?;
|
|
match fsync_quarantine_dirs(&final_shard, &shard) {
|
|
Ok(()) => Ok(ReconciliationMutation::AlreadyQuarantined),
|
|
Err(_) => Ok(ReconciliationMutation::Retryable),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn reconciliation_cursor(
|
|
root_dev: u64,
|
|
root_ino: u64,
|
|
namespace: u8,
|
|
namespace_end: u8,
|
|
) -> ReconciliationCursor {
|
|
ReconciliationCursor {
|
|
root_dev,
|
|
root_ino,
|
|
namespace_start: namespace,
|
|
namespace,
|
|
namespace_end,
|
|
shard: 0,
|
|
state: ReconciliationCursorState::OpenNamespace,
|
|
}
|
|
}
|
|
|
|
fn reconciliation_page(
|
|
mut report: ReconciliationScan,
|
|
entries: Vec<ReconciliationCandidate>,
|
|
cursor: ReconciliationCursor,
|
|
stop: ReconciliationScanStop,
|
|
) -> (ReconciliationScan, Vec<ReconciliationCandidate>) {
|
|
report.stop = stop;
|
|
report.continuation = Some(cursor);
|
|
(report, entries)
|
|
}
|
|
|
|
fn reconciliation_step(
|
|
root: &crate::store::Root,
|
|
cursor: &mut ReconciliationCursor,
|
|
state: ReconciliationCursorState,
|
|
report: &mut ReconciliationScan,
|
|
entries: &mut Vec<ReconciliationCandidate>,
|
|
) -> Result<(), (ReconciliationCursorState, ArtifactError)> {
|
|
match state {
|
|
ReconciliationCursorState::OpenNamespace => {
|
|
count_traversal_syscall(report);
|
|
match open_traversal_dir(root.fd.as_raw_fd(), namespace_name(cursor.namespace)) {
|
|
Ok(namespace) => {
|
|
cursor.state = ReconciliationCursorState::ValidateNamespace(namespace);
|
|
}
|
|
Err(ArtifactError::NotFound) => advance_namespace(cursor),
|
|
Err(ArtifactError::UnsafeRoot) => {
|
|
report.unsafe_entries += 1;
|
|
advance_namespace(cursor);
|
|
}
|
|
Err(error) => {
|
|
return Err((ReconciliationCursorState::OpenNamespace, error));
|
|
}
|
|
}
|
|
}
|
|
ReconciliationCursorState::ValidateNamespace(namespace) => {
|
|
count_traversal_syscall(report);
|
|
match validate_traversal_dir(&namespace) {
|
|
Ok(_) => {
|
|
cursor.state = ReconciliationCursorState::OpenShard(namespace);
|
|
}
|
|
Err(ArtifactError::UnsafeRoot) => {
|
|
report.unsafe_entries += 1;
|
|
advance_namespace(cursor);
|
|
}
|
|
Err(error) => {
|
|
return Err((
|
|
ReconciliationCursorState::ValidateNamespace(namespace),
|
|
error,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
ReconciliationCursorState::OpenShard(namespace) => {
|
|
let shard_name = format!("{:02x}", cursor.shard);
|
|
count_traversal_syscall(report);
|
|
match open_traversal_dir(namespace.as_raw_fd(), shard_name.as_bytes()) {
|
|
Ok(shard) => {
|
|
cursor.state = ReconciliationCursorState::ValidateShard { namespace, shard };
|
|
}
|
|
Err(ArtifactError::NotFound) => advance_reconciliation_shard(cursor, namespace),
|
|
Err(ArtifactError::UnsafeRoot) => {
|
|
report.unsafe_entries += 1;
|
|
advance_reconciliation_shard(cursor, namespace);
|
|
}
|
|
Err(error) => {
|
|
return Err((ReconciliationCursorState::OpenShard(namespace), error));
|
|
}
|
|
}
|
|
}
|
|
ReconciliationCursorState::ValidateShard { namespace, shard } => {
|
|
count_traversal_syscall(report);
|
|
match validate_traversal_dir(&shard) {
|
|
Ok(stat) => {
|
|
cursor.state = read_shard_state(
|
|
namespace,
|
|
shard,
|
|
ShardIdentity(stat.st_dev, stat.st_ino),
|
|
vec![0; 8192],
|
|
0,
|
|
0,
|
|
0,
|
|
);
|
|
}
|
|
Err(ArtifactError::UnsafeRoot) => {
|
|
report.unsafe_entries += 1;
|
|
advance_reconciliation_shard(cursor, namespace);
|
|
}
|
|
Err(error) => {
|
|
return Err((
|
|
ReconciliationCursorState::ValidateShard { namespace, shard },
|
|
error,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
ReconciliationCursorState::ReadShard {
|
|
namespace,
|
|
shard,
|
|
shard_dev,
|
|
shard_ino,
|
|
mut buffer,
|
|
mut offset,
|
|
mut filled,
|
|
mut cookie,
|
|
} => {
|
|
if offset == filled {
|
|
count_traversal_syscall(report);
|
|
match checkpointed("reconciliation_getdents", || {
|
|
getdents64(shard.as_raw_fd(), &mut buffer)
|
|
}) {
|
|
Ok(0) => advance_reconciliation_shard(cursor, namespace),
|
|
Ok(count) => {
|
|
offset = 0;
|
|
filled = count;
|
|
cursor.state = read_shard_state(
|
|
namespace,
|
|
shard,
|
|
ShardIdentity(shard_dev, shard_ino),
|
|
buffer,
|
|
offset,
|
|
filled,
|
|
cookie,
|
|
);
|
|
}
|
|
Err(error) => {
|
|
return Err((
|
|
ReconciliationCursorState::SeekShard {
|
|
namespace,
|
|
shard,
|
|
shard_dev,
|
|
shard_ino,
|
|
buffer,
|
|
cookie,
|
|
},
|
|
error,
|
|
));
|
|
}
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
let (name, after, after_cookie) = match parse_dirent(&buffer[..filled], offset) {
|
|
Ok(entry) => entry,
|
|
Err(error) => {
|
|
return Err((
|
|
read_shard_state(
|
|
namespace,
|
|
shard,
|
|
ShardIdentity(shard_dev, shard_ino),
|
|
buffer,
|
|
offset,
|
|
filled,
|
|
cookie,
|
|
),
|
|
error,
|
|
));
|
|
}
|
|
};
|
|
if name == b"." || name == b".." {
|
|
offset = after;
|
|
cookie = after_cookie;
|
|
cursor.state = read_shard_state(
|
|
namespace,
|
|
shard,
|
|
ShardIdentity(shard_dev, shard_ino),
|
|
buffer,
|
|
offset,
|
|
filled,
|
|
cookie,
|
|
);
|
|
return Ok(());
|
|
}
|
|
let shard_name = format!("{:02x}", cursor.shard);
|
|
let Ok(name) = String::from_utf8(name) else {
|
|
report.scanned += 1;
|
|
report.malformed += 1;
|
|
offset = after;
|
|
cookie = after_cookie;
|
|
cursor.state = read_shard_state(
|
|
namespace,
|
|
shard,
|
|
ShardIdentity(shard_dev, shard_ino),
|
|
buffer,
|
|
offset,
|
|
filled,
|
|
cookie,
|
|
);
|
|
return Ok(());
|
|
};
|
|
if !valid_final_name(&name, &shard_name) {
|
|
report.scanned += 1;
|
|
if cursor.namespace != FINAL_NAMESPACE || !valid_temp_name(&name) {
|
|
report.malformed += 1;
|
|
}
|
|
offset = after;
|
|
cookie = after_cookie;
|
|
cursor.state = read_shard_state(
|
|
namespace,
|
|
shard,
|
|
ShardIdentity(shard_dev, shard_ino),
|
|
buffer,
|
|
offset,
|
|
filled,
|
|
cookie,
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
#[cfg(debug_assertions)]
|
|
let _ = crate::test_support::checkpoint("reconciliation_before_stat");
|
|
count_traversal_syscall(report);
|
|
let stat = match checkpointed("reconciliation_stat", || {
|
|
record_traversal_call();
|
|
nofollow_stat(shard.as_raw_fd(), &name)
|
|
}) {
|
|
Ok(stat) => Some(stat),
|
|
Err(ArtifactError::NotFound) => None,
|
|
Err(ArtifactError::UnsafeRoot) => {
|
|
report.unsafe_entries += 1;
|
|
None
|
|
}
|
|
Err(error) => {
|
|
return Err((
|
|
read_shard_state(
|
|
namespace,
|
|
shard,
|
|
ShardIdentity(shard_dev, shard_ino),
|
|
buffer,
|
|
offset,
|
|
filled,
|
|
cookie,
|
|
),
|
|
error,
|
|
));
|
|
}
|
|
};
|
|
report.scanned += 1;
|
|
offset = after;
|
|
cookie = after_cookie;
|
|
if let Some(stat) = stat {
|
|
if check_file(&stat).is_err() {
|
|
report.unsafe_entries += 1;
|
|
} else {
|
|
let candidate_namespace = if cursor.namespace == FINAL_NAMESPACE {
|
|
report.final_entries += 1;
|
|
ReconciliationNamespace::Final
|
|
} else {
|
|
report.quarantined_entries += 1;
|
|
ReconciliationNamespace::Quarantine
|
|
};
|
|
entries.push(ReconciliationCandidate {
|
|
root_dev: root.dev,
|
|
root_ino: root.ino,
|
|
namespace: candidate_namespace,
|
|
shard: shard_name,
|
|
shard_dev,
|
|
shard_ino,
|
|
name,
|
|
dev: stat.st_dev,
|
|
ino: stat.st_ino,
|
|
modified_seconds: stat.st_mtime,
|
|
modified_nanoseconds: stat.st_mtime_nsec,
|
|
});
|
|
}
|
|
}
|
|
cursor.state = read_shard_state(
|
|
namespace,
|
|
shard,
|
|
ShardIdentity(shard_dev, shard_ino),
|
|
buffer,
|
|
offset,
|
|
filled,
|
|
cookie,
|
|
);
|
|
}
|
|
ReconciliationCursorState::SeekShard {
|
|
namespace,
|
|
shard,
|
|
shard_dev,
|
|
shard_ino,
|
|
buffer,
|
|
cookie,
|
|
} => {
|
|
count_traversal_syscall(report);
|
|
record_traversal_call();
|
|
if unsafe { libc::lseek(shard.as_raw_fd(), cookie, libc::SEEK_SET) } != cookie {
|
|
return Err((
|
|
ReconciliationCursorState::SeekShard {
|
|
namespace,
|
|
shard,
|
|
shard_dev,
|
|
shard_ino,
|
|
buffer,
|
|
cookie,
|
|
},
|
|
ArtifactError::Storage,
|
|
));
|
|
}
|
|
cursor.state = read_shard_state(
|
|
namespace,
|
|
shard,
|
|
ShardIdentity(shard_dev, shard_ino),
|
|
buffer,
|
|
0,
|
|
0,
|
|
cookie,
|
|
);
|
|
}
|
|
ReconciliationCursorState::Transition => unreachable!("temporary scanner state"),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn count_traversal_syscall(report: &mut ReconciliationScan) {
|
|
report.traversal_syscalls += 1;
|
|
}
|
|
|
|
fn read_shard_state(
|
|
namespace: OwnedFd,
|
|
shard: OwnedFd,
|
|
shard_identity: ShardIdentity,
|
|
buffer: Vec<u8>,
|
|
offset: usize,
|
|
filled: usize,
|
|
cookie: i64,
|
|
) -> ReconciliationCursorState {
|
|
let ShardIdentity(shard_dev, shard_ino) = shard_identity;
|
|
ReconciliationCursorState::ReadShard {
|
|
namespace,
|
|
shard,
|
|
shard_dev,
|
|
shard_ino,
|
|
buffer,
|
|
offset,
|
|
filled,
|
|
cookie,
|
|
}
|
|
}
|
|
|
|
fn open_traversal_dir(parent: i32, name: &[u8]) -> Result<OwnedFd, ArtifactError> {
|
|
let name = crate::store::c_name(name)?;
|
|
record_traversal_call();
|
|
let raw = unsafe {
|
|
libc::openat(
|
|
parent,
|
|
name.as_ptr(),
|
|
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
|
|
)
|
|
};
|
|
if raw < 0 {
|
|
match unsafe { *libc::__errno_location() } {
|
|
libc::ENOENT => Err(ArtifactError::NotFound),
|
|
libc::EACCES | libc::ELOOP | libc::ENOTDIR => Err(ArtifactError::UnsafeRoot),
|
|
_ => Err(ArtifactError::Storage),
|
|
}
|
|
} else {
|
|
Ok(unsafe { OwnedFd::from_raw_fd(raw) })
|
|
}
|
|
}
|
|
|
|
fn validate_traversal_dir(directory: &OwnedFd) -> Result<libc::stat, ArtifactError> {
|
|
record_traversal_call();
|
|
let stat = stat_fd(directory.as_raw_fd())?;
|
|
if stat.st_uid != unsafe { libc::geteuid() }
|
|
|| (stat.st_mode & libc::S_IFMT) != libc::S_IFDIR
|
|
|| (stat.st_mode & 0o777) != 0o700
|
|
{
|
|
Err(ArtifactError::UnsafeRoot)
|
|
} else {
|
|
Ok(stat)
|
|
}
|
|
}
|
|
|
|
fn getdents64(directory: i32, buffer: &mut [u8]) -> Result<usize, ArtifactError> {
|
|
record_traversal_call();
|
|
let count = unsafe {
|
|
libc::syscall(
|
|
libc::SYS_getdents64,
|
|
directory,
|
|
buffer.as_mut_ptr(),
|
|
buffer.len(),
|
|
)
|
|
};
|
|
if count < 0 {
|
|
Err(ArtifactError::Storage)
|
|
} else {
|
|
usize::try_from(count).map_err(|_| ArtifactError::Storage)
|
|
}
|
|
}
|
|
|
|
fn parse_dirent(buffer: &[u8], offset: usize) -> Result<(Vec<u8>, usize, i64), ArtifactError> {
|
|
const HEADER: usize = 19;
|
|
if buffer.len().saturating_sub(offset) < HEADER {
|
|
return Err(ArtifactError::Storage);
|
|
}
|
|
let record_length = usize::from(u16::from_ne_bytes([
|
|
buffer[offset + 16],
|
|
buffer[offset + 17],
|
|
]));
|
|
let after = offset
|
|
.checked_add(record_length)
|
|
.filter(|after| record_length >= HEADER && *after <= buffer.len())
|
|
.ok_or(ArtifactError::Storage)?;
|
|
let raw_name = &buffer[offset + HEADER..after];
|
|
let end = raw_name
|
|
.iter()
|
|
.position(|byte| *byte == 0)
|
|
.ok_or(ArtifactError::Storage)?;
|
|
let cookie = i64::from_ne_bytes(
|
|
buffer[offset + 8..offset + 16]
|
|
.try_into()
|
|
.map_err(|_| ArtifactError::Storage)?,
|
|
);
|
|
Ok((raw_name[..end].to_vec(), after, cookie))
|
|
}
|
|
|
|
fn advance_reconciliation_shard(cursor: &mut ReconciliationCursor, namespace: OwnedFd) {
|
|
if cursor.shard == u8::MAX {
|
|
advance_namespace(cursor);
|
|
} else {
|
|
cursor.shard += 1;
|
|
cursor.state = ReconciliationCursorState::OpenShard(namespace);
|
|
}
|
|
}
|
|
|
|
fn advance_namespace(cursor: &mut ReconciliationCursor) {
|
|
cursor.namespace = if cursor.namespace >= cursor.namespace_end {
|
|
cursor.namespace_end.saturating_add(1)
|
|
} else {
|
|
cursor.namespace + 1
|
|
};
|
|
cursor.shard = 0;
|
|
cursor.state = ReconciliationCursorState::OpenNamespace;
|
|
}
|
|
|
|
pub(crate) fn ensure_reconciliation_root(
|
|
root: &crate::store::Root,
|
|
candidate: &ReconciliationCandidate,
|
|
) -> Result<(), ArtifactError> {
|
|
ensure_root_unchanged(root)?;
|
|
if candidate.root_dev != root.dev
|
|
|| candidate.root_ino != root.ino
|
|
|| candidate.shard.len() != 2
|
|
|| !candidate.shard.bytes().all(is_lower_hex)
|
|
|| !valid_final_name(&candidate.name, &candidate.shard)
|
|
{
|
|
return Err(ArtifactError::UnsafeRoot);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn revalidate_candidate(
|
|
candidate: &ReconciliationCandidate,
|
|
shard: i32,
|
|
) -> Result<(), ArtifactError> {
|
|
let shard_stat = stat_fd(shard)?;
|
|
if shard_stat.st_dev != candidate.shard_dev || shard_stat.st_ino != candidate.shard_ino {
|
|
return Err(ArtifactError::UnsafeRoot);
|
|
}
|
|
let stat = nofollow_stat(shard, &candidate.name)?;
|
|
if stat.st_dev != candidate.dev
|
|
|| stat.st_ino != candidate.ino
|
|
|| stat.st_mtime != candidate.modified_seconds
|
|
|| stat.st_mtime_nsec != candidate.modified_nanoseconds
|
|
{
|
|
return Err(ArtifactError::UnsafeRoot);
|
|
}
|
|
check_file(&stat).map_err(|_| ArtifactError::UnsafeRoot)
|
|
}
|
|
|
|
/// After cross-directory rename, only canonical name and inode remain common;
|
|
/// the quarantine shard cannot equal the capability's recorded final shard.
|
|
fn revalidate_moved_candidate(
|
|
candidate: &ReconciliationCandidate,
|
|
shard: i32,
|
|
) -> Result<(), ArtifactError> {
|
|
let stat = nofollow_stat(shard, &candidate.name)?;
|
|
if stat.st_dev != candidate.dev
|
|
|| stat.st_ino != candidate.ino
|
|
|| stat.st_mtime != candidate.modified_seconds
|
|
|| stat.st_mtime_nsec != candidate.modified_nanoseconds
|
|
{
|
|
return Err(ArtifactError::UnsafeRoot);
|
|
}
|
|
check_file(&stat).map_err(|_| ArtifactError::UnsafeRoot)
|
|
}
|
|
|
|
fn fsync_mutation(
|
|
shard: &std::os::fd::OwnedFd,
|
|
success: ReconciliationMutation,
|
|
) -> Result<ReconciliationMutation, ArtifactError> {
|
|
match checkpointed("reconciliation_delete_fsync", || {
|
|
fsync_fd(shard.as_raw_fd())
|
|
}) {
|
|
Ok(()) => Ok(success),
|
|
Err(_) => Ok(ReconciliationMutation::Retryable),
|
|
}
|
|
}
|
|
|
|
/// Attempts both directory fsyncs; either failure leaves a retryable ambiguity.
|
|
fn fsync_quarantine_dirs(
|
|
final_shard: &std::os::fd::OwnedFd,
|
|
quarantine_shard: &std::os::fd::OwnedFd,
|
|
) -> Result<(), ArtifactError> {
|
|
let source = checkpointed("reconciliation_quarantine_source_fsync", || {
|
|
fsync_fd(final_shard.as_raw_fd())
|
|
});
|
|
let destination = checkpointed("reconciliation_quarantine_destination_fsync", || {
|
|
fsync_fd(quarantine_shard.as_raw_fd())
|
|
});
|
|
source.and(destination)
|
|
}
|
|
|
|
fn valid_final_name(name: &str, shard: &str) -> bool {
|
|
name.len() == 64 && name.starts_with(shard) && name.bytes().all(is_lower_hex)
|
|
}
|
|
|
|
fn is_lower_hex(byte: u8) -> bool {
|
|
byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)
|
|
}
|
|
fn nofollow_stat(parent: i32, name: &str) -> Result<libc::stat, ArtifactError> {
|
|
let name = crate::store::c_name(name.as_bytes())?;
|
|
let mut stat = unsafe { std::mem::zeroed() };
|
|
if unsafe { libc::fstatat(parent, name.as_ptr(), &mut stat, libc::AT_SYMLINK_NOFOLLOW) } == 0 {
|
|
Ok(stat)
|
|
} else {
|
|
Err(crate::store::classify_errno())
|
|
}
|
|
}
|
|
|
|
fn record_traversal_call() {
|
|
#[cfg(debug_assertions)]
|
|
crate::test_support::record_traversal_call();
|
|
}
|