use std::{
fs, io,
path::{Path, PathBuf},
};
use super::read::validate_session_id;
pub(crate) const SESSION_ROOT_MODE: u32 = 0o700;
pub(crate) const SESSION_FILE_MODE: u32 = 0o600;
pub(crate) fn prepare_session_root(root: &Path) -> anyhow::Result<()> {
match fs::symlink_metadata(root) {
Ok(_) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {
fs::create_dir_all(root)?;
set_mode(root, SESSION_ROOT_MODE)?;
}
Err(error) => return Err(error.into()),
}
#[cfg(unix)]
{
let metadata = fs::symlink_metadata(root)?;
if is_legacy_mode_candidate(&metadata) {
return Err(legacy_permission_error(root));
}
}
validate_session_root(root)
}
#[cfg(unix)]
fn is_legacy_mode_candidate(metadata: &fs::Metadata) -> bool {
use std::os::unix::fs::MetadataExt;
!metadata.file_type().is_symlink()
&& metadata.file_type().is_dir()
&& metadata.uid() == unsafe { libc::geteuid() }
&& metadata.mode() & 0o077 != 0
}
#[cfg(unix)]
fn legacy_permission_error(root: &Path) -> anyhow::Error {
if discover_repair_plan(root).is_ok() {
anyhow::anyhow!(
"session root permissions are not owner-private: {}. Run `magi-code sessions repair-permissions`; magi-code did not change any permissions itself.",
root.display()
)
} else {
anyhow::anyhow!(
"session root permissions are not owner-private: {}. Layout is unsafe, unreadable, or unrecognized; inspect it manually. magi-code did not change any permissions itself.",
root.display()
)
}
}
#[cfg(unix)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RepairPlan {
pub(crate) root: PathBuf,
targets: Vec<RepairTarget>,
}
#[cfg(unix)]
#[derive(Debug, Clone, PartialEq, Eq)]
struct RepairTarget {
relative: PathBuf,
dev: u64,
ino: u64,
is_dir: bool,
uid: u32,
nlink: u64,
}
#[cfg(unix)]
impl RepairPlan {
pub(crate) fn target_count(&self) -> usize {
self.targets.len()
}
}
#[cfg(unix)]
fn target_mode(is_dir: bool) -> u32 {
if is_dir {
SESSION_ROOT_MODE
} else {
SESSION_FILE_MODE
}
}
#[cfg(unix)]
pub(crate) fn discover_repair_plan(root: &Path) -> Result<RepairPlan, ()> {
use std::os::unix::fs::MetadataExt;
let metadata = fs::symlink_metadata(root).map_err(|_| ())?;
if !metadata.file_type().is_dir()
|| metadata.file_type().is_symlink()
|| metadata.uid() != unsafe { libc::geteuid() }
{
return Err(());
}
let mut targets = Vec::new();
add_repair_target(PathBuf::new(), &metadata, true, &mut targets)?;
collect_repair_entries(root, Path::new(""), &mut targets, true)?;
targets.sort_by(|a, b| a.relative.cmp(&b.relative));
Ok(RepairPlan {
root: root.to_path_buf(),
targets,
})
}
#[cfg(unix)]
fn add_repair_target(
relative: PathBuf,
metadata: &fs::Metadata,
is_dir: bool,
targets: &mut Vec<RepairTarget>,
) -> Result<(), ()> {
use std::os::unix::fs::MetadataExt;
if metadata.file_type().is_symlink()
|| (is_dir && !metadata.file_type().is_dir())
|| (!is_dir && !metadata.file_type().is_file())
|| metadata.uid() != unsafe { libc::geteuid() }
|| (!is_dir && metadata.nlink() != 1)
|| (!is_dir && metadata.mode() & 0o400 == 0)
{
return Err(());
}
targets.push(RepairTarget {
relative,
dev: metadata.dev(),
ino: metadata.ino(),
is_dir,
uid: metadata.uid(),
nlink: metadata.nlink(),
});
Ok(())
}
#[cfg(unix)]
fn collect_repair_entries(
root: &Path,
prefix: &Path,
targets: &mut Vec<RepairTarget>,
allow_subagents: bool,
) -> Result<(), ()> {
for (name, path) in read_legacy_entries(&root.join(prefix)).map_err(|_| ())? {
let relative = prefix.join(&name);
if name == "subagents" || name == ".history" {
let metadata = fs::symlink_metadata(&path).map_err(|_| ())?;
add_repair_target(relative.clone(), &metadata, true, targets)?;
if name == "subagents" {
if !allow_subagents {
return Err(());
}
collect_repair_entries(root, &relative, targets, false)?;
} else {
for (session_id, session_path) in read_legacy_entries(&path).map_err(|_| ())? {
validate_session_id(session_id.clone()).map_err(|_| ())?;
let session_relative = relative.join(&session_id);
let session_metadata = fs::symlink_metadata(&session_path).map_err(|_| ())?;
add_repair_target(session_relative.clone(), &session_metadata, true, targets)?;
for (generation, archive_path) in
read_legacy_entries(&session_path).map_err(|_| ())?
{
if generation
.strip_suffix(".jsonl")
.and_then(|v| v.parse::<u64>().ok())
.is_none()
{
return Err(());
}
let archive_metadata =
fs::symlink_metadata(&archive_path).map_err(|_| ())?;
add_repair_target(
session_relative.join(generation),
&archive_metadata,
false,
targets,
)?;
}
}
}
} else {
let metadata = fs::symlink_metadata(&path).map_err(|_| ())?;
validate_legacy_file(&path)?;
add_repair_target(relative, &metadata, false, targets)?;
}
}
Ok(())
}
#[cfg(unix)]
pub(crate) fn apply_repair_plan(plan: &RepairPlan) -> anyhow::Result<usize> {
let current = discover_repair_plan(&plan.root)
.map_err(|_| anyhow::anyhow!("session layout changed; refusing permission repair"))?;
if current != *plan {
anyhow::bail!("session layout changed; refusing permission repair")
}
use std::os::fd::{AsRawFd, FromRawFd};
let root_file = open_repair_root(&plan.root)?;
let mut files = Vec::new();
for target in &plan.targets {
let (fd, metadata) =
open_repair_target(root_file.as_raw_fd(), &target.relative, target.is_dir).map_err(
|error| {
anyhow::anyhow!(
"cannot safely open repair target {}: {error}",
target.relative.display()
)
},
)?;
use std::os::unix::fs::MetadataExt;
if metadata.dev() != target.dev
|| metadata.ino() != target.ino
|| metadata.uid() != target.uid
|| metadata.nlink() != target.nlink
|| (metadata.file_type().is_dir() != target.is_dir)
|| (!target.is_dir && metadata.nlink() != 1)
{
unsafe { libc::close(fd) };
anyhow::bail!("session layout changed; refusing permission repair")
}
let file = unsafe { fs::File::from_raw_fd(fd) };
files.push((file, target_mode(target.is_dir), target.relative.clone()));
}
let mut changed = 0;
for (file, mode, relative) in files {
let result = unsafe { libc::fchmod(file.as_raw_fd(), mode as libc::mode_t) };
if result != 0 {
return Err(anyhow::anyhow!(
"permission repair partially completed: changed {changed} of {} targets; failed target {}: {}",
plan.target_count(),
relative.display(),
io::Error::last_os_error()
));
}
changed += 1;
}
Ok(changed)
}
#[cfg(unix)]
fn open_repair_root(root: &Path) -> anyhow::Result<fs::File> {
use std::os::unix::fs::OpenOptionsExt;
let mut options = fs::OpenOptions::new();
options
.read(true)
.custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC);
Ok(options.open(root)?)
}
#[cfg(unix)]
fn open_repair_target(
root_fd: std::os::fd::RawFd,
relative: &Path,
is_dir: bool,
) -> anyhow::Result<(std::os::fd::RawFd, fs::Metadata)> {
use std::os::fd::{FromRawFd, IntoRawFd};
let mut directory = unsafe { libc::dup(root_fd) };
if directory < 0 {
return Err(io::Error::last_os_error().into());
}
let components = relative.components().collect::<Vec<_>>();
for (index, component) in components.iter().enumerate() {
let name = component
.as_os_str()
.to_str()
.ok_or_else(|| anyhow::anyhow!("invalid session entry"))?;
let c_name =
std::ffi::CString::new(name).map_err(|_| anyhow::anyhow!("invalid session entry"))?;
let last = index + 1 == components.len();
let flags = if last {
libc::O_RDONLY | if is_dir { libc::O_DIRECTORY } else { 0 }
} else {
libc::O_RDONLY | libc::O_DIRECTORY
};
let fd = unsafe {
libc::openat(
directory,
c_name.as_ptr(),
flags | libc::O_CLOEXEC | libc::O_NOFOLLOW,
)
};
unsafe { libc::close(directory) };
if fd < 0 {
return Err(io::Error::last_os_error().into());
}
directory = fd;
}
if components.is_empty() {
let duplicate = unsafe { libc::dup(directory) };
if duplicate < 0 {
unsafe { libc::close(directory) };
return Err(io::Error::last_os_error().into());
}
let metadata = unsafe { fs::File::from_raw_fd(duplicate) }.metadata()?;
return Ok((directory, metadata));
}
let file = unsafe { fs::File::from_raw_fd(directory) };
let metadata = file.metadata()?;
Ok((file.into_raw_fd(), metadata))
}
#[cfg(unix)]
fn read_legacy_entries(root: &Path) -> io::Result<Vec<(String, PathBuf)>> {
let mut entries = Vec::new();
for entry in fs::read_dir(root)? {
let entry = entry?;
let name = entry
.file_name()
.into_string()
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "non-UTF-8 session entry"))?;
entries.push((name, entry.path()));
}
entries.sort_by(|left, right| left.0.cmp(&right.0));
Ok(entries)
}
#[cfg(unix)]
fn validate_legacy_file(path: &Path) -> Result<(), ()> {
use std::os::unix::fs::MetadataExt;
let name = path.file_name().and_then(|name| name.to_str()).ok_or(())?;
let id = name
.strip_suffix(".metadata.json")
.or_else(|| name.strip_suffix(".jsonl"))
.ok_or(())?;
validate_session_id(id.to_string()).map_err(|_| ())?;
let metadata = fs::symlink_metadata(path).map_err(|_| ())?;
if metadata.file_type().is_symlink()
|| !metadata.file_type().is_file()
|| metadata.nlink() != 1
|| metadata.uid() != unsafe { libc::geteuid() }
{
return Err(());
}
Ok(())
}
pub(crate) fn validate_session_root(root: &Path) -> anyhow::Result<()> {
let metadata = fs::symlink_metadata(root)?;
validate_root_metadata(&metadata)
}
fn validate_root_metadata(metadata: &fs::Metadata) -> anyhow::Result<()> {
if metadata.file_type().is_symlink() || !metadata.file_type().is_dir() {
anyhow::bail!("session root must be a non-symlink directory");
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if metadata.uid() != unsafe { libc::geteuid() } {
anyhow::bail!("session root owner is not current user");
}
if metadata.mode() & 0o077 != 0 {
anyhow::bail!("session root permissions are not owner-private");
}
}
Ok(())
}
pub(crate) fn primary_path(root: &Path, id: &str) -> anyhow::Result<PathBuf> {
let id = validate_session_id(id.to_string())?;
Ok(root.join(format!("{id}.jsonl")))
}
pub(crate) fn validate_existing_file(path: &Path) -> anyhow::Result<Option<fs::Metadata>> {
let path_metadata = match fs::symlink_metadata(path) {
Ok(metadata) => metadata,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error.into()),
};
validate_file_metadata(&path_metadata)?;
Ok(Some(path_metadata))
}
fn validate_file_metadata(metadata: &fs::Metadata) -> anyhow::Result<()> {
if metadata.file_type().is_symlink() || !metadata.file_type().is_file() {
anyhow::bail!("session store object is not a regular file");
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if metadata.nlink() != 1 {
anyhow::bail!("session store object has unexpected hard links");
}
if metadata.uid() != unsafe { libc::geteuid() } {
anyhow::bail!("session store object owner is not current user");
}
if metadata.mode() & 0o077 != 0 {
anyhow::bail!("session store object permissions are not owner-private");
}
}
Ok(())
}
fn open_root(root: &Path) -> anyhow::Result<fs::File> {
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let mut options = fs::OpenOptions::new();
options
.read(true)
.custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW);
let file = options.open(root)?;
validate_root_metadata(&file.metadata()?)?;
Ok(file)
}
#[cfg(not(unix))]
{
validate_session_root(root)?;
Ok(fs::File::open(root)?)
}
}
fn open_relative(root: &Path, name: &str, write: bool, create: bool) -> anyhow::Result<fs::File> {
#[cfg(unix)]
{
use std::os::fd::{AsRawFd, FromRawFd};
let root_file = open_root(root)?;
let mut flags = if write {
libc::O_WRONLY | libc::O_APPEND
} else {
libc::O_RDONLY
};
flags |= libc::O_CLOEXEC | libc::O_NOFOLLOW;
if create {
flags |= libc::O_CREAT;
}
let name = std::ffi::CString::new(name)
.map_err(|_| anyhow::anyhow!("invalid session filename"))?;
let fd = unsafe {
libc::openat(
root_file.as_raw_fd(),
name.as_ptr(),
flags,
SESSION_FILE_MODE,
)
};
if fd < 0 {
return Err(io::Error::last_os_error().into());
}
let file = unsafe { fs::File::from_raw_fd(fd) };
validate_file_metadata(&file.metadata()?)?;
Ok(file)
}
#[cfg(not(unix))]
{
validate_session_root(root)?;
let path = root.join(name);
let mut options = fs::OpenOptions::new();
options
.read(!write)
.write(write)
.create(create)
.append(write);
let file = options.open(path)?;
validate_file_metadata(&file.metadata()?)?;
Ok(file)
}
}
fn open_relative_new_or_existing(root: &Path, name: &str) -> anyhow::Result<(fs::File, bool)> {
#[cfg(unix)]
{
use std::os::fd::{AsRawFd, FromRawFd};
let root_file = open_root(root)?;
let name = std::ffi::CString::new(name)
.map_err(|_| anyhow::anyhow!("invalid session filename"))?;
let flags = libc::O_WRONLY
| libc::O_APPEND
| libc::O_CREAT
| libc::O_EXCL
| libc::O_CLOEXEC
| libc::O_NOFOLLOW;
let fd = unsafe {
libc::openat(
root_file.as_raw_fd(),
name.as_ptr(),
flags,
SESSION_FILE_MODE,
)
};
if fd >= 0 {
let file = unsafe { fs::File::from_raw_fd(fd) };
validate_file_metadata(&file.metadata()?)?;
return Ok((file, true));
}
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::AlreadyExists {
return Err(error.into());
}
let flags = libc::O_WRONLY | libc::O_APPEND | libc::O_CLOEXEC | libc::O_NOFOLLOW;
let fd = unsafe { libc::openat(root_file.as_raw_fd(), name.as_ptr(), flags) };
if fd < 0 {
return Err(io::Error::last_os_error().into());
}
let file = unsafe { fs::File::from_raw_fd(fd) };
validate_file_metadata(&file.metadata()?)?;
Ok((file, false))
}
#[cfg(not(unix))]
{
let path = root.join(name);
let existed = path.try_exists()?;
Ok((open_relative(root, name, true, true)?, !existed))
}
}
pub(crate) fn open_primary(root: &Path, id: &str) -> anyhow::Result<(fs::File, bool)> {
let id = validate_session_id(id.to_string())?;
open_relative_new_or_existing(root, &format!("{id}.jsonl"))
}
pub(crate) fn open_existing_primary(root: &Path, id: &str) -> anyhow::Result<Option<fs::File>> {
let id = validate_session_id(id.to_string())?;
open_existing_named(root, &format!("{id}.jsonl"))
}
pub(crate) fn open_existing_named(root: &Path, name: &str) -> anyhow::Result<Option<fs::File>> {
if Path::new(name).file_name().and_then(|value| value.to_str()) != Some(name) {
anyhow::bail!("session store object has unexpected name");
}
match open_relative(root, name, false, false) {
Ok(file) => Ok(Some(file)),
Err(error)
if error
.downcast_ref::<io::Error>()
.is_some_and(|e| e.kind() == io::ErrorKind::NotFound) =>
{
Ok(None)
}
Err(error) => Err(error),
}
}
pub(crate) fn validate_path_file(
root: &Path,
id: &str,
path: &Path,
) -> anyhow::Result<fs::Metadata> {
let expected = primary_path(root, id)?;
if path != expected {
anyhow::bail!("session store object has unexpected path");
}
open_existing_primary(root, id)?
.map(|file| file.metadata())
.transpose()?
.ok_or_else(|| anyhow::anyhow!("session store object is missing"))
}
fn set_mode(path: &Path, mode: u32) -> io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(mode))?;
}
let _ = (path, mode);
Ok(())
}
#[cfg(test)]
pub(crate) fn secure_test_session_root(root: &Path) {
fs::create_dir_all(root).unwrap();
set_mode(root, SESSION_ROOT_MODE).unwrap();
for entry in fs::read_dir(root).unwrap().flatten() {
if entry
.file_type()
.map(|kind| kind.is_file())
.unwrap_or(false)
{
set_mode(&entry.path(), SESSION_FILE_MODE).unwrap();
}
}
}
#[cfg(unix)]
fn create_quarantine(
root_file: &std::fs::File,
name: &str,
) -> io::Result<(String, std::os::fd::RawFd)> {
use std::os::fd::AsRawFd;
for _ in 0..16 {
let quarantine_name = format!(
".{name}.prune-{}",
NEXT_QUARANTINE.fetch_add(1, Ordering::Relaxed)
);
let quarantine_c =
std::ffi::CString::new(quarantine_name.as_str()).expect("generated quarantine name");
let mkdir_result = unsafe {
libc::mkdirat(
root_file.as_raw_fd(),
quarantine_c.as_ptr(),
SESSION_ROOT_MODE as libc::mode_t,
)
};
if mkdir_result == 0 {
let quarantine_fd = unsafe {
libc::openat(
root_file.as_raw_fd(),
quarantine_c.as_ptr(),
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
)
};
if quarantine_fd >= 0 {
return Ok((quarantine_name, quarantine_fd));
}
let error = io::Error::last_os_error();
unsafe {
libc::unlinkat(
root_file.as_raw_fd(),
quarantine_c.as_ptr(),
libc::AT_REMOVEDIR,
);
}
return Err(error);
}
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::AlreadyExists {
return Err(error);
}
}
Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"unable to allocate session removal quarantine",
))
}
#[cfg(unix)]
fn stat_relative(
directory_fd: &std::os::fd::RawFd,
name: &std::ffi::CStr,
) -> io::Result<(u64, u64, bool)> {
let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
let result = unsafe {
libc::fstatat(
*directory_fd,
name.as_ptr(),
stat.as_mut_ptr(),
libc::AT_SYMLINK_NOFOLLOW,
)
};
if result != 0 {
return Err(io::Error::last_os_error());
}
let stat = unsafe { stat.assume_init() };
Ok((
stat.st_dev as u64,
stat.st_ino,
stat.st_mode & libc::S_IFMT == libc::S_IFDIR,
))
}
#[cfg(target_os = "linux")]
fn rename_noreplace(
source_dir: std::os::fd::RawFd,
source: &std::ffi::CStr,
target_dir: std::os::fd::RawFd,
target: &std::ffi::CStr,
) -> io::Result<()> {
let result = unsafe {
libc::renameat2(
source_dir,
source.as_ptr(),
target_dir,
target.as_ptr(),
libc::RENAME_NOREPLACE,
)
};
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
#[cfg(target_os = "macos")]
fn rename_noreplace(
source_dir: std::os::fd::RawFd,
source: &std::ffi::CStr,
target_dir: std::os::fd::RawFd,
target: &std::ffi::CStr,
) -> io::Result<()> {
let result = unsafe {
libc::renameatx_np(
source_dir,
source.as_ptr(),
target_dir,
target.as_ptr(),
libc::RENAME_EXCL,
)
};
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
pub(crate) fn remove_primary(root: &Path, id: &str) -> io::Result<()> {
let id = validate_session_id(id.to_string())
.map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
let name = format!("{id}.jsonl");
#[cfg(unix)]
{
use std::os::fd::{AsRawFd, FromRawFd};
use std::os::unix::fs::MetadataExt;
let root_file = open_root(root).map_err(|error| io::Error::other(error.to_string()))?;
let name_c = std::ffi::CString::new(name.clone()).expect("validated session filename");
let fd = unsafe {
libc::openat(
root_file.as_raw_fd(),
name_c.as_ptr(),
libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
)
};
if fd < 0 {
return Err(io::Error::last_os_error());
}
let candidate = unsafe { fs::File::from_raw_fd(fd) };
let candidate_metadata = candidate.metadata()?;
validate_file_metadata(&candidate_metadata)
.map_err(|error| io::Error::other(error.to_string()))?;
#[cfg(test)]
replace_primary_after_validation(root, &name)?;
let (quarantine_name, quarantine_fd) = create_quarantine(&root_file, &name)?;
let quarantine_c =
std::ffi::CString::new(quarantine_name.as_str()).expect("generated quarantine name");
let entry_c = std::ffi::CString::new("entry").unwrap();
let moved = unsafe {
libc::renameat(
root_file.as_raw_fd(),
name_c.as_ptr(),
quarantine_fd,
entry_c.as_ptr(),
)
};
if moved != 0 {
let error = io::Error::last_os_error();
unsafe {
libc::close(quarantine_fd);
libc::unlinkat(
root_file.as_raw_fd(),
quarantine_c.as_ptr(),
libc::AT_REMOVEDIR,
);
}
return Err(error);
}
let (moved_dev, moved_ino, moved_is_dir) = match stat_relative(&quarantine_fd, &entry_c) {
Ok(stat) => stat,
Err(error) => {
let restored =
rename_noreplace(quarantine_fd, &entry_c, root_file.as_raw_fd(), &name_c);
unsafe { libc::close(quarantine_fd) };
if restored.is_ok() {
unsafe {
libc::unlinkat(
root_file.as_raw_fd(),
quarantine_c.as_ptr(),
libc::AT_REMOVEDIR,
);
}
return Err(error);
}
return Err(io::Error::other(
"session store removal could not inspect or restore moved object",
));
}
};
let same_object =
candidate_metadata.dev() == moved_dev && candidate_metadata.ino() == moved_ino;
if same_object {
let result = unsafe {
libc::unlinkat(
quarantine_fd,
entry_c.as_ptr(),
if moved_is_dir { libc::AT_REMOVEDIR } else { 0 },
)
};
if result != 0 {
let error = io::Error::last_os_error();
unsafe { libc::close(quarantine_fd) };
return Err(error);
}
unsafe { libc::close(quarantine_fd) };
let result = unsafe {
libc::unlinkat(
root_file.as_raw_fd(),
quarantine_c.as_ptr(),
libc::AT_REMOVEDIR,
)
};
return if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
};
}
let restored = rename_noreplace(quarantine_fd, &entry_c, root_file.as_raw_fd(), &name_c);
unsafe { libc::close(quarantine_fd) };
if let Err(error) = restored {
if error.kind() == io::ErrorKind::AlreadyExists {
return Err(io::Error::other(
"session store object changed during removal; original path occupied",
));
}
return Err(error);
}
let result = unsafe {
libc::unlinkat(
root_file.as_raw_fd(),
quarantine_c.as_ptr(),
libc::AT_REMOVEDIR,
)
};
if result != 0 {
return Err(io::Error::last_os_error());
}
Err(io::Error::other(
"session store object changed during removal",
))
}
#[cfg(not(unix))]
{
open_root(root).map_err(|error| io::Error::other(error.to_string()))?;
fs::remove_file(root.join(name))
}
}
#[cfg(unix)]
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(unix)]
static NEXT_QUARANTINE: AtomicU64 = AtomicU64::new(0);
#[cfg(test)]
#[cfg(unix)]
fn replace_primary_after_validation(root: &Path, name: &str) -> io::Result<()> {
let replacement = REPLACE_PRIMARY_AFTER_VALIDATION.swap(0, Ordering::Relaxed);
if replacement == 0 {
return Ok(());
}
let original = root.join(name);
let saved = root.join(format!("{name}.validated-object"));
fs::rename(&original, saved)?;
match replacement {
1 => {
fs::write(&original, b"replacement")?;
set_mode(&original, SESSION_FILE_MODE)
}
2 => {
let target = root.join("replacement-target");
fs::write(&target, b"symlink replacement")?;
std::os::unix::fs::symlink("replacement-target", original)
}
3 => {
fs::create_dir(&original)?;
fs::write(original.join("contents"), b"directory replacement")
}
_ => unreachable!(),
}
}
#[cfg(test)]
#[cfg(unix)]
static REPLACE_PRIMARY_AFTER_VALIDATION: std::sync::atomic::AtomicU8 =
std::sync::atomic::AtomicU8::new(0);
#[cfg(test)]
#[cfg(unix)]
static TEST_REPLACEMENT_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
#[cfg(unix)]
fn arm_primary_replacement() {
REPLACE_PRIMARY_AFTER_VALIDATION.store(1, Ordering::Relaxed);
}
#[cfg(test)]
#[cfg(unix)]
fn arm_symlink_replacement() {
REPLACE_PRIMARY_AFTER_VALIDATION.store(2, Ordering::Relaxed);
}
#[cfg(test)]
#[cfg(unix)]
fn arm_directory_replacement() {
REPLACE_PRIMARY_AFTER_VALIDATION.store(3, Ordering::Relaxed);
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[cfg(unix)]
#[test]
fn broad_root_has_shared_repair_plan_and_concise_startup_error() {
use std::os::unix::fs::PermissionsExt;
let temp = TempDir::new().unwrap();
let root = temp.path().join("sessions");
fs::create_dir(&root).unwrap();
fs::write(root.join("session.jsonl"), b"session").unwrap();
fs::set_permissions(&root, fs::Permissions::from_mode(0o755)).unwrap();
fs::set_permissions(
root.join("session.jsonl"),
fs::Permissions::from_mode(0o644),
)
.unwrap();
let plan = discover_repair_plan(&root).unwrap();
assert_eq!(plan.target_count(), 2);
let error = prepare_session_root(&root).unwrap_err().to_string();
assert!(error.contains("magi-code sessions repair-permissions"));
assert!(!error.contains("chmod"));
}
#[cfg(unix)]
#[test]
fn repair_plan_repairs_legacy_metadata_file_mode() {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let temp = TempDir::new().unwrap();
let root = temp.path().join("sessions");
fs::create_dir(&root).unwrap();
let session = root.join("session.jsonl");
let metadata = root.join("session.metadata.json");
fs::write(&session, b"session").unwrap();
fs::write(&metadata, b"metadata").unwrap();
fs::set_permissions(&root, fs::Permissions::from_mode(0o755)).unwrap();
fs::set_permissions(&session, fs::Permissions::from_mode(0o644)).unwrap();
fs::set_permissions(&metadata, fs::Permissions::from_mode(0o644)).unwrap();
let plan = discover_repair_plan(&root).unwrap();
assert_eq!(plan.target_count(), 3);
apply_repair_plan(&plan).unwrap();
assert_eq!(fs::symlink_metadata(&root).unwrap().mode() & 0o777, 0o700);
assert_eq!(
fs::symlink_metadata(&session).unwrap().mode() & 0o777,
0o600
);
assert_eq!(
fs::symlink_metadata(&metadata).unwrap().mode() & 0o777,
0o600
);
}
#[cfg(unix)]
#[test]
fn repair_plan_rejects_file_without_owner_read_with_diagnostic() {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let temp = TempDir::new().unwrap();
let root = temp.path().join("sessions");
fs::create_dir(&root).unwrap();
let path = root.join("session.jsonl");
fs::write(&path, b"session").unwrap();
fs::set_permissions(&root, fs::Permissions::from_mode(0o755)).unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o044)).unwrap();
assert!(discover_repair_plan(&root).is_err());
let error = prepare_session_root(&root).unwrap_err().to_string();
assert!(!error.contains("magi-code sessions repair-permissions"));
assert!(error.contains("unsafe, unreadable, or unrecognized"));
assert_eq!(fs::symlink_metadata(&root).unwrap().mode() & 0o777, 0o755);
assert_eq!(fs::symlink_metadata(&path).unwrap().mode() & 0o777, 0o044);
}
#[cfg(unix)]
#[test]
fn repair_plan_covers_subagents_and_history() {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let temp = TempDir::new().unwrap();
let root = temp.path().join("sessions");
let history = root.join(".history").join("parent");
let child_history = root.join("subagents").join(".history").join("child");
fs::create_dir_all(&history).unwrap();
fs::create_dir_all(&child_history).unwrap();
fs::write(root.join("parent.jsonl"), b"parent").unwrap();
fs::write(history.join("1.jsonl"), b"archive").unwrap();
fs::write(root.join("subagents/child.jsonl"), b"child").unwrap();
fs::write(child_history.join("2.jsonl"), b"archive").unwrap();
fs::set_permissions(&root, fs::Permissions::from_mode(0o755)).unwrap();
let plan = discover_repair_plan(&root).unwrap();
assert_eq!(plan.target_count(), 10);
apply_repair_plan(&plan).unwrap();
assert_eq!(fs::symlink_metadata(&root).unwrap().mode() & 0o777, 0o700);
for target in &plan.targets {
let path = if target.relative.as_os_str().is_empty() {
root.clone()
} else {
root.join(&target.relative)
};
let metadata = fs::symlink_metadata(path).unwrap();
assert_eq!(
metadata.mode() & 0o777,
if target.is_dir { 0o700 } else { 0o600 }
);
}
}
#[cfg(unix)]
#[test]
fn unsafe_symlink_rejects_without_mutation() {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let temp = TempDir::new().unwrap();
let root = temp.path().join("sessions");
fs::create_dir(&root).unwrap();
fs::set_permissions(&root, fs::Permissions::from_mode(0o755)).unwrap();
let outside = temp.path().join("outside.jsonl");
fs::write(&outside, b"outside").unwrap();
std::os::unix::fs::symlink(&outside, root.join("id.jsonl")).unwrap();
assert!(discover_repair_plan(&root).is_err());
assert_eq!(fs::symlink_metadata(&root).unwrap().mode() & 0o777, 0o755);
assert_eq!(fs::symlink_metadata(outside).unwrap().mode() & 0o777, 0o644);
}
#[cfg(unix)]
#[test]
fn replacement_after_preview_causes_no_mutation() {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let temp = TempDir::new().unwrap();
let root = temp.path().join("sessions");
fs::create_dir(&root).unwrap();
let path = root.join("id.jsonl");
fs::write(&path, b"original").unwrap();
fs::set_permissions(&root, fs::Permissions::from_mode(0o755)).unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
let plan = discover_repair_plan(&root).unwrap();
fs::rename(&path, root.join("saved")).unwrap();
fs::write(&path, b"replacement").unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
assert!(apply_repair_plan(&plan).is_err());
assert_eq!(fs::symlink_metadata(&root).unwrap().mode() & 0o777, 0o755);
assert_eq!(fs::symlink_metadata(&path).unwrap().mode() & 0o777, 0o644);
}
#[cfg(unix)]
#[test]
fn primary_rejects_hard_links() {
let temp = TempDir::new().unwrap();
let root = temp.path().join("sessions");
prepare_session_root(&root).unwrap();
let source = root.join("source");
fs::write(&source, b"x").unwrap();
fs::hard_link(&source, root.join("id.jsonl")).unwrap();
assert!(open_primary(&root, "id").is_err());
assert!(open_primary(temp.path(), "id").is_err());
}
#[cfg(unix)]
#[test]
fn sidecar_rejects_hard_links_and_symlinks() {
let temp = TempDir::new().unwrap();
let root = temp.path().join("sessions");
prepare_session_root(&root).unwrap();
let source = root.join("source");
fs::write(&source, b"metadata").unwrap();
fs::hard_link(&source, root.join("id.metadata.json")).unwrap();
assert!(open_existing_named(&root, "id.metadata.json").is_err());
fs::remove_file(root.join("id.metadata.json")).unwrap();
std::os::unix::fs::symlink(&source, root.join("id.metadata.json")).unwrap();
assert!(open_existing_named(&root, "id.metadata.json").is_err());
}
#[test]
fn production_remove_uses_validated_relative_primary() {
let temp = TempDir::new().unwrap();
let root = temp.path().join("sessions");
prepare_session_root(&root).unwrap();
fs::write(root.join("old.jsonl"), b"session").unwrap();
set_mode(&root.join("old.jsonl"), SESSION_FILE_MODE).unwrap();
#[cfg(unix)]
let _test_guard = TEST_REPLACEMENT_LOCK.lock().unwrap();
remove_primary(&root, "old").unwrap();
assert!(!root.join("old.jsonl").exists());
}
#[cfg(unix)]
#[test]
fn production_remove_retains_replacement_after_validation_race() {
let temp = TempDir::new().unwrap();
let root = temp.path().join("sessions");
prepare_session_root(&root).unwrap();
let path = root.join("raced.jsonl");
fs::write(&path, b"validated").unwrap();
set_mode(&path, SESSION_FILE_MODE).unwrap();
let _test_guard = TEST_REPLACEMENT_LOCK.lock().unwrap();
arm_primary_replacement();
let error = remove_primary(&root, "raced").unwrap_err();
assert!(error.to_string().contains("changed during removal"));
assert_eq!(fs::read(&path).unwrap(), b"replacement");
assert_eq!(
fs::read(root.join("raced.jsonl.validated-object")).unwrap(),
b"validated"
);
}
#[cfg(unix)]
#[test]
fn production_remove_retains_symlink_replacement_after_validation_race() {
let temp = TempDir::new().unwrap();
let root = temp.path().join("sessions");
prepare_session_root(&root).unwrap();
let path = root.join("raced-symlink.jsonl");
fs::write(&path, b"validated").unwrap();
set_mode(&path, SESSION_FILE_MODE).unwrap();
let _test_guard = TEST_REPLACEMENT_LOCK.lock().unwrap();
arm_symlink_replacement();
let error = remove_primary(&root, "raced-symlink").unwrap_err();
assert!(error.to_string().contains("changed during removal"));
assert!(
fs::symlink_metadata(&path)
.unwrap()
.file_type()
.is_symlink()
);
assert_eq!(fs::read(&path).unwrap(), b"symlink replacement");
assert_eq!(
fs::read(root.join("raced-symlink.jsonl.validated-object")).unwrap(),
b"validated"
);
}
#[cfg(unix)]
#[test]
fn production_remove_retains_directory_replacement_after_validation_race() {
let temp = TempDir::new().unwrap();
let root = temp.path().join("sessions");
prepare_session_root(&root).unwrap();
let path = root.join("raced-directory.jsonl");
fs::write(&path, b"validated").unwrap();
set_mode(&path, SESSION_FILE_MODE).unwrap();
let _test_guard = TEST_REPLACEMENT_LOCK.lock().unwrap();
arm_directory_replacement();
let error = remove_primary(&root, "raced-directory").unwrap_err();
assert!(error.to_string().contains("changed during removal"));
assert!(fs::symlink_metadata(&path).unwrap().file_type().is_dir());
assert_eq!(
fs::read(path.join("contents")).unwrap(),
b"directory replacement"
);
assert_eq!(
fs::read(root.join("raced-directory.jsonl.validated-object")).unwrap(),
b"validated"
);
}
#[cfg(unix)]
#[test]
fn relative_open_stays_bound_to_original_root_descriptor() {
let temp = TempDir::new().unwrap();
let root = temp.path().join("sessions");
prepare_session_root(&root).unwrap();
fs::write(root.join("id.jsonl"), b"original").unwrap();
let root_file = open_root(&root).unwrap();
let moved = temp.path().join("moved");
fs::rename(&root, &moved).unwrap();
fs::create_dir(&root).unwrap();
fs::write(root.join("id.jsonl"), b"replacement").unwrap();
use std::io::Read;
use std::os::fd::{AsRawFd, FromRawFd};
let name = std::ffi::CString::new("id.jsonl").unwrap();
let fd = unsafe { libc::openat(root_file.as_raw_fd(), name.as_ptr(), libc::O_RDONLY) };
assert!(fd >= 0);
let mut file = unsafe { fs::File::from_raw_fd(fd) };
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
assert_eq!(contents, "original");
}
}