use std::collections::BTreeMap;
use std::fs::{self, OpenOptions};
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::path::{Component, Path, PathBuf};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::fd::KboxlikeFdSystem;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::fd::ModelThreadId;
use super::fd::{
FdError, ShadowBacking, ShadowInstallPlan, ShadowKind, ShadowObject, ShadowRebuilder,
};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::snapshot::{
KboxlikeEpollShadowBackingPlan, KboxlikeFdQueuedDataPlan, KboxlikeFdScmRightsPlan,
KboxlikeFdSocketOptionsPlan, KboxlikeFdSocketShutdownPlan, KboxlikeUnixCredentials,
};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct KboxlikeFdKernelReplaySummary {
pub(crate) epoll_interests: usize,
pub(crate) socket_options: usize,
pub(crate) queued_bytes: usize,
pub(crate) socket_shutdowns: usize,
pub(crate) scm_rights_messages: usize,
}
#[derive(Debug)]
pub(crate) struct HostShadowRebuilder {
rootfs: PathBuf,
handles: BTreeMap<i32, OwnedFd>,
pending_pipe_endpoints: BTreeMap<String, OwnedFd>,
pending_socketpair_endpoints: BTreeMap<String, OwnedFd>,
source_ns_pid: Option<i32>,
}
impl HostShadowRebuilder {
pub(crate) fn new(rootfs: impl Into<PathBuf>) -> Self {
Self {
rootfs: rootfs.into(),
handles: BTreeMap::new(),
pending_pipe_endpoints: BTreeMap::new(),
pending_socketpair_endpoints: BTreeMap::new(),
source_ns_pid: None,
}
}
pub(crate) fn set_source_ns_pid(&mut self, pid: Option<i32>) {
self.source_ns_pid = pid;
}
pub(crate) fn handle_count(&self) -> usize {
self.handles.len()
}
pub(crate) fn dup_handle(&self, supervisor_fd: i32) -> Result<OwnedFd, FdError> {
if !self.handles.contains_key(&supervisor_fd) {
return Err(FdError::BadFd(supervisor_fd));
}
dup_fd(supervisor_fd)
}
pub(crate) fn install_shadow(&self, plan: ShadowInstallPlan) -> Result<OwnedFd, FdError> {
self.dup_handle_to(plan.shadow.supervisor_fd, plan.target_fd, plan.cloexec)
}
pub(crate) fn dup_handle_to(
&self,
supervisor_fd: i32,
target_fd: i32,
cloexec: bool,
) -> Result<OwnedFd, FdError> {
if target_fd < 0 {
return Err(FdError::InvalidFd(target_fd));
}
if !self.handles.contains_key(&supervisor_fd) {
return Err(FdError::BadFd(supervisor_fd));
}
if supervisor_fd == target_fd {
return Err(FdError::ShadowRebuildFailed(
"local install target aliases supervisor fd",
));
}
dup_fd_to(supervisor_fd, target_fd, cloexec)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn replay_epoll_interests(
&self,
fd_system: &KboxlikeFdSystem,
plans: &[KboxlikeEpollShadowBackingPlan],
) -> Result<usize, FdError> {
let mut replayed = 0;
for plan in plans {
let epoll_shadow = fd_system
.descriptor(plan.fd_plan.model_pid, plan.fd_plan.fd)?
.shadow
.ok_or(FdError::NoBackingFd(plan.fd_plan.fd))?;
self.require_live_handle(epoll_shadow.supervisor_fd)?;
for interest in &plan.interests {
let watched_shadow = fd_system
.descriptor(plan.fd_plan.model_pid, interest.target_fd)?
.shadow
.ok_or(FdError::NoBackingFd(interest.target_fd))?;
self.require_live_handle(watched_shadow.supervisor_fd)?;
let mut event = libc::epoll_event {
events: interest.events,
u64: interest.data,
};
let mut rc = unsafe {
libc::epoll_ctl(
epoll_shadow.supervisor_fd,
libc::EPOLL_CTL_ADD,
watched_shadow.supervisor_fd,
&mut event,
)
};
if rc < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EEXIST) {
rc = unsafe {
libc::epoll_ctl(
epoll_shadow.supervisor_fd,
libc::EPOLL_CTL_MOD,
watched_shadow.supervisor_fd,
&mut event,
)
};
}
if rc < 0 {
return Err(FdError::ShadowRebuildFailed(
"failed to replay epoll interest",
));
}
replayed += 1;
}
}
Ok(replayed)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn replay_fd_queued_data(
&self,
fd_system: &KboxlikeFdSystem,
plans: &[KboxlikeFdQueuedDataPlan],
) -> Result<usize, FdError> {
let mut replayed_bytes = 0;
for plan in plans {
let injector_shadow = fd_system
.descriptor(plan.model_pid, plan.injector_fd)?
.shadow
.ok_or(FdError::NoBackingFd(plan.injector_fd))?;
self.require_live_handle(injector_shadow.supervisor_fd)?;
write_all_fd(injector_shadow.supervisor_fd, &plan.bytes)?;
replayed_bytes += plan.bytes.len();
}
Ok(replayed_bytes)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn replay_fd_socket_options(
&self,
fd_system: &KboxlikeFdSystem,
plans: &[KboxlikeFdSocketOptionsPlan],
) -> Result<usize, FdError> {
let mut replayed = 0;
for plan in plans {
let shadow = fd_system
.descriptor(plan.model_pid, plan.fd)?
.shadow
.ok_or(FdError::NoBackingFd(plan.fd))?;
self.require_live_handle(shadow.supervisor_fd)?;
for option in &plan.int_options {
set_socket_int_option(
shadow.supervisor_fd,
option.level,
option.name,
option.value,
)?;
replayed += 1;
}
}
Ok(replayed)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn replay_fd_socket_shutdown(
&self,
fd_system: &KboxlikeFdSystem,
plans: &[KboxlikeFdSocketShutdownPlan],
) -> Result<usize, FdError> {
let mut replayed = 0;
for plan in plans {
let shadow = fd_system
.descriptor(plan.model_pid, plan.fd)?
.shadow
.ok_or(FdError::NoBackingFd(plan.fd))?;
self.require_live_handle(shadow.supervisor_fd)?;
let how = match (plan.shutdown.read_closed, plan.shutdown.write_closed) {
(true, true) => libc::SHUT_RDWR,
(true, false) => libc::SHUT_RD,
(false, true) => libc::SHUT_WR,
(false, false) => continue,
};
if unsafe { libc::shutdown(shadow.supervisor_fd, how) } < 0 {
return Err(FdError::ShadowRebuildFailed(
"failed to replay socket shutdown",
));
}
replayed += 1;
}
Ok(replayed)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn replay_fd_scm_rights(
&self,
fd_system: &KboxlikeFdSystem,
plans: &[KboxlikeFdScmRightsPlan],
) -> Result<usize, FdError> {
self.replay_fd_scm_rights_with_replacement_pids(fd_system, plans, &BTreeMap::new())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn replay_fd_scm_rights_with_replacement_pids(
&self,
fd_system: &KboxlikeFdSystem,
plans: &[KboxlikeFdScmRightsPlan],
replacement_host_pids: &BTreeMap<ModelThreadId, i32>,
) -> Result<usize, FdError> {
let mut replayed = 0;
for plan in plans {
let injector_shadow = fd_system
.descriptor(plan.model_pid, plan.injector_fd)?
.shadow
.ok_or(FdError::NoBackingFd(plan.injector_fd))?;
self.require_live_handle(injector_shadow.supervisor_fd)?;
let mut passed_handles = Vec::with_capacity(plan.passed_fds.len());
for passed_fd in &plan.passed_fds {
let passed_shadow = fd_system
.descriptor(plan.model_pid, *passed_fd)?
.shadow
.ok_or(FdError::NoBackingFd(*passed_fd))?;
self.require_live_handle(passed_shadow.supervisor_fd)?;
passed_handles.push(self.dup_handle(passed_shadow.supervisor_fd)?);
}
let credentials = remap_scm_credentials(plan, replacement_host_pids)?;
send_scm_rights_message(
injector_shadow.supervisor_fd,
&plan.data,
&passed_handles,
credentials.as_ref(),
)?;
replayed += 1;
}
Ok(replayed)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn replay_fd_kernel_state(
&self,
fd_system: &KboxlikeFdSystem,
epoll_plans: &[KboxlikeEpollShadowBackingPlan],
socket_options_plans: &[KboxlikeFdSocketOptionsPlan],
queued_data_plans: &[KboxlikeFdQueuedDataPlan],
socket_shutdown_plans: &[KboxlikeFdSocketShutdownPlan],
scm_rights_plans: &[KboxlikeFdScmRightsPlan],
) -> Result<KboxlikeFdKernelReplaySummary, FdError> {
self.replay_fd_kernel_state_with_replacement_pids(
fd_system,
epoll_plans,
socket_options_plans,
queued_data_plans,
socket_shutdown_plans,
scm_rights_plans,
&BTreeMap::new(),
)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn replay_fd_kernel_state_with_replacement_pids(
&self,
fd_system: &KboxlikeFdSystem,
epoll_plans: &[KboxlikeEpollShadowBackingPlan],
socket_options_plans: &[KboxlikeFdSocketOptionsPlan],
queued_data_plans: &[KboxlikeFdQueuedDataPlan],
socket_shutdown_plans: &[KboxlikeFdSocketShutdownPlan],
scm_rights_plans: &[KboxlikeFdScmRightsPlan],
replacement_host_pids: &BTreeMap<ModelThreadId, i32>,
) -> Result<KboxlikeFdKernelReplaySummary, FdError> {
let epoll_interests = self.replay_epoll_interests(fd_system, epoll_plans)?;
let socket_options = self.replay_fd_socket_options(fd_system, socket_options_plans)?;
let queued_bytes = self.replay_fd_queued_data(fd_system, queued_data_plans)?;
let scm_rights_messages = self.replay_fd_scm_rights_with_replacement_pids(
fd_system,
scm_rights_plans,
replacement_host_pids,
)?;
let socket_shutdowns = self.replay_fd_socket_shutdown(fd_system, socket_shutdown_plans)?;
Ok(KboxlikeFdKernelReplaySummary {
epoll_interests,
socket_options,
queued_bytes,
socket_shutdowns,
scm_rights_messages,
})
}
fn require_live_handle(&self, supervisor_fd: i32) -> Result<(), FdError> {
if self.handles.contains_key(&supervisor_fd) {
Ok(())
} else {
Err(FdError::BadFd(supervisor_fd))
}
}
fn rebuild_guest_path(
&mut self,
kind: ShadowKind,
path: &str,
writable: bool,
) -> Result<ShadowObject, FdError> {
let host_path = self.resolve_guest_path(path)?;
let file = OpenOptions::new()
.read(true)
.write(writable)
.open(host_path)
.map_err(|_| FdError::ShadowRebuildFailed("failed to open guest-path shadow"))?;
let fd: OwnedFd = file.into();
Ok(self.insert_handle(kind, fd))
}
fn rebuild_host_path(
&mut self,
kind: ShadowKind,
path: &str,
writable: bool,
) -> Result<ShadowObject, FdError> {
let open = |p: &Path| OpenOptions::new().read(true).write(writable).open(p);
let file = match open(Path::new(path)) {
Ok(file) => file,
Err(direct_err) => {
let ns_retry = (direct_err.kind() == std::io::ErrorKind::NotFound)
.then_some(self.source_ns_pid)
.flatten()
.map(|pid| {
open(
&Path::new(&format!("/proc/{pid}/root"))
.join(path.trim_start_matches('/')),
)
});
match ns_retry {
Some(Ok(file)) => file,
_ => {
if path.contains("/proc/") {
if let Ok(placeholder) = OpenOptions::new()
.read(true)
.write(writable)
.open("/dev/null")
{
let fd: OwnedFd = placeholder.into();
return Ok(self.insert_handle(kind, fd));
}
}
return Err(FdError::ShadowRebuildFailed(
"failed to open host-path shadow",
));
}
}
}
};
let fd: OwnedFd = file.into();
Ok(self.insert_handle(kind, fd))
}
fn rebuild_host_passthrough(
&mut self,
kind: ShadowKind,
label: &str,
) -> Result<ShadowObject, FdError> {
match HostPassthroughSpec::parse(label)? {
HostPassthroughSpec::EventFd { initval, flags } => {
let fd = create_eventfd(initval, flags)?;
Ok(self.insert_handle(kind, fd))
}
HostPassthroughSpec::SignalFd { nonblock } => {
let fd = create_signalfd(nonblock)?;
Ok(self.insert_handle(kind, fd))
}
HostPassthroughSpec::Inotify { nonblock } => {
let fd = create_inotify(nonblock)?;
Ok(self.insert_handle(kind, fd))
}
HostPassthroughSpec::Epoll { .. } => {
let fd = create_epoll()?;
Ok(self.insert_handle(kind, fd))
}
HostPassthroughSpec::TimerFd {
clockid,
flags,
value_sec,
value_nsec,
interval_sec,
interval_nsec,
} => {
let fd = create_timerfd(
clockid,
flags,
value_sec,
value_nsec,
interval_sec,
interval_nsec,
)?;
Ok(self.insert_handle(kind, fd))
}
HostPassthroughSpec::Pipe { group, endpoint } => {
let key = endpoint_key(&group, endpoint);
if let Some(fd) = self.pending_pipe_endpoints.remove(&key) {
return Ok(self.insert_handle(kind, fd));
}
let [read_end, write_end] = create_pipe()?;
let (selected, pending, pending_endpoint) = if endpoint == 0 {
(read_end, write_end, 1)
} else {
(write_end, read_end, 0)
};
self.pending_pipe_endpoints
.insert(endpoint_key(&group, pending_endpoint), pending);
Ok(self.insert_handle(kind, selected))
}
HostPassthroughSpec::SocketPair { group, endpoint } => {
let key = endpoint_key(&group, endpoint);
if let Some(fd) = self.pending_socketpair_endpoints.remove(&key) {
return Ok(self.insert_handle(kind, fd));
}
let [a, b] = create_socketpair()?;
let (selected, pending, pending_endpoint) =
if endpoint == 0 { (a, b, 1) } else { (b, a, 0) };
self.pending_socketpair_endpoints
.insert(endpoint_key(&group, pending_endpoint), pending);
Ok(self.insert_handle(kind, selected))
}
HostPassthroughSpec::LiveFd { pid, fd } => {
let fd = duplicate_live_fd(pid, fd)?;
Ok(self.insert_handle(kind, fd))
}
}
}
fn insert_handle(&mut self, kind: ShadowKind, fd: OwnedFd) -> ShadowObject {
let supervisor_fd = fd.as_raw_fd();
self.handles.insert(supervisor_fd, fd);
ShadowObject {
kind,
supervisor_fd,
}
}
fn resolve_guest_path(&self, path: &str) -> Result<PathBuf, FdError> {
let guest_path = Path::new(path);
if !guest_path.is_absolute() {
return Err(FdError::ShadowRebuildFailed(
"guest-path shadow backing must be absolute",
));
}
let mut joined = self.rootfs.clone();
for component in guest_path.components() {
match component {
Component::RootDir | Component::CurDir => {}
Component::Normal(part) => joined.push(part),
Component::ParentDir | Component::Prefix(_) => {
return Err(FdError::ShadowRebuildFailed(
"guest-path shadow escapes rootfs",
));
}
}
}
let root = fs::canonicalize(&self.rootfs)
.map_err(|_| FdError::ShadowRebuildFailed("failed to canonicalize rootfs"))?;
let resolved = fs::canonicalize(joined)
.map_err(|_| FdError::ShadowRebuildFailed("failed to resolve guest-path shadow"))?;
if !resolved.starts_with(root) {
return Err(FdError::ShadowRebuildFailed(
"guest-path shadow escapes rootfs",
));
}
Ok(resolved)
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn remap_scm_credentials(
plan: &KboxlikeFdScmRightsPlan,
replacement_host_pids: &BTreeMap<ModelThreadId, i32>,
) -> Result<Option<KboxlikeUnixCredentials>, FdError> {
let Some(mut credentials) = plan.credentials.clone() else {
return Ok(None);
};
let Some(identity) = plan.credential_identity else {
return Ok(Some(credentials));
};
if replacement_host_pids.is_empty() {
return Ok(Some(credentials));
}
let Some(replacement_host_pid) = replacement_host_pids
.get(&identity.model_thread_id)
.copied()
else {
return Err(FdError::ShadowRebuildFailed(
"missing replacement credential pid",
));
};
if replacement_host_pid <= 0 {
return Err(FdError::ShadowRebuildFailed(
"invalid replacement credential pid",
));
}
credentials.pid = replacement_host_pid;
Ok(Some(credentials))
}
impl ShadowRebuilder for HostShadowRebuilder {
fn rebuild_shadow(
&mut self,
kind: ShadowKind,
backing: &ShadowBacking,
) -> Result<ShadowObject, FdError> {
match backing {
ShadowBacking::Ephemeral => Err(FdError::InvalidSnapshot(
"ephemeral shadow cannot be rebuilt after restore",
)),
ShadowBacking::GuestPath { path, writable } => {
self.rebuild_guest_path(kind, path, *writable)
}
ShadowBacking::HostPath { path, writable } => {
self.rebuild_host_path(kind, path, *writable)
}
ShadowBacking::HostPassthrough { label } => self.rebuild_host_passthrough(kind, label),
}
}
}
#[derive(Debug, PartialEq, Eq)]
enum HostPassthroughSpec {
EventFd {
initval: u32,
flags: i32,
},
SignalFd {
nonblock: bool,
},
Inotify {
nonblock: bool,
},
Epoll {
group: String,
},
TimerFd {
clockid: i32,
flags: i32,
value_sec: i64,
value_nsec: i64,
interval_sec: i64,
interval_nsec: i64,
},
Pipe {
group: String,
endpoint: u8,
},
SocketPair {
group: String,
endpoint: u8,
},
LiveFd {
pid: i32,
fd: i32,
},
}
impl HostPassthroughSpec {
fn parse(label: &str) -> Result<Self, FdError> {
if let Some(rest) = label.strip_prefix("eventfd:") {
return parse_eventfd_spec(rest);
}
if let Some(rest) = label.strip_prefix("signalfd:") {
return Ok(HostPassthroughSpec::SignalFd {
nonblock: rest == "nonblock=1",
});
}
if let Some(rest) = label.strip_prefix("inotify:") {
return Ok(HostPassthroughSpec::Inotify {
nonblock: rest == "nonblock=1",
});
}
if let Some(group) = label.strip_prefix("epoll:") {
if group.is_empty() {
return Err(FdError::ShadowRebuildFailed("invalid epoll shadow label"));
}
return Ok(HostPassthroughSpec::Epoll {
group: group.to_string(),
});
}
if let Some(rest) = label.strip_prefix("timerfd:") {
return parse_timerfd_spec(rest);
}
if let Some(rest) = label.strip_prefix("pipe:") {
let (group, endpoint) = parse_group_endpoint_spec(rest, "pipe")?;
return Ok(HostPassthroughSpec::Pipe { group, endpoint });
}
if let Some(rest) = label.strip_prefix("socketpair:") {
let (group, endpoint) = parse_group_endpoint_spec(rest, "socketpair")?;
return Ok(HostPassthroughSpec::SocketPair { group, endpoint });
}
if let Some(rest) = label.strip_prefix("livefd:") {
let (pid, fd) = parse_live_fd_spec(rest)?;
return Ok(HostPassthroughSpec::LiveFd { pid, fd });
}
Err(FdError::ShadowRebuildUnsupported(
"unsupported host-passthrough shadow label",
))
}
}
fn parse_eventfd_spec(spec: &str) -> Result<HostPassthroughSpec, FdError> {
let mut initval = 0u32;
let mut flags = 0i32;
if !spec.is_empty() {
for field in spec.split(',') {
let (key, value) = field
.split_once('=')
.ok_or(FdError::ShadowRebuildFailed("invalid eventfd shadow label"))?;
match key {
"init" => {
initval = value
.parse()
.map_err(|_| FdError::ShadowRebuildFailed("invalid eventfd init value"))?;
}
"flags" => {
flags = value
.parse()
.map_err(|_| FdError::ShadowRebuildFailed("invalid eventfd flags"))?;
}
_ => {
return Err(FdError::ShadowRebuildFailed("invalid eventfd shadow label"));
}
}
}
}
Ok(HostPassthroughSpec::EventFd { initval, flags })
}
fn parse_timerfd_spec(spec: &str) -> Result<HostPassthroughSpec, FdError> {
let mut clockid = None;
let mut flags = None;
let mut value_sec = None;
let mut value_nsec = None;
let mut interval_sec = None;
let mut interval_nsec = None;
for field in spec.split(',') {
let (key, value) = field
.split_once('=')
.ok_or(FdError::ShadowRebuildFailed("invalid timerfd shadow label"))?;
match key {
"clockid" => {
clockid = Some(
value
.parse()
.map_err(|_| FdError::ShadowRebuildFailed("invalid timerfd clockid"))?,
);
}
"flags" => {
flags = Some(
value
.parse()
.map_err(|_| FdError::ShadowRebuildFailed("invalid timerfd flags"))?,
);
}
"value_sec" => {
value_sec = Some(
value
.parse()
.map_err(|_| FdError::ShadowRebuildFailed("invalid timerfd value"))?,
);
}
"value_nsec" => {
value_nsec = Some(
value
.parse()
.map_err(|_| FdError::ShadowRebuildFailed("invalid timerfd value"))?,
);
}
"interval_sec" => {
interval_sec = Some(
value
.parse()
.map_err(|_| FdError::ShadowRebuildFailed("invalid timerfd interval"))?,
);
}
"interval_nsec" => {
interval_nsec = Some(
value
.parse()
.map_err(|_| FdError::ShadowRebuildFailed("invalid timerfd interval"))?,
);
}
_ => {
return Err(FdError::ShadowRebuildFailed("invalid timerfd shadow label"));
}
}
}
Ok(HostPassthroughSpec::TimerFd {
clockid: clockid.ok_or(FdError::ShadowRebuildFailed("invalid timerfd shadow label"))?,
flags: flags.ok_or(FdError::ShadowRebuildFailed("invalid timerfd shadow label"))?,
value_sec: value_sec.ok_or(FdError::ShadowRebuildFailed("invalid timerfd shadow label"))?,
value_nsec: value_nsec
.ok_or(FdError::ShadowRebuildFailed("invalid timerfd shadow label"))?,
interval_sec: interval_sec
.ok_or(FdError::ShadowRebuildFailed("invalid timerfd shadow label"))?,
interval_nsec: interval_nsec
.ok_or(FdError::ShadowRebuildFailed("invalid timerfd shadow label"))?,
})
}
fn parse_group_endpoint_spec(spec: &str, kind: &str) -> Result<(String, u8), FdError> {
let invalid_label = match kind {
"pipe" => "invalid pipe shadow label",
"socketpair" => "invalid socketpair shadow label",
_ => "invalid host-passthrough shadow label",
};
let invalid_endpoint = match kind {
"pipe" => "invalid pipe endpoint",
"socketpair" => "invalid socketpair endpoint",
_ => "invalid host-passthrough endpoint",
};
let (group, endpoint) = spec
.split_once(':')
.ok_or(FdError::ShadowRebuildFailed(invalid_label))?;
if group.is_empty() {
return Err(FdError::ShadowRebuildFailed(invalid_label));
}
let endpoint = endpoint
.parse::<u8>()
.map_err(|_| FdError::ShadowRebuildFailed(invalid_endpoint))?;
if endpoint > 1 {
return Err(FdError::ShadowRebuildFailed(invalid_endpoint));
}
Ok((group.to_string(), endpoint))
}
fn parse_live_fd_spec(spec: &str) -> Result<(i32, i32), FdError> {
let (pid_raw, fd_raw) = spec
.split_once(':')
.ok_or(FdError::ShadowRebuildFailed("invalid livefd shadow label"))?;
let pid = pid_raw
.parse::<i32>()
.map_err(|_| FdError::ShadowRebuildFailed("invalid livefd pid"))?;
let fd = fd_raw
.parse::<i32>()
.map_err(|_| FdError::ShadowRebuildFailed("invalid livefd fd"))?;
if pid <= 0 || fd < 0 {
return Err(FdError::ShadowRebuildFailed("invalid livefd values"));
}
Ok((pid, fd))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn thread_group_leader_of(tid: i32) -> Option<i32> {
let status = std::fs::read_to_string(format!("/proc/{tid}/status")).ok()?;
status
.lines()
.find_map(|line| line.strip_prefix("Tgid:"))
.and_then(|value| value.trim().parse::<i32>().ok())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn duplicate_live_fd(pid: i32, fd: i32) -> Result<OwnedFd, FdError> {
let mut pidfd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) };
if pidfd < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EINVAL) {
if let Some(tgid) = thread_group_leader_of(pid) {
if tgid != pid {
pidfd = unsafe { libc::syscall(libc::SYS_pidfd_open, tgid, 0) };
}
}
}
if pidfd < 0 {
return Err(FdError::ShadowRebuildFailed("pidfd_open failed"));
}
let pidfd = unsafe { OwnedFd::from_raw_fd(pidfd as i32) };
let duplicated = unsafe {
libc::syscall(
libc::SYS_pidfd_getfd,
pidfd.as_raw_fd(),
fd,
0 as libc::c_uint,
)
};
if duplicated < 0 {
return Err(FdError::ShadowRebuildFailed("pidfd_getfd failed"));
}
Ok(unsafe { OwnedFd::from_raw_fd(duplicated as i32) })
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
fn duplicate_live_fd(_pid: i32, _fd: i32) -> Result<OwnedFd, FdError> {
Err(FdError::ShadowRebuildUnsupported(
"livefd host-passthrough requires linux",
))
}
fn endpoint_key(group: &str, endpoint: u8) -> String {
format!("{group}:{endpoint}")
}
fn write_all_fd(fd: i32, mut bytes: &[u8]) -> Result<(), FdError> {
while !bytes.is_empty() {
let written =
unsafe { libc::write(fd, bytes.as_ptr() as *const libc::c_void, bytes.len()) };
if written < 0 {
return Err(FdError::ShadowRebuildFailed(
"failed to replay queued fd bytes",
));
}
if written == 0 {
return Err(FdError::ShadowRebuildFailed(
"zero-length queued fd replay write",
));
}
bytes = &bytes[written as usize..];
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn send_scm_rights_message(
fd: i32,
data: &[u8],
passed: &[OwnedFd],
credentials: Option<&KboxlikeUnixCredentials>,
) -> Result<(), FdError> {
if data.is_empty() || passed.is_empty() {
return Err(FdError::ShadowRebuildFailed("invalid SCM_RIGHTS replay"));
}
let mut data = data.to_vec();
let mut iov = libc::iovec {
iov_base: data.as_mut_ptr() as *mut libc::c_void,
iov_len: data.len(),
};
let fd_bytes = passed
.len()
.checked_mul(std::mem::size_of::<libc::c_int>())
.ok_or(FdError::ShadowRebuildFailed("too many SCM_RIGHTS fds"))?;
let credentials_len = credentials
.map(|_| unsafe {
libc::CMSG_SPACE(std::mem::size_of::<libc::ucred>() as libc::c_uint) as usize
})
.unwrap_or(0);
let control_len = unsafe { libc::CMSG_SPACE(fd_bytes as libc::c_uint) as usize }
.checked_add(credentials_len)
.ok_or(FdError::ShadowRebuildFailed("too many control bytes"))?;
let mut control = vec![0u8; control_len];
let msg = libc::msghdr {
msg_name: std::ptr::null_mut(),
msg_namelen: 0,
msg_iov: &mut iov,
msg_iovlen: 1,
msg_control: control.as_mut_ptr() as *mut libc::c_void,
msg_controllen: control.len(),
msg_flags: 0,
};
let cmsg = unsafe { libc::CMSG_FIRSTHDR(&msg) };
if cmsg.is_null() {
return Err(FdError::ShadowRebuildFailed("missing SCM_RIGHTS header"));
}
unsafe {
(*cmsg).cmsg_level = libc::SOL_SOCKET;
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
(*cmsg).cmsg_len = libc::CMSG_LEN(fd_bytes as libc::c_uint) as _;
let data_ptr = libc::CMSG_DATA(cmsg) as *mut libc::c_int;
for (index, passed_fd) in passed.iter().enumerate() {
data_ptr.add(index).write_unaligned(passed_fd.as_raw_fd());
}
}
if let Some(credentials) = credentials {
let cmsg = unsafe { libc::CMSG_NXTHDR(&msg, cmsg) };
if cmsg.is_null() {
return Err(FdError::ShadowRebuildFailed(
"missing SCM_CREDENTIALS header",
));
}
unsafe {
(*cmsg).cmsg_level = libc::SOL_SOCKET;
(*cmsg).cmsg_type = libc::SCM_CREDENTIALS;
(*cmsg).cmsg_len =
libc::CMSG_LEN(std::mem::size_of::<libc::ucred>() as libc::c_uint) as _;
let creds = libc::ucred {
pid: credentials.pid,
uid: credentials.uid,
gid: credentials.gid,
};
(libc::CMSG_DATA(cmsg) as *mut libc::ucred).write_unaligned(creds);
}
}
let written = unsafe { libc::sendmsg(fd, &msg, libc::MSG_NOSIGNAL) };
if written != data.len() as isize {
return Err(FdError::ShadowRebuildFailed(
"failed to replay SCM_RIGHTS message",
));
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn set_socket_int_option(fd: i32, level: i32, name: i32, value: i32) -> Result<(), FdError> {
let value = value as libc::c_int;
let rc = unsafe {
libc::setsockopt(
fd,
level,
name,
&value as *const libc::c_int as *const libc::c_void,
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
)
};
if rc < 0 {
return Err(FdError::ShadowRebuildFailed(
"failed to replay socket option",
));
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn get_socket_int_option(fd: i32, level: i32, name: i32) -> Result<i32, FdError> {
let mut value = 0 as libc::c_int;
let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
let rc = unsafe {
libc::getsockopt(
fd,
level,
name,
&mut value as *mut libc::c_int as *mut libc::c_void,
&mut len,
)
};
if rc < 0 || len as usize != std::mem::size_of::<libc::c_int>() {
return Err(FdError::ShadowRebuildFailed("failed to read socket option"));
}
Ok(value)
}
fn dup_fd(fd: i32) -> Result<OwnedFd, FdError> {
let duplicated = unsafe { libc::dup(fd) };
if duplicated < 0 {
return Err(FdError::ShadowRebuildFailed(
"failed to duplicate shadow fd",
));
}
Ok(unsafe { OwnedFd::from_raw_fd(duplicated) })
}
fn dup_fd_to(source_fd: i32, target_fd: i32, cloexec: bool) -> Result<OwnedFd, FdError> {
let installed = unsafe { libc::dup2(source_fd, target_fd) };
if installed < 0 {
return Err(FdError::ShadowRebuildFailed("failed to install shadow fd"));
}
set_cloexec_state(installed, cloexec)?;
Ok(unsafe { OwnedFd::from_raw_fd(installed) })
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn create_eventfd(initval: u32, flags: i32) -> Result<OwnedFd, FdError> {
let fd = unsafe { libc::eventfd(initval, flags) };
if fd < 0 {
return Err(FdError::ShadowRebuildFailed("failed to create eventfd"));
}
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
fn create_eventfd(_initval: u32, _flags: i32) -> Result<OwnedFd, FdError> {
Err(FdError::ShadowRebuildUnsupported(
"eventfd host-passthrough requires linux",
))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn create_signalfd(nonblock: bool) -> Result<OwnedFd, FdError> {
let mut mask: libc::sigset_t = unsafe { std::mem::zeroed() };
unsafe { libc::sigemptyset(&mut mask) };
let mut flags = libc::SFD_CLOEXEC;
if nonblock {
flags |= libc::SFD_NONBLOCK;
}
let fd = unsafe { libc::signalfd(-1, &mask, flags) };
if fd < 0 {
return Err(FdError::ShadowRebuildFailed("failed to create signalfd"));
}
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
fn create_signalfd(_nonblock: bool) -> Result<OwnedFd, FdError> {
Err(FdError::ShadowRebuildUnsupported(
"signalfd host-passthrough requires linux",
))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn create_inotify(nonblock: bool) -> Result<OwnedFd, FdError> {
let mut flags = libc::IN_CLOEXEC;
if nonblock {
flags |= libc::IN_NONBLOCK;
}
let fd = unsafe { libc::inotify_init1(flags) };
if fd < 0 {
return Err(FdError::ShadowRebuildFailed("failed to create inotify"));
}
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
fn create_inotify(_nonblock: bool) -> Result<OwnedFd, FdError> {
Err(FdError::ShadowRebuildUnsupported(
"inotify host-passthrough requires linux",
))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn create_epoll() -> Result<OwnedFd, FdError> {
let fd = unsafe { libc::epoll_create1(libc::EPOLL_CLOEXEC) };
if fd < 0 {
return Err(FdError::ShadowRebuildFailed("failed to create epoll fd"));
}
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
fn create_epoll() -> Result<OwnedFd, FdError> {
Err(FdError::ShadowRebuildUnsupported(
"epoll host-passthrough requires linux",
))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn create_timerfd(
clockid: i32,
flags: i32,
value_sec: i64,
value_nsec: i64,
interval_sec: i64,
interval_nsec: i64,
) -> Result<OwnedFd, FdError> {
if !(0..1_000_000_000).contains(&value_nsec) || !(0..1_000_000_000).contains(&interval_nsec) {
return Err(FdError::ShadowRebuildFailed("invalid timerfd timespec"));
}
let fd = unsafe { libc::timerfd_create(clockid, flags) };
if fd < 0 {
return Err(FdError::ShadowRebuildFailed("failed to create timerfd"));
}
let owned = unsafe { OwnedFd::from_raw_fd(fd) };
let spec = libc::itimerspec {
it_interval: libc::timespec {
tv_sec: interval_sec as libc::time_t,
tv_nsec: interval_nsec as libc::c_long,
},
it_value: libc::timespec {
tv_sec: value_sec as libc::time_t,
tv_nsec: value_nsec as libc::c_long,
},
};
let rc = unsafe { libc::timerfd_settime(owned.as_raw_fd(), 0, &spec, std::ptr::null_mut()) };
if rc < 0 {
return Err(FdError::ShadowRebuildFailed("failed to arm timerfd"));
}
Ok(owned)
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
fn create_timerfd(
_clockid: i32,
_flags: i32,
_value_sec: i64,
_value_nsec: i64,
_interval_sec: i64,
_interval_nsec: i64,
) -> Result<OwnedFd, FdError> {
Err(FdError::ShadowRebuildUnsupported(
"timerfd host-passthrough requires linux",
))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn create_pipe() -> Result<[OwnedFd; 2], FdError> {
let mut fds = [-1; 2];
let rc = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) };
if rc < 0 {
return Err(FdError::ShadowRebuildFailed("failed to create pipe"));
}
Ok(unsafe { [OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])] })
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
fn create_pipe() -> Result<[OwnedFd; 2], FdError> {
let mut fds = [-1; 2];
let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
if rc < 0 {
return Err(FdError::ShadowRebuildFailed("failed to create pipe"));
}
set_cloexec_state(fds[0], true)?;
set_cloexec_state(fds[1], true)?;
Ok(unsafe { [OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])] })
}
fn create_socketpair() -> Result<[OwnedFd; 2], FdError> {
let mut fds = [-1; 2];
let rc = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) };
if rc < 0 {
return Err(FdError::ShadowRebuildFailed("failed to create socketpair"));
}
set_cloexec_state(fds[0], true)?;
set_cloexec_state(fds[1], true)?;
Ok(unsafe { [OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])] })
}
fn set_cloexec_state(fd: i32, cloexec: bool) -> Result<(), FdError> {
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
if flags < 0 {
return Err(FdError::ShadowRebuildFailed("failed to read fd flags"));
}
let new_flags = if cloexec {
flags | libc::FD_CLOEXEC
} else {
flags & !libc::FD_CLOEXEC
};
let rc = unsafe { libc::fcntl(fd, libc::F_SETFD, new_flags) };
if rc < 0 {
return Err(FdError::ShadowRebuildFailed("failed to set fd cloexec"));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use crate::kboxlike::dispatch::KboxlikeTraceeIdentity;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use crate::kboxlike::fd::KboxlikeSocketShutdownState;
use crate::kboxlike::fd::{KboxlikeFdSystem, ShadowInstallPlan};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use crate::kboxlike::snapshot::{
KboxlikeEpollInterestBackingPlan, KboxlikeEpollShadowBackingPlan, KboxlikeFdQueuedDataPlan,
KboxlikeFdScmRightsPlan, KboxlikeFdShadowBackingPlan, KboxlikeFdSocketOptionsPlan,
KboxlikeFdSocketShutdownPlan, KboxlikeSocketIntOption, KboxlikeUnixCredentials,
};
use std::fs::File;
use std::io::{Read, Write};
use std::os::fd::{AsRawFd, IntoRawFd};
fn test_rootfs(name: &str) -> PathBuf {
let mut path = std::env::temp_dir();
path.push(format!(
"supermachine-kboxlike-shadow-{name}-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&path);
fs::create_dir_all(&path).unwrap();
path
}
fn read_owned_fd(fd: OwnedFd) -> String {
let mut file = File::from(fd);
let mut out = String::new();
file.read_to_string(&mut out).unwrap();
out
}
fn placeholder_fd(rootfs: &Path, name: &str) -> i32 {
let path = rootfs.join(name);
fs::write(&path, b"placeholder").unwrap();
File::open(path).unwrap().into_raw_fd()
}
fn fd_cloexec(fd: i32) -> bool {
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
assert!(flags >= 0);
flags & libc::FD_CLOEXEC != 0
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn recv_one_scm_right(
fd: i32,
len: usize,
) -> (Vec<u8>, OwnedFd, Option<KboxlikeUnixCredentials>) {
let mut data = vec![0u8; len];
let mut iov = libc::iovec {
iov_base: data.as_mut_ptr() as *mut libc::c_void,
iov_len: data.len(),
};
let mut control = vec![
0u8;
unsafe {
libc::CMSG_SPACE(std::mem::size_of::<libc::c_int>() as u32)
+ libc::CMSG_SPACE(std::mem::size_of::<libc::ucred>() as u32)
} as usize
];
let mut msg = libc::msghdr {
msg_name: std::ptr::null_mut(),
msg_namelen: 0,
msg_iov: &mut iov,
msg_iovlen: 1,
msg_control: control.as_mut_ptr() as *mut libc::c_void,
msg_controllen: control.len(),
msg_flags: 0,
};
let read = unsafe { libc::recvmsg(fd, &mut msg, 0) };
assert_eq!(read, len as isize);
data.truncate(read as usize);
let mut passed = None;
let mut credentials = None;
let mut cmsg = unsafe { libc::CMSG_FIRSTHDR(&msg) };
while !cmsg.is_null() {
let header = unsafe { &*cmsg };
if header.cmsg_level == libc::SOL_SOCKET && header.cmsg_type == libc::SCM_RIGHTS {
passed =
Some(unsafe { (libc::CMSG_DATA(cmsg) as *const libc::c_int).read_unaligned() });
} else if header.cmsg_level == libc::SOL_SOCKET
&& header.cmsg_type == libc::SCM_CREDENTIALS
{
let creds =
unsafe { (libc::CMSG_DATA(cmsg) as *const libc::ucred).read_unaligned() };
credentials = Some(KboxlikeUnixCredentials {
pid: creds.pid,
uid: creds.uid,
gid: creds.gid,
});
}
cmsg = unsafe { libc::CMSG_NXTHDR(&msg, cmsg) };
}
let passed = passed.expect("missing SCM_RIGHTS cmsg");
assert!(passed >= 0);
(data, unsafe { OwnedFd::from_raw_fd(passed) }, credentials)
}
#[test]
fn guest_path_rebuilder_opens_confined_rootfs_file() {
let rootfs = test_rootfs("guest-path");
let lib = rootfs.join("usr/lib");
fs::create_dir_all(&lib).unwrap();
fs::write(lib.join("libstdc++.so.6"), b"shadow-bytes").unwrap();
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let shadow = rebuilder
.rebuild_shadow(
ShadowKind::LocalOnly,
&ShadowBacking::GuestPath {
path: "/usr/lib/libstdc++.so.6".to_string(),
writable: false,
},
)
.unwrap();
assert_eq!(shadow.kind, ShadowKind::LocalOnly);
assert_eq!(rebuilder.handle_count(), 1);
let dup = rebuilder.dup_handle(shadow.supervisor_fd).unwrap();
assert_eq!(read_owned_fd(dup), "shadow-bytes");
fs::remove_dir_all(rootfs).unwrap();
}
#[test]
fn install_shadow_replaces_target_fd_with_rebuilt_handle() {
let rootfs = test_rootfs("install-shadow");
let lib = rootfs.join("usr/lib");
fs::create_dir_all(&lib).unwrap();
fs::write(lib.join("libstdc++.so.6"), b"installed-shadow").unwrap();
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let shadow = rebuilder
.rebuild_shadow(
ShadowKind::LocalOnly,
&ShadowBacking::GuestPath {
path: "/usr/lib/libstdc++.so.6".to_string(),
writable: false,
},
)
.unwrap();
let target_fd = placeholder_fd(&rootfs, "placeholder");
let installed = rebuilder
.install_shadow(ShadowInstallPlan {
source_fd: 4,
target_fd,
shadow,
cloexec: false,
})
.unwrap();
assert_eq!(installed.as_raw_fd(), target_fd);
assert!(!fd_cloexec(target_fd));
assert_eq!(read_owned_fd(installed), "installed-shadow");
fs::remove_dir_all(rootfs).unwrap();
}
#[test]
fn install_shadow_honors_cloexec() {
let rootfs = test_rootfs("install-cloexec");
let lib = rootfs.join("usr/lib");
fs::create_dir_all(&lib).unwrap();
fs::write(lib.join("libv8.so"), b"cloexec-shadow").unwrap();
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let shadow = rebuilder
.rebuild_shadow(
ShadowKind::LocalOnly,
&ShadowBacking::GuestPath {
path: "/usr/lib/libv8.so".to_string(),
writable: false,
},
)
.unwrap();
let target_fd = placeholder_fd(&rootfs, "placeholder");
let installed = rebuilder
.install_shadow(ShadowInstallPlan {
source_fd: 33033,
target_fd,
shadow,
cloexec: true,
})
.unwrap();
assert_eq!(installed.as_raw_fd(), target_fd);
assert!(fd_cloexec(target_fd));
drop(installed);
fs::remove_dir_all(rootfs).unwrap();
}
#[test]
fn install_shadow_rejects_unknown_supervisor_handle() {
let rootfs = test_rootfs("install-missing");
let rebuilder = HostShadowRebuilder::new(&rootfs);
let err = rebuilder
.install_shadow(ShadowInstallPlan {
source_fd: 4,
target_fd: 99_999,
shadow: ShadowObject {
kind: ShadowKind::LocalOnly,
supervisor_fd: 88_888,
},
cloexec: false,
})
.unwrap_err();
assert_eq!(err, FdError::BadFd(88_888));
fs::remove_dir_all(rootfs).unwrap();
}
#[test]
fn local_install_rejects_target_that_aliases_supervisor_handle() {
let rootfs = test_rootfs("install-alias");
let lib = rootfs.join("usr/lib");
fs::create_dir_all(&lib).unwrap();
fs::write(lib.join("libalias.so"), b"alias-shadow").unwrap();
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let shadow = rebuilder
.rebuild_shadow(
ShadowKind::LocalOnly,
&ShadowBacking::GuestPath {
path: "/usr/lib/libalias.so".to_string(),
writable: false,
},
)
.unwrap();
let err = rebuilder
.dup_handle_to(shadow.supervisor_fd, shadow.supervisor_fd, false)
.unwrap_err();
assert_eq!(
err,
FdError::ShadowRebuildFailed("local install target aliases supervisor fd")
);
fs::remove_dir_all(rootfs).unwrap();
}
#[test]
fn fd_snapshot_restore_uses_host_shadow_rebuilder() {
let rootfs = test_rootfs("snapshot-restore");
let lib = rootfs.join("usr/lib");
fs::create_dir_all(&lib).unwrap();
fs::write(lib.join("libv8_snapshot.so"), b"rebuilt-shadow").unwrap();
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init_with_exe("/usr/local/bin/node");
fds.insert_shadow_fd_with_backing(
pid,
4,
Some(44),
ShadowObject {
kind: ShadowKind::LocalOnly,
supervisor_fd: 123,
},
ShadowBacking::GuestPath {
path: "/usr/lib/libv8_snapshot.so".to_string(),
writable: false,
},
false,
)
.unwrap();
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let restored =
KboxlikeFdSystem::restore_rebuilding_shadows(fds.snapshot(), &mut rebuilder).unwrap();
let rebuilt = restored.descriptor(pid, 4).unwrap().shadow.unwrap();
assert_eq!(rebuilt.kind, ShadowKind::LocalOnly);
assert_ne!(rebuilt.supervisor_fd, 123);
let dup = rebuilder.dup_handle(rebuilt.supervisor_fd).unwrap();
assert_eq!(read_owned_fd(dup), "rebuilt-shadow");
fs::remove_dir_all(rootfs).unwrap();
}
#[test]
fn guest_path_rebuilder_rejects_parent_escape() {
let rootfs = test_rootfs("parent-escape");
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
assert_eq!(
rebuilder.rebuild_shadow(
ShadowKind::LocalOnly,
&ShadowBacking::GuestPath {
path: "/../etc/passwd".to_string(),
writable: false,
},
),
Err(FdError::ShadowRebuildFailed(
"guest-path shadow escapes rootfs"
))
);
fs::remove_dir_all(rootfs).unwrap();
}
#[cfg(unix)]
#[test]
fn guest_path_rebuilder_rejects_symlink_escape() {
let rootfs = test_rootfs("symlink-escape");
let outside = test_rootfs("symlink-outside");
fs::write(outside.join("secret"), b"host-secret").unwrap();
std::os::unix::fs::symlink(outside.join("secret"), rootfs.join("secret-link")).unwrap();
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
assert_eq!(
rebuilder.rebuild_shadow(
ShadowKind::LocalOnly,
&ShadowBacking::GuestPath {
path: "/secret-link".to_string(),
writable: false,
},
),
Err(FdError::ShadowRebuildFailed(
"guest-path shadow escapes rootfs"
))
);
fs::remove_dir_all(rootfs).unwrap();
fs::remove_dir_all(outside).unwrap();
}
#[test]
fn host_passthrough_rebuilder_recreates_socketpair_endpoints() {
let rootfs = test_rootfs("socketpair");
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let a = rebuilder
.rebuild_shadow(
ShadowKind::HostPassthrough,
&ShadowBacking::HostPassthrough {
label: "socketpair:crashpad:0".to_string(),
},
)
.unwrap();
let b = rebuilder
.rebuild_shadow(
ShadowKind::HostPassthrough,
&ShadowBacking::HostPassthrough {
label: "socketpair:crashpad:1".to_string(),
},
)
.unwrap();
let mut a_file = File::from(rebuilder.dup_handle(a.supervisor_fd).unwrap());
let mut b_file = File::from(rebuilder.dup_handle(b.supervisor_fd).unwrap());
a_file.write_all(b"ping").unwrap();
let mut buf = [0u8; 4];
b_file.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"ping");
fs::remove_dir_all(rootfs).unwrap();
}
#[test]
fn host_passthrough_rebuilder_recreates_pipe_endpoints() {
let rootfs = test_rootfs("pipe");
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let read_end = rebuilder
.rebuild_shadow(
ShadowKind::HostPassthrough,
&ShadowBacking::HostPassthrough {
label: "pipe:network:0".to_string(),
},
)
.unwrap();
let write_end = rebuilder
.rebuild_shadow(
ShadowKind::HostPassthrough,
&ShadowBacking::HostPassthrough {
label: "pipe:network:1".to_string(),
},
)
.unwrap();
let mut reader = File::from(rebuilder.dup_handle(read_end.supervisor_fd).unwrap());
let mut writer = File::from(rebuilder.dup_handle(write_end.supervisor_fd).unwrap());
writer.write_all(b"pipe").unwrap();
let mut buf = [0u8; 4];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"pipe");
fs::remove_dir_all(rootfs).unwrap();
}
#[test]
fn parses_host_passthrough_labels() {
assert_eq!(
HostPassthroughSpec::parse("eventfd:init=7,flags=0").unwrap(),
HostPassthroughSpec::EventFd {
initval: 7,
flags: 0
}
);
assert_eq!(
HostPassthroughSpec::parse("epoll:browser").unwrap(),
HostPassthroughSpec::Epoll {
group: "browser".to_string(),
}
);
assert_eq!(
HostPassthroughSpec::parse(
"timerfd:clockid=1,flags=2048,value_sec=0,value_nsec=1000000,interval_sec=2,interval_nsec=3"
)
.unwrap(),
HostPassthroughSpec::TimerFd {
clockid: 1,
flags: 2048,
value_sec: 0,
value_nsec: 1_000_000,
interval_sec: 2,
interval_nsec: 3,
}
);
assert_eq!(
HostPassthroughSpec::parse("pipe:network:0").unwrap(),
HostPassthroughSpec::Pipe {
group: "network".to_string(),
endpoint: 0
}
);
assert_eq!(
HostPassthroughSpec::parse("socketpair:crashpad:1").unwrap(),
HostPassthroughSpec::SocketPair {
group: "crashpad".to_string(),
endpoint: 1
}
);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn host_passthrough_rebuilder_recreates_eventfd() {
let rootfs = test_rootfs("eventfd");
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let shadow = rebuilder
.rebuild_shadow(
ShadowKind::HostPassthrough,
&ShadowBacking::HostPassthrough {
label: "eventfd:init=5,flags=0".to_string(),
},
)
.unwrap();
let raw = rebuilder.dup_handle(shadow.supervisor_fd).unwrap();
let mut value = 0u64;
let rc = unsafe {
libc::read(
raw.as_raw_fd(),
&mut value as *mut u64 as *mut libc::c_void,
std::mem::size_of::<u64>(),
)
};
assert_eq!(rc, std::mem::size_of::<u64>() as isize);
assert_eq!(value, 5);
fs::remove_dir_all(rootfs).unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn host_passthrough_rebuilder_recreates_epoll_fd() {
let rootfs = test_rootfs("epoll");
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let shadow = rebuilder
.rebuild_shadow(
ShadowKind::HostPassthrough,
&ShadowBacking::HostPassthrough {
label: "epoll:browser".to_string(),
},
)
.unwrap();
let raw = rebuilder.dup_handle(shadow.supervisor_fd).unwrap();
let fd_path = fs::read_link(format!("/proc/self/fd/{}", raw.as_raw_fd())).unwrap();
assert_eq!(fd_path.to_string_lossy(), "anon_inode:[eventpoll]");
fs::remove_dir_all(rootfs).unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn host_passthrough_rebuilder_recreates_armed_timerfd() {
let rootfs = test_rootfs("timerfd");
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let shadow = rebuilder
.rebuild_shadow(
ShadowKind::HostPassthrough,
&ShadowBacking::HostPassthrough {
label:
"timerfd:clockid=1,flags=2048,value_sec=0,value_nsec=1000000,interval_sec=0,interval_nsec=0"
.to_string(),
},
)
.unwrap();
let raw = rebuilder.dup_handle(shadow.supervisor_fd).unwrap();
let mut pollfd = libc::pollfd {
fd: raw.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let ready = unsafe { libc::poll(&mut pollfd, 1, 500) };
assert_eq!(ready, 1);
let mut expirations = 0u64;
let rc = unsafe {
libc::read(
raw.as_raw_fd(),
&mut expirations as *mut u64 as *mut libc::c_void,
std::mem::size_of::<u64>(),
)
};
assert_eq!(rc, std::mem::size_of::<u64>() as isize);
assert!(expirations >= 1);
fs::remove_dir_all(rootfs).unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn replay_epoll_interests_restores_eventfd_readiness() {
let rootfs = test_rootfs("epoll-replay");
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init_with_exe("/chrome");
let eventfd_backing = ShadowBacking::HostPassthrough {
label: "eventfd:init=0,flags=0".to_string(),
};
let epoll_backing = ShadowBacking::HostPassthrough {
label: "epoll:browser".to_string(),
};
fds.insert_shadow_fd_with_backing(
pid,
8,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: 8,
},
eventfd_backing.clone(),
false,
)
.unwrap();
fds.insert_shadow_fd_with_backing(
pid,
9,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: 9,
},
epoll_backing.clone(),
false,
)
.unwrap();
let epoll_plan = KboxlikeEpollShadowBackingPlan {
fd_plan: KboxlikeFdShadowBackingPlan {
host_pid: 2001,
model_pid: pid,
model_thread_id: 1,
fd: 9,
target: "anon_inode:[eventpoll]".to_string(),
cloexec: false,
backing: epoll_backing,
},
interests: vec![KboxlikeEpollInterestBackingPlan {
target_fd: 8,
events: libc::EPOLLIN as u32,
data: 0xfeed_cafe,
target: Some("anon_inode:[eventfd]".to_string()),
target_kind: Some(crate::kboxlike::snapshot::KboxlikeFdTargetKind::EventFd),
target_backing: Some(eventfd_backing),
}],
};
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let rebuilt =
KboxlikeFdSystem::restore_rebuilding_shadows(fds.snapshot(), &mut rebuilder).unwrap();
assert_eq!(
rebuilder
.replay_epoll_interests(&rebuilt, &[epoll_plan])
.unwrap(),
1
);
let event_shadow = rebuilt.descriptor(pid, 8).unwrap().shadow.unwrap();
let epoll_shadow = rebuilt.descriptor(pid, 9).unwrap().shadow.unwrap();
let event_handle = rebuilder.dup_handle(event_shadow.supervisor_fd).unwrap();
let value = 1u64;
let rc = unsafe {
libc::write(
event_handle.as_raw_fd(),
&value as *const u64 as *const libc::c_void,
std::mem::size_of::<u64>(),
)
};
assert_eq!(rc, std::mem::size_of::<u64>() as isize);
let epoll_handle = rebuilder.dup_handle(epoll_shadow.supervisor_fd).unwrap();
let mut events = [libc::epoll_event { events: 0, u64: 0 }; 1];
let ready =
unsafe { libc::epoll_wait(epoll_handle.as_raw_fd(), events.as_mut_ptr(), 1, 0) };
assert_eq!(ready, 1);
let event_data = unsafe { std::ptr::addr_of!(events[0].u64).read_unaligned() };
let event_flags = unsafe { std::ptr::addr_of!(events[0].events).read_unaligned() };
assert_eq!(event_data, 0xfeed_cafe);
assert_ne!(event_flags & libc::EPOLLIN as u32, 0);
fs::remove_dir_all(rootfs).unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn replay_fd_kernel_state_applies_ordered_restore_sequence() {
let rootfs = test_rootfs("kernel-replay-sequence");
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init_with_exe("/chrome");
let queue_a_backing = ShadowBacking::HostPassthrough {
label: "socketpair:queue:0".to_string(),
};
let queue_b_backing = ShadowBacking::HostPassthrough {
label: "socketpair:queue:1".to_string(),
};
let scm_a_backing = ShadowBacking::HostPassthrough {
label: "socketpair:broker:0".to_string(),
};
let scm_b_backing = ShadowBacking::HostPassthrough {
label: "socketpair:broker:1".to_string(),
};
let eventfd_backing = ShadowBacking::HostPassthrough {
label: "eventfd:init=7,flags=0".to_string(),
};
let epoll_backing = ShadowBacking::HostPassthrough {
label: "epoll:browser".to_string(),
};
for (fd, backing) in [
(6, queue_a_backing.clone()),
(7, queue_b_backing.clone()),
(8, eventfd_backing.clone()),
(9, epoll_backing.clone()),
(10, scm_a_backing.clone()),
(11, scm_b_backing.clone()),
] {
fds.insert_shadow_fd_with_backing(
pid,
fd,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: fd,
},
backing,
false,
)
.unwrap();
}
let epoll_plan = KboxlikeEpollShadowBackingPlan {
fd_plan: KboxlikeFdShadowBackingPlan {
host_pid: 2001,
model_pid: pid,
model_thread_id: 1,
fd: 9,
target: "anon_inode:[eventpoll]".to_string(),
cloexec: false,
backing: epoll_backing,
},
interests: vec![KboxlikeEpollInterestBackingPlan {
target_fd: 8,
events: libc::EPOLLIN as u32,
data: 0xfeed_cafe,
target: Some("anon_inode:[eventfd]".to_string()),
target_kind: Some(crate::kboxlike::snapshot::KboxlikeFdTargetKind::EventFd),
target_backing: Some(eventfd_backing.clone()),
}],
};
let queued_plan = KboxlikeFdQueuedDataPlan {
host_pid: 2001,
model_pid: pid,
model_thread_id: 1,
receiver_fd: 7,
injector_fd: 6,
target: "socket:[222]".to_string(),
receiver_backing: queue_b_backing,
injector_backing: queue_a_backing,
bytes: b"sockq".to_vec(),
};
let socket_options_plan = KboxlikeFdSocketOptionsPlan {
host_pid: 2001,
model_pid: pid,
model_thread_id: 1,
fd: 11,
target: "socket:[444]".to_string(),
backing: scm_b_backing.clone(),
int_options: vec![KboxlikeSocketIntOption {
level: libc::SOL_SOCKET,
name: libc::SO_PASSCRED,
value: 1,
}],
};
let scm_plan = KboxlikeFdScmRightsPlan {
host_pid: 2001,
model_pid: pid,
model_thread_id: 1,
receiver_fd: 11,
injector_fd: 10,
target: "socket:[444]".to_string(),
receiver_backing: scm_b_backing,
injector_backing: scm_a_backing.clone(),
data: b"r".to_vec(),
passed_fds: vec![8],
passed_backings: vec![eventfd_backing],
credentials: Some(KboxlikeUnixCredentials {
pid: unsafe { libc::getpid() },
uid: unsafe { libc::getuid() },
gid: unsafe { libc::getgid() },
}),
credential_identity: None,
};
let socket_shutdown_plan = KboxlikeFdSocketShutdownPlan {
host_pid: 2001,
model_pid: pid,
model_thread_id: 1,
fd: 10,
target: "socket:[333]".to_string(),
backing: scm_a_backing,
shutdown: KboxlikeSocketShutdownState {
read_closed: false,
write_closed: true,
},
};
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let rebuilt =
KboxlikeFdSystem::restore_rebuilding_shadows(fds.snapshot(), &mut rebuilder).unwrap();
let summary = rebuilder
.replay_fd_kernel_state(
&rebuilt,
std::slice::from_ref(&epoll_plan),
std::slice::from_ref(&socket_options_plan),
std::slice::from_ref(&queued_plan),
std::slice::from_ref(&socket_shutdown_plan),
std::slice::from_ref(&scm_plan),
)
.unwrap();
assert_eq!(
summary,
KboxlikeFdKernelReplaySummary {
epoll_interests: 1,
socket_options: 1,
queued_bytes: queued_plan.bytes.len(),
socket_shutdowns: 1,
scm_rights_messages: 1,
}
);
let queued_receiver_shadow = rebuilt.descriptor(pid, 7).unwrap().shadow.unwrap();
let mut queued_receiver = File::from(
rebuilder
.dup_handle(queued_receiver_shadow.supervisor_fd)
.unwrap(),
);
let mut queued_buf = vec![0u8; queued_plan.bytes.len()];
queued_receiver.read_exact(&mut queued_buf).unwrap();
assert_eq!(queued_buf, queued_plan.bytes);
let scm_receiver_shadow = rebuilt.descriptor(pid, 11).unwrap().shadow.unwrap();
let scm_receiver = rebuilder
.dup_handle(scm_receiver_shadow.supervisor_fd)
.unwrap();
assert_eq!(
get_socket_int_option(
scm_receiver.as_raw_fd(),
libc::SOL_SOCKET,
libc::SO_PASSCRED
)
.unwrap(),
1
);
let (data, passed, credentials) = recv_one_scm_right(scm_receiver.as_raw_fd(), 1);
assert_eq!(data, b"r");
assert_eq!(credentials, scm_plan.credentials);
let mut value = 0u64;
let rc = unsafe {
libc::read(
passed.as_raw_fd(),
&mut value as *mut u64 as *mut libc::c_void,
std::mem::size_of::<u64>(),
)
};
assert_eq!(rc, std::mem::size_of::<u64>() as isize);
assert_eq!(value, 7);
let mut eof_buf = [0u8; 1];
let eof_read = unsafe {
libc::read(
scm_receiver.as_raw_fd(),
eof_buf.as_mut_ptr() as *mut libc::c_void,
eof_buf.len(),
)
};
assert_eq!(eof_read, 0);
let value = 1u64;
let rc = unsafe {
libc::write(
passed.as_raw_fd(),
&value as *const u64 as *const libc::c_void,
std::mem::size_of::<u64>(),
)
};
assert_eq!(rc, std::mem::size_of::<u64>() as isize);
let epoll_shadow = rebuilt.descriptor(pid, 9).unwrap().shadow.unwrap();
let epoll_handle = rebuilder.dup_handle(epoll_shadow.supervisor_fd).unwrap();
let mut events = [libc::epoll_event { events: 0, u64: 0 }; 1];
let ready =
unsafe { libc::epoll_wait(epoll_handle.as_raw_fd(), events.as_mut_ptr(), 1, 0) };
assert_eq!(ready, 1);
let event_data = unsafe { std::ptr::addr_of!(events[0].u64).read_unaligned() };
assert_eq!(event_data, 0xfeed_cafe);
fs::remove_dir_all(rootfs).unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn replay_fd_queued_data_restores_pipe_buffer() {
let rootfs = test_rootfs("queued-pipe");
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init_with_exe("/chrome");
let read_backing = ShadowBacking::HostPassthrough {
label: "pipe:network:0".to_string(),
};
let write_backing = ShadowBacking::HostPassthrough {
label: "pipe:network:1".to_string(),
};
fds.insert_shadow_fd_with_backing(
pid,
3,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: 3,
},
read_backing.clone(),
false,
)
.unwrap();
fds.insert_shadow_fd_with_backing(
pid,
4,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: 4,
},
write_backing.clone(),
false,
)
.unwrap();
let plan = KboxlikeFdQueuedDataPlan {
host_pid: 2001,
model_pid: pid,
model_thread_id: 1,
receiver_fd: 3,
injector_fd: 4,
target: "pipe:[12345]".to_string(),
receiver_backing: read_backing,
injector_backing: write_backing,
bytes: b"pipeq".to_vec(),
};
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let rebuilt =
KboxlikeFdSystem::restore_rebuilding_shadows(fds.snapshot(), &mut rebuilder).unwrap();
assert_eq!(
rebuilder
.replay_fd_queued_data(&rebuilt, std::slice::from_ref(&plan))
.unwrap(),
plan.bytes.len()
);
let read_shadow = rebuilt.descriptor(pid, 3).unwrap().shadow.unwrap();
let mut reader = File::from(rebuilder.dup_handle(read_shadow.supervisor_fd).unwrap());
let mut buf = vec![0u8; plan.bytes.len()];
reader.read_exact(&mut buf).unwrap();
assert_eq!(buf, plan.bytes);
fs::remove_dir_all(rootfs).unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn replay_fd_queued_data_restores_socketpair_buffer() {
let rootfs = test_rootfs("queued-socketpair");
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init_with_exe("/chrome");
let a_backing = ShadowBacking::HostPassthrough {
label: "socketpair:crashpad:0".to_string(),
};
let b_backing = ShadowBacking::HostPassthrough {
label: "socketpair:crashpad:1".to_string(),
};
fds.insert_shadow_fd_with_backing(
pid,
6,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: 6,
},
a_backing.clone(),
false,
)
.unwrap();
fds.insert_shadow_fd_with_backing(
pid,
7,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: 7,
},
b_backing.clone(),
false,
)
.unwrap();
let plan = KboxlikeFdQueuedDataPlan {
host_pid: 2001,
model_pid: pid,
model_thread_id: 1,
receiver_fd: 7,
injector_fd: 6,
target: "socket:[222]".to_string(),
receiver_backing: b_backing,
injector_backing: a_backing,
bytes: b"sockq".to_vec(),
};
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let rebuilt =
KboxlikeFdSystem::restore_rebuilding_shadows(fds.snapshot(), &mut rebuilder).unwrap();
assert_eq!(
rebuilder
.replay_fd_queued_data(&rebuilt, std::slice::from_ref(&plan))
.unwrap(),
plan.bytes.len()
);
let receiver_shadow = rebuilt.descriptor(pid, 7).unwrap().shadow.unwrap();
let mut receiver = File::from(rebuilder.dup_handle(receiver_shadow.supervisor_fd).unwrap());
let mut buf = vec![0u8; plan.bytes.len()];
receiver.read_exact(&mut buf).unwrap();
assert_eq!(buf, plan.bytes);
fs::remove_dir_all(rootfs).unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn replay_fd_socket_options_restores_socketpair_options() {
let rootfs = test_rootfs("socket-options");
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init_with_exe("/chrome");
let a_backing = ShadowBacking::HostPassthrough {
label: "socketpair:crashpad:0".to_string(),
};
let b_backing = ShadowBacking::HostPassthrough {
label: "socketpair:crashpad:1".to_string(),
};
fds.insert_shadow_fd_with_backing(
pid,
6,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: 6,
},
a_backing,
false,
)
.unwrap();
fds.insert_shadow_fd_with_backing(
pid,
7,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: 7,
},
b_backing.clone(),
false,
)
.unwrap();
let plan = KboxlikeFdSocketOptionsPlan {
host_pid: 2001,
model_pid: pid,
model_thread_id: 1,
fd: 7,
target: "socket:[222]".to_string(),
backing: b_backing,
int_options: vec![KboxlikeSocketIntOption {
level: libc::SOL_SOCKET,
name: libc::SO_PASSCRED,
value: 1,
}],
};
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let rebuilt =
KboxlikeFdSystem::restore_rebuilding_shadows(fds.snapshot(), &mut rebuilder).unwrap();
assert_eq!(
rebuilder
.replay_fd_socket_options(&rebuilt, std::slice::from_ref(&plan))
.unwrap(),
1
);
let receiver_shadow = rebuilt.descriptor(pid, 7).unwrap().shadow.unwrap();
let receiver = rebuilder.dup_handle(receiver_shadow.supervisor_fd).unwrap();
assert_eq!(
get_socket_int_option(receiver.as_raw_fd(), libc::SOL_SOCKET, libc::SO_PASSCRED)
.unwrap(),
1
);
fs::remove_dir_all(rootfs).unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn replay_fd_socket_shutdown_restores_socketpair_write_eof() {
let rootfs = test_rootfs("socket-shutdown");
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init_with_exe("/chrome");
let a_backing = ShadowBacking::HostPassthrough {
label: "socketpair:crashpad:0".to_string(),
};
let b_backing = ShadowBacking::HostPassthrough {
label: "socketpair:crashpad:1".to_string(),
};
fds.insert_shadow_fd_with_backing(
pid,
6,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: 6,
},
a_backing.clone(),
false,
)
.unwrap();
fds.insert_shadow_fd_with_backing(
pid,
7,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: 7,
},
b_backing,
false,
)
.unwrap();
let plan = KboxlikeFdSocketShutdownPlan {
host_pid: 2001,
model_pid: pid,
model_thread_id: 1,
fd: 6,
target: "socket:[111]".to_string(),
backing: a_backing,
shutdown: KboxlikeSocketShutdownState {
read_closed: false,
write_closed: true,
},
};
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let rebuilt =
KboxlikeFdSystem::restore_rebuilding_shadows(fds.snapshot(), &mut rebuilder).unwrap();
assert_eq!(
rebuilder
.replay_fd_socket_shutdown(&rebuilt, std::slice::from_ref(&plan))
.unwrap(),
1
);
let peer_shadow = rebuilt.descriptor(pid, 7).unwrap().shadow.unwrap();
let peer = rebuilder.dup_handle(peer_shadow.supervisor_fd).unwrap();
let mut buf = [0u8; 1];
let read = unsafe {
libc::read(
peer.as_raw_fd(),
buf.as_mut_ptr() as *mut libc::c_void,
buf.len(),
)
};
assert_eq!(read, 0);
fs::remove_dir_all(rootfs).unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn replay_fd_scm_rights_restores_passed_eventfd() {
let rootfs = test_rootfs("scm-rights");
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init_with_exe("/chrome");
let a_backing = ShadowBacking::HostPassthrough {
label: "socketpair:broker:0".to_string(),
};
let b_backing = ShadowBacking::HostPassthrough {
label: "socketpair:broker:1".to_string(),
};
let eventfd_backing = ShadowBacking::HostPassthrough {
label: "eventfd:init=7,flags=0".to_string(),
};
fds.insert_shadow_fd_with_backing(
pid,
6,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: 6,
},
a_backing.clone(),
false,
)
.unwrap();
fds.insert_shadow_fd_with_backing(
pid,
7,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: 7,
},
b_backing.clone(),
false,
)
.unwrap();
fds.insert_shadow_fd_with_backing(
pid,
8,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: 8,
},
eventfd_backing.clone(),
false,
)
.unwrap();
let plan = KboxlikeFdScmRightsPlan {
host_pid: 2001,
model_pid: pid,
model_thread_id: 1,
receiver_fd: 7,
injector_fd: 6,
target: "socket:[222]".to_string(),
receiver_backing: b_backing,
injector_backing: a_backing,
data: b"r".to_vec(),
passed_fds: vec![8],
passed_backings: vec![eventfd_backing],
credentials: Some(KboxlikeUnixCredentials {
pid: unsafe { libc::getpid() },
uid: unsafe { libc::getuid() },
gid: unsafe { libc::getgid() },
}),
credential_identity: None,
};
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let rebuilt =
KboxlikeFdSystem::restore_rebuilding_shadows(fds.snapshot(), &mut rebuilder).unwrap();
let receiver_shadow = rebuilt.descriptor(pid, 7).unwrap().shadow.unwrap();
let receiver = rebuilder.dup_handle(receiver_shadow.supervisor_fd).unwrap();
set_socket_int_option(receiver.as_raw_fd(), libc::SOL_SOCKET, libc::SO_PASSCRED, 1)
.unwrap();
assert_eq!(
rebuilder
.replay_fd_scm_rights(&rebuilt, std::slice::from_ref(&plan))
.unwrap(),
1
);
let (data, passed, credentials) = recv_one_scm_right(receiver.as_raw_fd(), 1);
assert_eq!(data, b"r");
assert_eq!(credentials, plan.credentials);
let mut value = 0u64;
let rc = unsafe {
libc::read(
passed.as_raw_fd(),
&mut value as *mut u64 as *mut libc::c_void,
std::mem::size_of::<u64>(),
)
};
assert_eq!(rc, std::mem::size_of::<u64>() as isize);
assert_eq!(value, 7);
fs::remove_dir_all(rootfs).unwrap();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
fn replay_fd_scm_rights_remaps_credentials_to_replacement_pid() {
let rootfs = test_rootfs("scm-rights-remap");
let mut fds = KboxlikeFdSystem::new();
let pid = fds.spawn_init_with_exe("/chrome");
let a_backing = ShadowBacking::HostPassthrough {
label: "socketpair:broker-remap:0".to_string(),
};
let b_backing = ShadowBacking::HostPassthrough {
label: "socketpair:broker-remap:1".to_string(),
};
let eventfd_backing = ShadowBacking::HostPassthrough {
label: "eventfd:init=9,flags=0".to_string(),
};
fds.insert_shadow_fd_with_backing(
pid,
6,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: 6,
},
a_backing.clone(),
false,
)
.unwrap();
fds.insert_shadow_fd_with_backing(
pid,
7,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: 7,
},
b_backing.clone(),
false,
)
.unwrap();
fds.insert_shadow_fd_with_backing(
pid,
8,
None,
ShadowObject {
kind: ShadowKind::HostPassthrough,
supervisor_fd: 8,
},
eventfd_backing.clone(),
false,
)
.unwrap();
let old_sender_pid = 424_242;
let model_thread_id = 99;
let replacement_pid = unsafe { libc::getpid() };
let plan = KboxlikeFdScmRightsPlan {
host_pid: 2001,
model_pid: pid,
model_thread_id: 1,
receiver_fd: 7,
injector_fd: 6,
target: "socket:[222]".to_string(),
receiver_backing: b_backing,
injector_backing: a_backing,
data: b"r".to_vec(),
passed_fds: vec![8],
passed_backings: vec![eventfd_backing],
credentials: Some(KboxlikeUnixCredentials {
pid: old_sender_pid,
uid: unsafe { libc::getuid() },
gid: unsafe { libc::getgid() },
}),
credential_identity: Some(KboxlikeTraceeIdentity {
host_pid: old_sender_pid,
model_pid: pid,
model_thread_id,
}),
};
let mut replacement_host_pids = BTreeMap::new();
replacement_host_pids.insert(model_thread_id, replacement_pid);
let mut rebuilder = HostShadowRebuilder::new(&rootfs);
let rebuilt =
KboxlikeFdSystem::restore_rebuilding_shadows(fds.snapshot(), &mut rebuilder).unwrap();
let receiver_shadow = rebuilt.descriptor(pid, 7).unwrap().shadow.unwrap();
let receiver = rebuilder.dup_handle(receiver_shadow.supervisor_fd).unwrap();
set_socket_int_option(receiver.as_raw_fd(), libc::SOL_SOCKET, libc::SO_PASSCRED, 1)
.unwrap();
assert_eq!(
rebuilder
.replay_fd_scm_rights_with_replacement_pids(
&rebuilt,
std::slice::from_ref(&plan),
&replacement_host_pids,
)
.unwrap(),
1
);
let (data, _passed, credentials) = recv_one_scm_right(receiver.as_raw_fd(), 1);
assert_eq!(data, b"r");
assert_eq!(
credentials,
Some(KboxlikeUnixCredentials {
pid: replacement_pid,
uid: unsafe { libc::getuid() },
gid: unsafe { libc::getgid() },
})
);
fs::remove_dir_all(rootfs).unwrap();
}
}