use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub(crate) type FdNumber = i32;
pub(crate) type ProcessId = u64;
pub(crate) type ModelThreadId = u64;
type OpenFileId = u64;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum ShadowKind {
HostSameFd,
LocalOnly,
HostPassthrough,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ShadowObject {
pub(crate) kind: ShadowKind,
pub(crate) supervisor_fd: i32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum ShadowBacking {
Ephemeral,
GuestPath { path: String, writable: bool },
HostPath { path: String, writable: bool },
HostPassthrough { label: String },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct DescriptorInfo {
pub(crate) fd: FdNumber,
pub(crate) lkl_fd: Option<i64>,
pub(crate) shadow: Option<ShadowObject>,
pub(crate) cloexec: bool,
pub(crate) open_file: u64,
pub(crate) ref_count: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum FdOperation {
Read,
Write,
Readv,
Writev,
Preadv,
Pwritev,
Pwritev2,
Mmap,
FcntlLock,
Generic,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum FdRoute {
Lkl { fd: i64 },
HostShadow { shadow: ShadowObject },
HostPassthrough { shadow: ShadowObject },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ShadowInstallPlan {
pub(crate) source_fd: FdNumber,
pub(crate) target_fd: FdNumber,
pub(crate) shadow: ShadowObject,
pub(crate) cloexec: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ShadowInstallPlanWithBacking {
pub(crate) install: ShadowInstallPlan,
pub(crate) backing: Option<ShadowBacking>,
}
pub(crate) trait ShadowRebuilder {
fn rebuild_shadow(
&mut self,
kind: ShadowKind,
backing: &ShadowBacking,
) -> Result<ShadowObject, FdError>;
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Descriptor {
open_file: OpenFileId,
cloexec: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct OpenFileDescription {
lkl_fd: Option<i64>,
shadow: Option<ShadowObject>,
shadow_backing: Option<ShadowBacking>,
#[serde(default)]
socket_shutdown: Option<KboxlikeSocketShutdownState>,
ref_count: usize,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct KboxlikeSocketShutdownState {
pub(crate) read_closed: bool,
pub(crate) write_closed: bool,
}
impl KboxlikeSocketShutdownState {
pub(crate) fn from_shutdown_how(how: i32) -> Result<Self, FdError> {
match how {
libc::SHUT_RD => Ok(Self {
read_closed: true,
write_closed: false,
}),
libc::SHUT_WR => Ok(Self {
read_closed: false,
write_closed: true,
}),
libc::SHUT_RDWR => Ok(Self {
read_closed: true,
write_closed: true,
}),
_ => Err(FdError::TraceeInstallFailed("invalid socket shutdown mode")),
}
}
pub(crate) fn merge(self, other: Self) -> Self {
Self {
read_closed: self.read_closed || other.read_closed,
write_closed: self.write_closed || other.write_closed,
}
}
pub(crate) fn is_empty(self) -> bool {
!self.read_closed && !self.write_closed
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
struct ProcessFdTable {
parent: Option<ProcessId>,
current_exe: Option<String>,
fds: BTreeMap<FdNumber, Descriptor>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct KboxlikeFdSnapshot {
next_pid: ProcessId,
next_open_file: OpenFileId,
processes: BTreeMap<ProcessId, ProcessFdTable>,
open_files: BTreeMap<OpenFileId, OpenFileDescription>,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum FdError {
BadProcess(ProcessId),
BadFd(FdNumber),
InvalidFd(FdNumber),
NoAvailableFd,
NoBackingFd(FdNumber),
InvalidSnapshot(&'static str),
ShadowRebuildFailed(&'static str),
ShadowRebuildUnsupported(&'static str),
TraceeInstallFailed(&'static str),
}
#[derive(Debug)]
pub(crate) struct KboxlikeFdSystem {
next_pid: ProcessId,
next_open_file: OpenFileId,
processes: BTreeMap<ProcessId, ProcessFdTable>,
open_files: BTreeMap<OpenFileId, OpenFileDescription>,
}
impl Default for KboxlikeFdSystem {
fn default() -> Self {
Self::new()
}
}
impl KboxlikeFdSystem {
pub(crate) fn new() -> Self {
Self {
next_pid: 1,
next_open_file: 1,
processes: BTreeMap::new(),
open_files: BTreeMap::new(),
}
}
pub(crate) fn spawn_init(&mut self) -> ProcessId {
self.spawn_init_inner(None)
}
pub(crate) fn spawn_init_with_exe(&mut self, exe: impl Into<String>) -> ProcessId {
self.spawn_init_inner(Some(exe.into()))
}
fn spawn_init_inner(&mut self, current_exe: Option<String>) -> ProcessId {
let pid = self.alloc_pid();
self.processes.insert(
pid,
ProcessFdTable {
current_exe,
..ProcessFdTable::default()
},
);
pid
}
pub(crate) fn fork(&mut self, parent: ProcessId) -> Result<ProcessId, FdError> {
let mut child_table = self
.processes
.get(&parent)
.ok_or(FdError::BadProcess(parent))?
.clone();
for desc in child_table.fds.values() {
self.open_file_mut(desc.open_file).ref_count += 1;
}
child_table.parent = Some(parent);
let child = self.alloc_pid();
self.processes.insert(child, child_table);
Ok(child)
}
pub(crate) fn insert_shadow_fd(
&mut self,
pid: ProcessId,
fd: FdNumber,
lkl_fd: Option<i64>,
shadow: ShadowObject,
cloexec: bool,
) -> Result<(), FdError> {
self.validate_fd(fd)?;
self.replace_descriptor(
pid,
fd,
lkl_fd,
Some(shadow),
Some(ShadowBacking::Ephemeral),
cloexec,
)
}
pub(crate) fn insert_shadow_fd_with_backing(
&mut self,
pid: ProcessId,
fd: FdNumber,
lkl_fd: Option<i64>,
shadow: ShadowObject,
backing: ShadowBacking,
cloexec: bool,
) -> Result<(), FdError> {
self.validate_fd(fd)?;
self.replace_descriptor(pid, fd, lkl_fd, Some(shadow), Some(backing), cloexec)
}
pub(crate) fn insert_plain_fd(
&mut self,
pid: ProcessId,
fd: FdNumber,
lkl_fd: Option<i64>,
cloexec: bool,
) -> Result<(), FdError> {
self.validate_fd(fd)?;
self.replace_descriptor(pid, fd, lkl_fd, None, None, cloexec)
}
pub(crate) fn dup(&mut self, pid: ProcessId, oldfd: FdNumber) -> Result<FdNumber, FdError> {
self.dup_from_min(pid, oldfd, 0, false)
}
pub(crate) fn dup_cloexec(
&mut self,
pid: ProcessId,
oldfd: FdNumber,
minfd: FdNumber,
) -> Result<FdNumber, FdError> {
self.dup_from_min(pid, oldfd, minfd, true)
}
pub(crate) fn dup_from_min(
&mut self,
pid: ProcessId,
oldfd: FdNumber,
minfd: FdNumber,
cloexec: bool,
) -> Result<FdNumber, FdError> {
let newfd = self.next_available_fd(pid, minfd)?;
self.dup_to(pid, oldfd, newfd, cloexec)?;
Ok(newfd)
}
pub(crate) fn dup_to(
&mut self,
pid: ProcessId,
oldfd: FdNumber,
newfd: FdNumber,
cloexec: bool,
) -> Result<(), FdError> {
self.validate_fd(oldfd)?;
self.validate_fd(newfd)?;
if oldfd == newfd {
if self
.processes
.get(&pid)
.ok_or(FdError::BadProcess(pid))?
.fds
.contains_key(&oldfd)
{
return Ok(());
}
return Err(FdError::BadFd(oldfd));
}
let old = self
.processes
.get(&pid)
.ok_or(FdError::BadProcess(pid))?
.fds
.get(&oldfd)
.cloned()
.ok_or(FdError::BadFd(oldfd))?;
self.close_if_present(pid, newfd)?;
self.open_file_mut(old.open_file).ref_count += 1;
self.process_mut(pid)?.fds.insert(
newfd,
Descriptor {
open_file: old.open_file,
cloexec,
},
);
Ok(())
}
pub(crate) fn alias_fd_from(
&mut self,
source_pid: ProcessId,
source_fd: FdNumber,
target_pid: ProcessId,
target_fd: FdNumber,
cloexec: bool,
) -> Result<(), FdError> {
self.validate_fd(source_fd)?;
self.validate_fd(target_fd)?;
if source_pid == target_pid && source_fd == target_fd {
return self.set_cloexec(target_pid, target_fd, cloexec);
}
let source = self
.processes
.get(&source_pid)
.ok_or(FdError::BadProcess(source_pid))?
.fds
.get(&source_fd)
.cloned()
.ok_or(FdError::BadFd(source_fd))?;
self.close_if_present(target_pid, target_fd)?;
self.open_file_mut(source.open_file).ref_count += 1;
self.process_mut(target_pid)?.fds.insert(
target_fd,
Descriptor {
open_file: source.open_file,
cloexec,
},
);
Ok(())
}
pub(crate) fn set_cloexec(
&mut self,
pid: ProcessId,
fd: FdNumber,
cloexec: bool,
) -> Result<(), FdError> {
let desc = self
.process_mut(pid)?
.fds
.get_mut(&fd)
.ok_or(FdError::BadFd(fd))?;
desc.cloexec = cloexec;
Ok(())
}
pub(crate) fn close(&mut self, pid: ProcessId, fd: FdNumber) -> Result<(), FdError> {
self.validate_fd(fd)?;
let removed = self
.process_mut(pid)?
.fds
.remove(&fd)
.ok_or(FdError::BadFd(fd))?;
self.release_open_file(removed.open_file);
Ok(())
}
pub(crate) fn exec(&mut self, pid: ProcessId) -> Result<(), FdError> {
self.exec_inner(pid, None)
}
pub(crate) fn exec_with_path(
&mut self,
pid: ProcessId,
exe: impl Into<String>,
) -> Result<(), FdError> {
self.exec_inner(pid, Some(exe.into()))
}
fn exec_inner(&mut self, pid: ProcessId, exe: Option<String>) -> Result<(), FdError> {
let cloexec_fds: Vec<FdNumber> = self
.processes
.get(&pid)
.ok_or(FdError::BadProcess(pid))?
.fds
.iter()
.filter_map(|(fd, desc)| desc.cloexec.then_some(*fd))
.collect();
for fd in cloexec_fds {
let removed = self
.process_mut(pid)?
.fds
.remove(&fd)
.expect("fd collected from same table");
self.release_open_file(removed.open_file);
}
if let Some(exe) = exe {
self.process_mut(pid)?.current_exe = Some(exe);
}
Ok(())
}
pub(crate) fn current_exe(&self, pid: ProcessId) -> Result<Option<&str>, FdError> {
Ok(self
.processes
.get(&pid)
.ok_or(FdError::BadProcess(pid))?
.current_exe
.as_deref())
}
pub(crate) fn parent(&self, pid: ProcessId) -> Result<Option<ProcessId>, FdError> {
Ok(self
.processes
.get(&pid)
.ok_or(FdError::BadProcess(pid))?
.parent)
}
pub(crate) fn resolve_proc_exe(
&self,
caller: ProcessId,
path: &str,
) -> Result<Option<String>, FdError> {
let target = if path == "/proc/self/exe" || path == "/proc/thread-self/exe" {
Some(caller)
} else {
path.strip_prefix("/proc/")
.and_then(|rest| rest.strip_suffix("/exe"))
.and_then(|pid| pid.parse::<ProcessId>().ok())
};
let Some(target) = target else {
return Ok(None);
};
Ok(self
.processes
.get(&target)
.ok_or(FdError::BadProcess(target))?
.current_exe
.clone())
}
pub(crate) fn exit_process(&mut self, pid: ProcessId) -> Result<(), FdError> {
self.remove_process(pid)
}
pub(crate) fn kill_subtree(&mut self, root: ProcessId) -> Result<Vec<ProcessId>, FdError> {
if !self.processes.contains_key(&root) {
return Err(FdError::BadProcess(root));
}
let mut victims = Vec::new();
self.collect_subtree(root, &mut victims);
for pid in &victims {
self.remove_process(*pid)?;
}
Ok(victims)
}
pub(crate) fn process_count(&self) -> usize {
self.processes.len()
}
pub(crate) fn snapshot(&self) -> KboxlikeFdSnapshot {
KboxlikeFdSnapshot {
next_pid: self.next_pid,
next_open_file: self.next_open_file,
processes: self.processes.clone(),
open_files: self.open_files.clone(),
}
}
pub(crate) fn restore(snapshot: KboxlikeFdSnapshot) -> Result<Self, FdError> {
validate_snapshot(&snapshot)?;
Ok(Self {
next_pid: snapshot.next_pid,
next_open_file: snapshot.next_open_file,
processes: snapshot.processes,
open_files: snapshot.open_files,
})
}
pub(crate) fn restore_rebuilding_shadows(
mut snapshot: KboxlikeFdSnapshot,
rebuilder: &mut impl ShadowRebuilder,
) -> Result<Self, FdError> {
validate_snapshot(&snapshot)?;
for open_file in snapshot.open_files.values_mut() {
let Some(shadow) = open_file.shadow else {
continue;
};
let backing = open_file
.shadow_backing
.as_ref()
.expect("validated shadow backing");
if matches!(backing, ShadowBacking::Ephemeral) {
return Err(FdError::InvalidSnapshot(
"ephemeral shadow cannot be rebuilt after restore",
));
}
open_file.shadow = Some(rebuilder.rebuild_shadow(shadow.kind, backing)?);
}
Ok(Self {
next_pid: snapshot.next_pid,
next_open_file: snapshot.next_open_file,
processes: snapshot.processes,
open_files: snapshot.open_files,
})
}
pub(crate) fn descriptor(
&self,
pid: ProcessId,
fd: FdNumber,
) -> Result<DescriptorInfo, FdError> {
let table = self.processes.get(&pid).ok_or(FdError::BadProcess(pid))?;
let desc = table.fds.get(&fd).ok_or(FdError::BadFd(fd))?;
let ofd = self
.open_files
.get(&desc.open_file)
.expect("descriptor references live open file");
Ok(DescriptorInfo {
fd,
lkl_fd: ofd.lkl_fd,
shadow: ofd.shadow,
cloexec: desc.cloexec,
open_file: desc.open_file,
ref_count: ofd.ref_count,
})
}
pub(crate) fn shadow_backing(
&self,
pid: ProcessId,
fd: FdNumber,
) -> Result<Option<&ShadowBacking>, FdError> {
let table = self.processes.get(&pid).ok_or(FdError::BadProcess(pid))?;
let desc = table.fds.get(&fd).ok_or(FdError::BadFd(fd))?;
Ok(self
.open_files
.get(&desc.open_file)
.expect("descriptor references live open file")
.shadow_backing
.as_ref())
}
pub(crate) fn set_socket_shutdown(
&mut self,
pid: ProcessId,
fd: FdNumber,
how: i32,
) -> Result<(), FdError> {
self.validate_fd(fd)?;
let shutdown = KboxlikeSocketShutdownState::from_shutdown_how(how)?;
let open_file = self
.processes
.get(&pid)
.ok_or(FdError::BadProcess(pid))?
.fds
.get(&fd)
.cloned()
.ok_or(FdError::BadFd(fd))?
.open_file;
let ofd = self.open_file_mut(open_file);
ofd.socket_shutdown = Some(ofd.socket_shutdown.unwrap_or_default().merge(shutdown));
Ok(())
}
pub(crate) fn socket_shutdown(
&self,
pid: ProcessId,
fd: FdNumber,
) -> Result<Option<KboxlikeSocketShutdownState>, FdError> {
self.validate_fd(fd)?;
let table = self.processes.get(&pid).ok_or(FdError::BadProcess(pid))?;
let desc = table.fds.get(&fd).ok_or(FdError::BadFd(fd))?;
Ok(self
.open_files
.get(&desc.open_file)
.expect("descriptor references live open file")
.socket_shutdown)
}
pub(crate) fn route_fd_operation(
&self,
pid: ProcessId,
fd: FdNumber,
operation: FdOperation,
) -> Result<FdRoute, FdError> {
let info = self.descriptor(pid, fd)?;
match operation {
FdOperation::Mmap => self.route_host_if_shadow_else_lkl(&info),
FdOperation::Write
| FdOperation::Readv
| FdOperation::Writev
| FdOperation::Preadv
| FdOperation::Pwritev
| FdOperation::Pwritev2
| FdOperation::FcntlLock => match info.shadow {
Some(
shadow @ ShadowObject {
kind: ShadowKind::HostSameFd,
..
},
) => Ok(FdRoute::HostShadow { shadow }),
Some(
shadow @ ShadowObject {
kind: ShadowKind::HostPassthrough,
..
},
) => Ok(FdRoute::HostPassthrough { shadow }),
_ => self.route_lkl_or_shadow(&info),
},
FdOperation::Read | FdOperation::Generic => match info.shadow {
Some(
shadow @ ShadowObject {
kind: ShadowKind::HostPassthrough,
..
},
) => Ok(FdRoute::HostPassthrough { shadow }),
_ => self.route_lkl_or_shadow(&info),
},
}
}
pub(crate) fn plan_shadow_install(
&self,
pid: ProcessId,
source_fd: FdNumber,
target_fd: FdNumber,
cloexec: bool,
) -> Result<Option<ShadowInstallPlan>, FdError> {
self.validate_fd(target_fd)?;
let info = self.descriptor(pid, source_fd)?;
Ok(info.shadow.map(|shadow| ShadowInstallPlan {
source_fd,
target_fd,
shadow,
cloexec,
}))
}
pub(crate) fn process_shadow_install_plans(
&self,
pid: ProcessId,
) -> Result<Vec<ShadowInstallPlan>, FdError> {
Ok(self
.process_shadow_install_plans_with_backing(pid)?
.into_iter()
.map(|plan| plan.install)
.collect())
}
pub(crate) fn process_shadow_install_plans_with_backing(
&self,
pid: ProcessId,
) -> Result<Vec<ShadowInstallPlanWithBacking>, FdError> {
let table = self.processes.get(&pid).ok_or(FdError::BadProcess(pid))?;
let mut plans = Vec::new();
for (fd, desc) in &table.fds {
let open_file = self
.open_files
.get(&desc.open_file)
.expect("descriptor references live open file");
if let Some(shadow) = open_file.shadow {
plans.push(ShadowInstallPlanWithBacking {
install: ShadowInstallPlan {
source_fd: *fd,
target_fd: *fd,
shadow,
cloexec: desc.cloexec,
},
backing: open_file.shadow_backing.clone(),
});
}
}
Ok(plans)
}
pub(crate) fn plan_operation_shadow_install(
&self,
pid: ProcessId,
fd: FdNumber,
operation: FdOperation,
) -> Result<Option<ShadowInstallPlan>, FdError> {
match self.route_fd_operation(pid, fd, operation)? {
FdRoute::Lkl { .. } => Ok(None),
FdRoute::HostShadow { shadow } | FdRoute::HostPassthrough { shadow } => {
Ok(Some(ShadowInstallPlan {
source_fd: fd,
target_fd: fd,
shadow,
cloexec: false,
}))
}
}
}
fn route_host_if_shadow_else_lkl(&self, info: &DescriptorInfo) -> Result<FdRoute, FdError> {
match info.shadow {
Some(
shadow @ ShadowObject {
kind: ShadowKind::HostPassthrough,
..
},
) => Ok(FdRoute::HostPassthrough { shadow }),
Some(shadow) => Ok(FdRoute::HostShadow { shadow }),
None => self.route_lkl_or_shadow(info),
}
}
fn route_lkl_or_shadow(&self, info: &DescriptorInfo) -> Result<FdRoute, FdError> {
if let Some(fd) = info.lkl_fd {
return Ok(FdRoute::Lkl { fd });
}
match info.shadow {
Some(
shadow @ ShadowObject {
kind: ShadowKind::HostPassthrough,
..
},
) => Ok(FdRoute::HostPassthrough { shadow }),
Some(shadow) => Ok(FdRoute::HostShadow { shadow }),
None => Err(FdError::NoBackingFd(info.fd)),
}
}
fn alloc_pid(&mut self) -> ProcessId {
let pid = self.next_pid;
self.next_pid += 1;
pid
}
fn alloc_open_file(
&mut self,
lkl_fd: Option<i64>,
shadow: Option<ShadowObject>,
shadow_backing: Option<ShadowBacking>,
) -> OpenFileId {
let id = self.next_open_file;
self.next_open_file += 1;
self.open_files.insert(
id,
OpenFileDescription {
lkl_fd,
shadow,
shadow_backing,
socket_shutdown: None,
ref_count: 1,
},
);
id
}
fn replace_descriptor(
&mut self,
pid: ProcessId,
fd: FdNumber,
lkl_fd: Option<i64>,
shadow: Option<ShadowObject>,
shadow_backing: Option<ShadowBacking>,
cloexec: bool,
) -> Result<(), FdError> {
self.close_if_present(pid, fd)?;
let open_file = self.alloc_open_file(lkl_fd, shadow, shadow_backing);
self.process_mut(pid)?
.fds
.insert(fd, Descriptor { open_file, cloexec });
Ok(())
}
fn close_if_present(&mut self, pid: ProcessId, fd: FdNumber) -> Result<(), FdError> {
let removed = self.process_mut(pid)?.fds.remove(&fd);
if let Some(desc) = removed {
self.release_open_file(desc.open_file);
}
Ok(())
}
fn remove_process(&mut self, pid: ProcessId) -> Result<(), FdError> {
let table = self
.processes
.remove(&pid)
.ok_or(FdError::BadProcess(pid))?;
for desc in table.fds.into_values() {
self.release_open_file(desc.open_file);
}
Ok(())
}
fn collect_subtree(&self, root: ProcessId, out: &mut Vec<ProcessId>) {
out.push(root);
let children: Vec<ProcessId> = self
.processes
.iter()
.filter_map(|(pid, table)| (table.parent == Some(root)).then_some(*pid))
.collect();
for child in children {
self.collect_subtree(child, out);
}
}
fn release_open_file(&mut self, open_file: OpenFileId) {
let ofd = self.open_file_mut(open_file);
ofd.ref_count -= 1;
if ofd.ref_count == 0 {
self.open_files.remove(&open_file);
}
}
fn next_available_fd(&self, pid: ProcessId, minfd: FdNumber) -> Result<FdNumber, FdError> {
self.validate_fd(minfd)?;
let table = self.processes.get(&pid).ok_or(FdError::BadProcess(pid))?;
for fd in minfd..=i32::MAX {
if !table.fds.contains_key(&fd) {
return Ok(fd);
}
}
Err(FdError::NoAvailableFd)
}
fn process_mut(&mut self, pid: ProcessId) -> Result<&mut ProcessFdTable, FdError> {
self.processes.get_mut(&pid).ok_or(FdError::BadProcess(pid))
}
fn open_file_mut(&mut self, id: OpenFileId) -> &mut OpenFileDescription {
self.open_files
.get_mut(&id)
.expect("descriptor references live open file")
}
fn validate_fd(&self, fd: FdNumber) -> Result<(), FdError> {
if fd < 0 {
Err(FdError::InvalidFd(fd))
} else {
Ok(())
}
}
}
fn validate_snapshot(snapshot: &KboxlikeFdSnapshot) -> Result<(), FdError> {
if snapshot.next_pid == 0 || snapshot.next_open_file == 0 {
return Err(FdError::InvalidSnapshot("next ids must be non-zero"));
}
if snapshot
.processes
.keys()
.any(|pid| *pid == 0 || *pid >= snapshot.next_pid)
{
return Err(FdError::InvalidSnapshot(
"process id outside snapshot allocation range",
));
}
if snapshot
.open_files
.keys()
.any(|id| *id == 0 || *id >= snapshot.next_open_file)
{
return Err(FdError::InvalidSnapshot(
"open-file id outside snapshot allocation range",
));
}
let mut refs: BTreeMap<OpenFileId, usize> = BTreeMap::new();
for table in snapshot.processes.values() {
if let Some(parent) = table.parent {
if !snapshot.processes.contains_key(&parent) {
return Err(FdError::InvalidSnapshot(
"process parent references missing process",
));
}
}
for (fd, desc) in &table.fds {
if *fd < 0 {
return Err(FdError::InvalidSnapshot("snapshot contains negative fd"));
}
if !snapshot.open_files.contains_key(&desc.open_file) {
return Err(FdError::InvalidSnapshot(
"descriptor references missing open-file description",
));
}
*refs.entry(desc.open_file).or_insert(0) += 1;
}
}
for (id, open_file) in &snapshot.open_files {
if refs.get(id).copied().unwrap_or(0) != open_file.ref_count {
return Err(FdError::InvalidSnapshot(
"open-file ref_count does not match descriptor references",
));
}
match (&open_file.shadow, &open_file.shadow_backing) {
(Some(_), Some(_)) => {}
(Some(_), None) => {
return Err(FdError::InvalidSnapshot(
"shadow open-file is missing reconstruction backing",
));
}
(None, Some(_)) => {
return Err(FdError::InvalidSnapshot(
"non-shadow open-file has shadow reconstruction backing",
));
}
(None, None) => {}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn shadow(supervisor_fd: i32) -> ShadowObject {
ShadowObject {
kind: ShadowKind::HostSameFd,
supervisor_fd,
}
}
fn local_shadow(supervisor_fd: i32) -> ShadowObject {
ShadowObject {
kind: ShadowKind::LocalOnly,
supervisor_fd,
}
}
fn host_passthrough(supervisor_fd: i32) -> ShadowObject {
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd,
}
}
fn guest_path(path: &str) -> ShadowBacking {
ShadowBacking::GuestPath {
path: path.to_string(),
writable: false,
}
}
#[derive(Default)]
struct RecordingRebuilder {
next_fd: i32,
calls: Vec<(ShadowKind, ShadowBacking)>,
}
impl ShadowRebuilder for RecordingRebuilder {
fn rebuild_shadow(
&mut self,
kind: ShadowKind,
backing: &ShadowBacking,
) -> Result<ShadowObject, FdError> {
let fd = if self.next_fd == 0 {
9000
} else {
self.next_fd
};
self.next_fd = fd + 1;
self.calls.push((kind, backing.clone()));
Ok(ShadowObject {
kind,
supervisor_fd: fd,
})
}
}
#[test]
fn raw_shadow_survives_chromium_style_dup_fork_close_dup2_exec() {
let mut fds = KboxlikeFdSystem::new();
let browser = fds.spawn_init();
for fd in (0..32).filter(|fd| *fd != 27 && *fd != 30) {
fds.insert_plain_fd(browser, fd, None, false).unwrap();
}
fds.insert_shadow_fd(browser, 27, None, shadow(23), false)
.unwrap();
fds.insert_shadow_fd(browser, 30, None, shadow(30), false)
.unwrap();
fds.insert_shadow_fd(browser, 33033, Some(272), shadow(372), false)
.unwrap();
let network_pipe = fds.dup(browser, 27).unwrap();
let v8_shared = fds.dup(browser, 30).unwrap();
assert_eq!(network_pipe, 32);
assert_eq!(v8_shared, 33);
let helper = fds.fork(browser).unwrap();
fds.close(browser, network_pipe).unwrap();
fds.close(browser, v8_shared).unwrap();
fds.dup_to(helper, network_pipe, 6, false).unwrap();
fds.dup_to(helper, v8_shared, 10, false).unwrap();
fds.dup_to(helper, 33033, 104, false).unwrap();
fds.exec(helper).unwrap();
assert_eq!(fds.descriptor(helper, 6).unwrap().shadow, Some(shadow(23)));
assert_eq!(fds.descriptor(helper, 10).unwrap().shadow, Some(shadow(30)));
assert_eq!(
fds.descriptor(helper, 104).unwrap().shadow,
Some(shadow(372))
);
assert!(fds.descriptor(browser, network_pipe).is_err());
assert!(fds.descriptor(browser, v8_shared).is_err());
}
#[test]
fn close_in_one_process_does_not_drop_inherited_open_file() {
let mut fds = KboxlikeFdSystem::new();
let parent = fds.spawn_init();
fds.insert_shadow_fd(parent, 20, None, shadow(15), false)
.unwrap();
let child = fds.fork(parent).unwrap();
let parent_info = fds.descriptor(parent, 20).unwrap();
let child_info = fds.descriptor(child, 20).unwrap();
assert_eq!(parent_info.open_file, child_info.open_file);
assert_eq!(parent_info.ref_count, 2);
fds.close(parent, 20).unwrap();
let child_after = fds.descriptor(child, 20).unwrap();
assert_eq!(child_after.shadow, Some(shadow(15)));
assert_eq!(child_after.ref_count, 1);
}
#[test]
fn exec_drops_only_cloexec_descriptors() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
fds.insert_plain_fd(pid, 3, Some(3), false).unwrap();
fds.insert_plain_fd(pid, 4, Some(4), true).unwrap();
fds.insert_shadow_fd(pid, 5, None, shadow(7), false)
.unwrap();
fds.set_cloexec(pid, 5, true).unwrap();
fds.exec(pid).unwrap();
assert!(fds.descriptor(pid, 3).is_ok());
assert_eq!(fds.descriptor(pid, 4), Err(FdError::BadFd(4)));
assert_eq!(fds.descriptor(pid, 5), Err(FdError::BadFd(5)));
}
#[test]
fn dup_to_replaces_target_without_dropping_source() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
fds.insert_shadow_fd(pid, 8, None, shadow(11), false)
.unwrap();
fds.insert_shadow_fd(pid, 9, None, shadow(12), false)
.unwrap();
fds.dup_to(pid, 8, 9, false).unwrap();
let source = fds.descriptor(pid, 8).unwrap();
let target = fds.descriptor(pid, 9).unwrap();
assert_eq!(source.open_file, target.open_file);
assert_eq!(target.shadow, Some(shadow(11)));
assert_eq!(target.ref_count, 2);
}
#[test]
fn alias_fd_from_preserves_open_file_identity_across_processes() {
let mut fds = KboxlikeFdSystem::new();
let browser = fds.spawn_init();
let helper = fds.fork(browser).unwrap();
fds.insert_shadow_fd_with_backing(
browser,
8,
None,
host_passthrough(88),
ShadowBacking::HostPassthrough {
label: "pipe:pipe123:0".to_string(),
},
false,
)
.unwrap();
fds.insert_plain_fd(helper, 9, Some(9), false).unwrap();
fds.alias_fd_from(browser, 8, helper, 9, true).unwrap();
let source = fds.descriptor(browser, 8).unwrap();
let target = fds.descriptor(helper, 9).unwrap();
assert_eq!(source.open_file, target.open_file);
assert_eq!(source.ref_count, 2);
assert_eq!(target.shadow, Some(host_passthrough(88)));
assert!(target.cloexec);
}
#[test]
fn proc_self_exe_is_inherited_across_fork_and_updated_on_exec() {
let mut fds = KboxlikeFdSystem::new();
let browser = fds.spawn_init_with_exe("/ms-playwright/chromium/chrome");
let helper = fds.fork(browser).unwrap();
assert_eq!(fds.parent(helper), Ok(Some(browser)));
assert_eq!(
fds.current_exe(helper).unwrap(),
Some("/ms-playwright/chromium/chrome")
);
assert_eq!(
fds.resolve_proc_exe(helper, "/proc/self/exe").unwrap(),
Some("/ms-playwright/chromium/chrome".to_string())
);
assert_eq!(
fds.resolve_proc_exe(helper, &format!("/proc/{browser}/exe"))
.unwrap(),
Some("/ms-playwright/chromium/chrome".to_string())
);
fds.exec_with_path(helper, "/usr/lib/chrome_crashpad_handler")
.unwrap();
assert_eq!(
fds.current_exe(helper).unwrap(),
Some("/usr/lib/chrome_crashpad_handler")
);
assert_eq!(
fds.current_exe(browser).unwrap(),
Some("/ms-playwright/chromium/chrome")
);
}
#[test]
fn proc_self_exe_exec_uses_model_identity_not_lkl_procfs() {
let mut fds = KboxlikeFdSystem::new();
let browser = fds.spawn_init_with_exe("/ms-playwright/chromium/chrome");
let gpu = fds.fork(browser).unwrap();
let target = fds
.resolve_proc_exe(gpu, "/proc/self/exe")
.unwrap()
.expect("proc self exe is known");
fds.exec_with_path(gpu, target).unwrap();
assert_eq!(
fds.current_exe(gpu).unwrap(),
Some("/ms-playwright/chromium/chrome")
);
}
#[test]
fn fcntl_dupfd_preserves_shadow_metadata_from_min_fd() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
fds.insert_plain_fd(pid, 100, Some(100), false).unwrap();
fds.insert_shadow_fd(pid, 12, None, shadow(44), false)
.unwrap();
let duped = fds.dup_from_min(pid, 12, 100, true).unwrap();
assert_eq!(duped, 101);
let source = fds.descriptor(pid, 12).unwrap();
let target = fds.descriptor(pid, duped).unwrap();
assert_eq!(source.open_file, target.open_file);
assert_eq!(target.shadow, Some(shadow(44)));
assert!(target.cloexec);
assert_eq!(target.ref_count, 2);
}
#[test]
fn kill_subtree_reaps_descendants_and_releases_open_files() {
let mut fds = KboxlikeFdSystem::new();
let browser = fds.spawn_init_with_exe("/chrome");
fds.insert_shadow_fd(browser, 20, None, shadow(20), false)
.unwrap();
let crashpad = fds.fork(browser).unwrap();
let monitor = fds.fork(crashpad).unwrap();
let sibling = fds.fork(browser).unwrap();
assert_eq!(fds.descriptor(browser, 20).unwrap().ref_count, 4);
let victims = fds.kill_subtree(crashpad).unwrap();
assert_eq!(victims, [crashpad, monitor]);
assert_eq!(fds.process_count(), 2);
assert_eq!(fds.descriptor(browser, 20).unwrap().ref_count, 2);
assert_eq!(fds.descriptor(sibling, 20).unwrap().ref_count, 2);
assert_eq!(
fds.current_exe(crashpad),
Err(FdError::BadProcess(crashpad))
);
assert_eq!(fds.current_exe(monitor), Err(FdError::BadProcess(monitor)));
}
#[test]
fn mmap_of_local_shadow_routes_to_host_for_dynamic_linker() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
let lib_shadow = local_shadow(55);
fds.insert_shadow_fd(pid, 4, Some(44), lib_shadow, false)
.unwrap();
assert_eq!(
fds.route_fd_operation(pid, 4, FdOperation::Mmap),
Ok(FdRoute::HostShadow { shadow: lib_shadow })
);
assert_eq!(
fds.route_fd_operation(pid, 4, FdOperation::Read),
Ok(FdRoute::Lkl { fd: 44 })
);
}
#[test]
fn vector_io_on_same_fd_shadow_routes_to_host_for_postgres() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
let config_shadow = shadow(70);
fds.insert_shadow_fd(pid, 8, Some(108), config_shadow, false)
.unwrap();
for op in [
FdOperation::Writev,
FdOperation::Pwritev,
FdOperation::Pwritev2,
FdOperation::Readv,
] {
assert_eq!(
fds.route_fd_operation(pid, 8, op),
Ok(FdRoute::HostShadow {
shadow: config_shadow
}),
"{op:?} must use the host shadow"
);
}
}
#[test]
fn fcntl_locks_on_same_fd_shadow_route_to_host_for_sqlite() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
let sqlite_shadow = shadow(90);
fds.insert_shadow_fd(pid, 11, Some(211), sqlite_shadow, false)
.unwrap();
assert_eq!(
fds.route_fd_operation(pid, 11, FdOperation::FcntlLock),
Ok(FdRoute::HostShadow {
shadow: sqlite_shadow
})
);
}
#[test]
fn plain_lkl_fd_routes_to_lkl_and_unbacked_fd_errors() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
fds.insert_plain_fd(pid, 3, Some(3), false).unwrap();
fds.insert_plain_fd(pid, 4, None, false).unwrap();
assert_eq!(
fds.route_fd_operation(pid, 3, FdOperation::Writev),
Ok(FdRoute::Lkl { fd: 3 })
);
assert_eq!(
fds.route_fd_operation(pid, 4, FdOperation::Generic),
Err(FdError::NoBackingFd(4))
);
}
#[test]
fn host_passthrough_fd_always_routes_to_host() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
let socket = host_passthrough(120);
fds.insert_shadow_fd(pid, 12, None, socket, false).unwrap();
for op in [
FdOperation::Read,
FdOperation::Write,
FdOperation::Mmap,
FdOperation::Generic,
] {
assert_eq!(
fds.route_fd_operation(pid, 12, op),
Ok(FdRoute::HostPassthrough { shadow: socket }),
"{op:?} must use host passthrough"
);
}
}
#[test]
fn snapshot_restore_preserves_chromium_process_fd_and_route_state() {
let mut fds = KboxlikeFdSystem::new();
let browser = fds.spawn_init_with_exe("/ms-playwright/chromium/chrome");
for fd in (0..32).filter(|fd| *fd != 27 && *fd != 30) {
fds.insert_plain_fd(browser, fd, Some(fd as i64), false)
.unwrap();
}
let network_shadow = shadow(21);
let v8_shadow = shadow(372);
fds.insert_shadow_fd(browser, 27, None, network_shadow, false)
.unwrap();
fds.insert_shadow_fd(browser, 33033, Some(272), v8_shadow, false)
.unwrap();
let network_pipe = fds.dup(browser, 27).unwrap();
let helper = fds.fork(browser).unwrap();
fds.close(browser, network_pipe).unwrap();
fds.dup_to(helper, network_pipe, 6, false).unwrap();
fds.dup_to(helper, 33033, 104, false).unwrap();
fds.exec_with_path(helper, "/ms-playwright/chromium/chrome")
.unwrap();
let encoded = serde_json::to_vec(&fds.snapshot()).unwrap();
let decoded: KboxlikeFdSnapshot = serde_json::from_slice(&encoded).unwrap();
let restored = KboxlikeFdSystem::restore(decoded).unwrap();
assert_eq!(
restored.current_exe(helper).unwrap(),
Some("/ms-playwright/chromium/chrome")
);
assert_eq!(restored.parent(helper), Ok(Some(browser)));
assert_eq!(
restored.resolve_proc_exe(helper, "/proc/self/exe").unwrap(),
Some("/ms-playwright/chromium/chrome".to_string())
);
assert_eq!(
restored.descriptor(helper, 6).unwrap().shadow,
Some(network_shadow)
);
assert_eq!(
restored.route_fd_operation(helper, 104, FdOperation::Mmap),
Ok(FdRoute::HostShadow { shadow: v8_shadow })
);
assert!(restored.descriptor(browser, network_pipe).is_err());
}
#[test]
fn guest_path_shadow_backing_survives_json_restore() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init_with_exe("/usr/local/bin/node");
let lib_shadow = local_shadow(55);
let backing = guest_path("/usr/lib/libstdc++.so.6");
fds.insert_shadow_fd_with_backing(pid, 4, Some(44), lib_shadow, backing.clone(), false)
.unwrap();
let encoded = serde_json::to_vec(&fds.snapshot()).unwrap();
let decoded: KboxlikeFdSnapshot = serde_json::from_slice(&encoded).unwrap();
let restored = KboxlikeFdSystem::restore(decoded).unwrap();
assert_eq!(restored.shadow_backing(pid, 4).unwrap(), Some(&backing));
assert_eq!(
restored.route_fd_operation(pid, 4, FdOperation::Mmap),
Ok(FdRoute::HostShadow { shadow: lib_shadow })
);
}
#[test]
fn restore_rebuilding_shadows_replaces_stale_guest_path_handles() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init_with_exe("/usr/local/bin/node");
let stale_shadow = local_shadow(55);
let backing = guest_path("/usr/lib/libstdc++.so.6");
fds.insert_shadow_fd_with_backing(pid, 4, Some(44), stale_shadow, backing.clone(), false)
.unwrap();
let encoded = serde_json::to_vec(&fds.snapshot()).unwrap();
let decoded: KboxlikeFdSnapshot = serde_json::from_slice(&encoded).unwrap();
let mut rebuilder = RecordingRebuilder::default();
let restored =
KboxlikeFdSystem::restore_rebuilding_shadows(decoded, &mut rebuilder).unwrap();
let rebuilt_shadow = local_shadow(9000);
assert_eq!(
rebuilder.calls,
vec![(ShadowKind::LocalOnly, backing.clone())]
);
assert_eq!(
restored.descriptor(pid, 4).unwrap().shadow,
Some(rebuilt_shadow)
);
assert_eq!(restored.shadow_backing(pid, 4).unwrap(), Some(&backing));
assert_eq!(
restored.route_fd_operation(pid, 4, FdOperation::Mmap),
Ok(FdRoute::HostShadow {
shadow: rebuilt_shadow
})
);
}
#[test]
fn restore_rebuilding_shadows_replaces_shared_chromium_shadow_once() {
let mut fds = KboxlikeFdSystem::new();
let browser = fds.spawn_init_with_exe("/ms-playwright/chromium/chrome");
let stale_shadow = shadow(372);
let backing =
guest_path("/ms-playwright/chromium-1217/chrome-linux64/v8_context_snapshot.bin");
fds.insert_shadow_fd_with_backing(
browser,
33033,
Some(272),
stale_shadow,
backing.clone(),
false,
)
.unwrap();
let helper = fds.fork(browser).unwrap();
fds.dup_to(helper, 33033, 104, false).unwrap();
fds.close(helper, 33033).unwrap();
let mut rebuilder = RecordingRebuilder::default();
let restored =
KboxlikeFdSystem::restore_rebuilding_shadows(fds.snapshot(), &mut rebuilder).unwrap();
let rebuilt_shadow = shadow(9000);
let browser_info = restored.descriptor(browser, 33033).unwrap();
let helper_info = restored.descriptor(helper, 104).unwrap();
assert_eq!(
rebuilder.calls,
vec![(ShadowKind::HostSameFd, backing.clone())]
);
assert_eq!(browser_info.open_file, helper_info.open_file);
assert_eq!(browser_info.ref_count, 2);
assert_eq!(browser_info.shadow, Some(rebuilt_shadow));
assert_eq!(helper_info.shadow, Some(rebuilt_shadow));
}
#[test]
fn restore_rebuilding_shadows_rebuilds_host_passthrough() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init_with_exe("/ms-playwright/chromium/chrome");
let stale_socket = host_passthrough(120);
let backing = ShadowBacking::HostPassthrough {
label: "crashpad-socketpair".to_string(),
};
fds.insert_shadow_fd_with_backing(pid, 12, None, stale_socket, backing.clone(), false)
.unwrap();
let mut rebuilder = RecordingRebuilder::default();
let restored =
KboxlikeFdSystem::restore_rebuilding_shadows(fds.snapshot(), &mut rebuilder).unwrap();
let rebuilt_socket = host_passthrough(9000);
assert_eq!(
rebuilder.calls,
vec![(ShadowKind::HostPassthrough, backing)]
);
assert_eq!(
restored.route_fd_operation(pid, 12, FdOperation::Read),
Ok(FdRoute::HostPassthrough {
shadow: rebuilt_socket
})
);
}
#[test]
fn restore_rebuilding_shadows_rejects_ephemeral_shadow() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
fds.insert_shadow_fd(pid, 3, Some(3), shadow(9), false)
.unwrap();
let mut rebuilder = RecordingRebuilder::default();
assert_eq!(
KboxlikeFdSystem::restore_rebuilding_shadows(fds.snapshot(), &mut rebuilder)
.unwrap_err(),
FdError::InvalidSnapshot("ephemeral shadow cannot be rebuilt after restore")
);
assert!(rebuilder.calls.is_empty());
}
#[test]
fn mmap_shadow_operation_plans_exact_fd_install() {
let mut fds = KboxlikeFdSystem::new();
let browser = fds.spawn_init_with_exe("/ms-playwright/chromium/chrome");
let v8_shadow = shadow(372);
fds.insert_shadow_fd_with_backing(
browser,
33033,
Some(272),
v8_shadow,
guest_path("/ms-playwright/chromium-1217/chrome-linux64/v8_context_snapshot.bin"),
false,
)
.unwrap();
assert_eq!(
fds.plan_operation_shadow_install(browser, 33033, FdOperation::Mmap),
Ok(Some(ShadowInstallPlan {
source_fd: 33033,
target_fd: 33033,
shadow: v8_shadow,
cloexec: false,
}))
);
}
#[test]
fn chromium_helper_remap_plans_install_from_preserved_shadow_metadata() {
let mut fds = KboxlikeFdSystem::new();
let browser = fds.spawn_init_with_exe("/ms-playwright/chromium/chrome");
let v8_shadow = shadow(372);
fds.insert_shadow_fd_with_backing(
browser,
33033,
Some(272),
v8_shadow,
guest_path("/ms-playwright/chromium-1217/chrome-linux64/v8_context_snapshot.bin"),
false,
)
.unwrap();
let helper = fds.fork(browser).unwrap();
fds.dup_to(helper, 33033, 104, false).unwrap();
assert_eq!(
fds.plan_shadow_install(helper, 104, 104, false),
Ok(Some(ShadowInstallPlan {
source_fd: 104,
target_fd: 104,
shadow: v8_shadow,
cloexec: false,
}))
);
}
#[test]
fn plain_lkl_operation_does_not_plan_shadow_install() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
fds.insert_plain_fd(pid, 3, Some(3), false).unwrap();
assert_eq!(
fds.plan_operation_shadow_install(pid, 3, FdOperation::Read),
Ok(None)
);
assert_eq!(fds.plan_shadow_install(pid, 3, 8, false), Ok(None));
}
#[test]
fn exact_shadow_install_rejects_negative_target_fd() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
fds.insert_shadow_fd(pid, 3, Some(3), shadow(9), false)
.unwrap();
assert_eq!(
fds.plan_shadow_install(pid, 3, -1, false),
Err(FdError::InvalidFd(-1))
);
}
#[test]
fn shadow_backing_follows_dup_fork_and_dup2_remap() {
let mut fds = KboxlikeFdSystem::new();
let browser = fds.spawn_init_with_exe("/ms-playwright/chromium/chrome");
let v8_shadow = shadow(372);
let backing =
guest_path("/ms-playwright/chromium-1217/chrome-linux64/v8_context_snapshot.bin");
fds.insert_shadow_fd_with_backing(
browser,
33033,
Some(272),
v8_shadow,
backing.clone(),
false,
)
.unwrap();
let duplicate = fds.dup(browser, 33033).unwrap();
let helper = fds.fork(browser).unwrap();
fds.close(browser, duplicate).unwrap();
fds.dup_to(helper, duplicate, 104, false).unwrap();
assert_eq!(fds.shadow_backing(helper, 104).unwrap(), Some(&backing));
assert_eq!(fds.descriptor(helper, 104).unwrap().shadow, Some(v8_shadow));
assert!(fds.shadow_backing(browser, duplicate).is_err());
}
#[test]
fn restore_rejects_shadow_without_backing() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
fds.insert_shadow_fd(pid, 3, Some(3), shadow(9), false)
.unwrap();
let mut snapshot = fds.snapshot();
let open_file = snapshot
.processes
.get(&pid)
.unwrap()
.fds
.get(&3)
.unwrap()
.open_file;
snapshot
.open_files
.get_mut(&open_file)
.unwrap()
.shadow_backing = None;
assert_eq!(
KboxlikeFdSystem::restore(snapshot).unwrap_err(),
FdError::InvalidSnapshot("shadow open-file is missing reconstruction backing")
);
}
#[test]
fn restore_rejects_backing_without_shadow() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
fds.insert_plain_fd(pid, 3, Some(3), false).unwrap();
let mut snapshot = fds.snapshot();
let open_file = snapshot
.processes
.get(&pid)
.unwrap()
.fds
.get(&3)
.unwrap()
.open_file;
snapshot
.open_files
.get_mut(&open_file)
.unwrap()
.shadow_backing = Some(guest_path("/not-a-shadow"));
assert_eq!(
KboxlikeFdSystem::restore(snapshot).unwrap_err(),
FdError::InvalidSnapshot("non-shadow open-file has shadow reconstruction backing")
);
}
#[test]
fn restore_rejects_missing_open_file_reference() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
fds.insert_plain_fd(pid, 3, Some(3), false).unwrap();
let mut snapshot = fds.snapshot();
let open_file = snapshot
.processes
.get(&pid)
.unwrap()
.fds
.get(&3)
.unwrap()
.open_file;
snapshot.open_files.remove(&open_file);
assert_eq!(
KboxlikeFdSystem::restore(snapshot).unwrap_err(),
FdError::InvalidSnapshot("descriptor references missing open-file description")
);
}
#[test]
fn restore_rejects_refcount_mismatch() {
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init();
fds.insert_plain_fd(pid, 3, Some(3), false).unwrap();
let mut snapshot = fds.snapshot();
let open_file = snapshot
.processes
.get(&pid)
.unwrap()
.fds
.get(&3)
.unwrap()
.open_file;
snapshot.open_files.get_mut(&open_file).unwrap().ref_count = 99;
assert_eq!(
KboxlikeFdSystem::restore(snapshot).unwrap_err(),
FdError::InvalidSnapshot("open-file ref_count does not match descriptor references")
);
}
}