use boxlite_shared::errors::{BoxliteError, BoxliteResult};
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::{Path, PathBuf};
const MAX_SUN_PATH: usize = 104;
const LONGEST_SOCKET_NAME: &str = "net.sock-krun.sock";
const BOX_SOCK: &str = "box.sock";
const READY_SOCK: &str = "ready.sock";
const NET_SOCK: &str = "net.sock";
const SYMLINK_BASE: &str = "/tmp";
fn parent_dir_name() -> String {
format!("bl-{}", unsafe { libc::getuid() })
}
#[derive(Clone, Debug)]
pub struct BoxSockets {
box_id: String,
real_dir: PathBuf,
}
impl BoxSockets {
pub fn new(box_id: impl Into<String>, real_dir: impl Into<PathBuf>) -> Self {
Self {
box_id: box_id.into(),
real_dir: real_dir.into(),
}
}
pub fn real_dir(&self) -> &Path {
&self.real_dir
}
fn parent_dir() -> PathBuf {
Path::new(SYMLINK_BASE).join(parent_dir_name())
}
pub fn binding_dir(&self) -> PathBuf {
Self::parent_dir().join(&self.box_id)
}
pub fn box_sock(&self) -> PathBuf {
self.binding_dir().join(BOX_SOCK)
}
pub fn ready_sock(&self) -> PathBuf {
self.binding_dir().join(READY_SOCK)
}
pub fn net_backend_sock(&self) -> PathBuf {
self.binding_dir().join(NET_SOCK)
}
pub fn ensure(&self) -> BoxliteResult<()> {
let parent = Self::parent_dir();
ensure_owned_private_dir(&parent)?;
let longest = self.binding_dir().join(LONGEST_SOCKET_NAME);
if longest.as_os_str().len() >= MAX_SUN_PATH {
return Err(BoxliteError::Internal(format!(
"Socket path '{}' ({} bytes) exceeds sun_path limit ({} bytes) \
even via the binding symlink.",
longest.display(),
longest.as_os_str().len(),
MAX_SUN_PATH,
)));
}
let symlink_path = self.binding_dir();
match std::fs::symlink_metadata(&symlink_path) {
Ok(meta) if meta.file_type().is_symlink() => {
if std::fs::read_link(&symlink_path).ok().as_deref() == Some(self.real_dir()) {
return Ok(()); }
let _ = std::fs::remove_file(&symlink_path);
}
Ok(_) => {
return Err(BoxliteError::Internal(format!(
"{} exists but is not a symlink — refusing to overwrite",
symlink_path.display(),
)));
}
Err(_) => {} }
if let Err(e) = std::os::unix::fs::symlink(self.real_dir(), &symlink_path) {
let racing_winner_ok = e.kind() == std::io::ErrorKind::AlreadyExists
&& std::fs::read_link(&symlink_path).ok().as_deref() == Some(self.real_dir());
if !racing_winner_ok {
return Err(BoxliteError::Storage(format!(
"Failed to create socket symlink {} → {}: {}",
symlink_path.display(),
self.real_dir().display(),
e,
)));
}
}
tracing::debug!(
symlink = %symlink_path.display(),
target = %self.real_dir().display(),
"Ensured socket binding symlink"
);
Ok(())
}
pub fn remove(&self) {
let symlink_path = self.binding_dir();
if let Err(e) = std::fs::remove_file(&symlink_path)
&& e.kind() != std::io::ErrorKind::NotFound
{
tracing::warn!(
path = %symlink_path.display(),
error = %e,
"Failed to remove socket binding symlink"
);
}
}
pub fn policy_paths(&self) -> Vec<(PathBuf, bool)> {
vec![
(self.real_dir().to_path_buf(), true),
(self.binding_dir(), true),
(Self::parent_dir(), false),
]
}
pub fn sweep_stale() {
sweep_dangling_symlinks_in(&Self::parent_dir());
}
}
fn ensure_owned_private_dir(dir: &Path) -> BoxliteResult<()> {
use std::os::unix::fs::DirBuilderExt;
match std::fs::DirBuilder::new().mode(0o700).create(dir) {
Ok(()) => return Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(e) => {
return Err(BoxliteError::Storage(format!(
"Failed to create socket symlink dir {}: {}",
dir.display(),
e,
)));
}
}
let meta = std::fs::symlink_metadata(dir)
.map_err(|e| BoxliteError::Storage(format!("Failed to stat {}: {}", dir.display(), e)))?;
if !meta.file_type().is_dir() {
return Err(BoxliteError::Internal(format!(
"{} exists but is not a directory — refusing to use it \
(possible squatting; remove it or check ownership)",
dir.display(),
)));
}
let uid = unsafe { libc::getuid() };
if meta.uid() != uid {
return Err(BoxliteError::Internal(format!(
"{} is owned by uid {} (expected {}) — refusing to use it \
(possible squatting; remove it or check ownership)",
dir.display(),
meta.uid(),
uid,
)));
}
if meta.permissions().mode() & 0o077 != 0 {
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700)).map_err(|e| {
BoxliteError::Storage(format!(
"Failed to tighten permissions on {}: {}",
dir.display(),
e,
))
})?;
}
Ok(())
}
fn sweep_dangling_symlinks_in(dir: &Path) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if let Ok(meta) = std::fs::symlink_metadata(&path) {
if meta.file_type().is_symlink() && !path.exists() {
tracing::debug!(path = %path.display(), "Removing stale socket symlink");
let _ = std::fs::remove_file(&path);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::fs::symlink;
fn unique_sockets(test_tag: &str) -> (tempfile::TempDir, BoxSockets) {
let tmp = tempfile::TempDir::new().unwrap();
let real = tmp.path().join("sockets");
std::fs::create_dir_all(&real).unwrap();
(tmp, BoxSockets::new(test_tag, real))
}
#[test]
fn binding_paths_are_short_and_deterministic() {
let (_tmp, s) = unique_sockets("purepath1");
let expect = Path::new("/tmp")
.join(format!("bl-{}", unsafe { libc::getuid() }))
.join("purepath1");
assert_eq!(s.binding_dir(), expect);
assert_eq!(s.box_sock(), expect.join("box.sock"));
assert_eq!(s.ready_sock(), expect.join("ready.sock"));
assert_eq!(s.net_backend_sock(), expect.join("net.sock"));
for p in [s.box_sock(), s.ready_sock(), s.net_backend_sock()] {
assert!(p.as_os_str().len() + "-krun.sock".len() < MAX_SUN_PATH);
}
}
#[test]
fn dead_zone_real_dir_still_binds_short() {
let tmp = tempfile::TempDir::new().unwrap();
let base_len = tmp.path().as_os_str().len();
assert!(base_len < 80, "tempdir base unexpectedly long: {base_len}");
let deep = tmp.path().join("p".repeat(88 - base_len - 1));
std::fs::create_dir_all(&deep).unwrap();
let ready = deep.join("ready.sock").as_os_str().len();
let krun = deep.join(LONGEST_SOCKET_NAME).as_os_str().len();
assert!(
ready < MAX_SUN_PATH && krun >= MAX_SUN_PATH,
"test setup: want dead zone, got ready.sock={ready}, krun={krun}",
);
let s = BoxSockets::new("deadzone1", &deep);
s.ensure().unwrap();
assert!(
s.net_backend_sock().as_os_str().len() + "-krun.sock".len() < MAX_SUN_PATH,
"binding path must fit with krun's derived suffix"
);
assert_eq!(std::fs::read_link(s.binding_dir()).unwrap(), deep);
s.remove();
}
#[test]
fn ensure_creates_symlink_and_is_idempotent() {
let (_tmp, s) = unique_sockets("ens_idem1");
s.ensure().unwrap();
let meta = std::fs::symlink_metadata(s.binding_dir()).unwrap();
assert!(meta.file_type().is_symlink());
assert_eq!(std::fs::read_link(s.binding_dir()).unwrap(), s.real_dir());
s.ensure().unwrap(); assert_eq!(std::fs::read_link(s.binding_dir()).unwrap(), s.real_dir());
s.remove();
}
#[test]
fn ensure_replaces_stale_symlink() {
let (_tmp, s) = unique_sockets("ens_stale1");
std::fs::create_dir_all(BoxSockets::parent_dir()).unwrap();
let _ = std::fs::remove_file(s.binding_dir());
symlink(Path::new("/nonexistent/stale/path"), s.binding_dir()).unwrap();
s.ensure().unwrap();
assert_eq!(
std::fs::read_link(s.binding_dir()).unwrap(),
s.real_dir(),
"should point at the new target, not the stale one"
);
s.remove();
}
#[test]
fn ensure_refuses_to_overwrite_regular_file() {
let (_tmp, s) = unique_sockets("ens_file1");
std::fs::create_dir_all(BoxSockets::parent_dir()).unwrap();
std::fs::write(s.binding_dir(), "not a symlink").unwrap();
let err = s.ensure().unwrap_err().to_string();
assert!(err.contains("not a symlink"), "got: {err}");
let _ = std::fs::remove_file(s.binding_dir());
}
#[test]
fn ensure_refuses_to_overwrite_directory() {
let (_tmp, s) = unique_sockets("ens_dir1");
std::fs::create_dir_all(s.binding_dir()).unwrap();
assert!(s.ensure().is_err());
let _ = std::fs::remove_dir_all(s.binding_dir());
}
#[test]
fn ensure_repairs_parent_permissions() {
let parent = BoxSockets::parent_dir();
std::fs::create_dir_all(&parent).unwrap();
std::fs::set_permissions(&parent, std::fs::Permissions::from_mode(0o755)).unwrap();
let (_tmp, s) = unique_sockets("ens_perm1");
s.ensure().unwrap();
let mode = std::fs::symlink_metadata(&parent)
.unwrap()
.permissions()
.mode();
assert_eq!(mode & 0o777, 0o700, "parent must be repaired to 0700");
s.remove();
}
#[test]
fn remove_deletes_symlink_and_is_idempotent() {
let (_tmp, s) = unique_sockets("rm_idem1");
s.ensure().unwrap();
assert!(std::fs::symlink_metadata(s.binding_dir()).is_ok());
s.remove();
assert!(std::fs::symlink_metadata(s.binding_dir()).is_err());
s.remove(); }
#[test]
fn sweep_removes_dangling_keeps_live() {
let parent = BoxSockets::parent_dir();
std::fs::create_dir_all(&parent).unwrap();
let dead = parent.join("sweep_dead1");
let _ = std::fs::remove_file(&dead);
symlink(Path::new("/nonexistent/target/for/test"), &dead).unwrap();
let (_tmp, live) = unique_sockets("sweep_live1");
live.ensure().unwrap();
BoxSockets::sweep_stale();
assert!(
std::fs::symlink_metadata(&dead).is_err(),
"dangling symlink should be removed"
);
assert!(
std::fs::symlink_metadata(live.binding_dir()).is_ok(),
"live symlink should be kept"
);
live.remove();
}
#[test]
fn sweep_ignores_non_symlink_entries() {
let parent = BoxSockets::parent_dir();
std::fs::create_dir_all(&parent).unwrap();
let dir_entry = parent.join("sweep_realdir1");
let _ = std::fs::remove_dir_all(&dir_entry);
std::fs::create_dir_all(&dir_entry).unwrap();
BoxSockets::sweep_stale();
assert!(
std::fs::symlink_metadata(&dir_entry).is_ok(),
"real directories must never be swept"
);
let _ = std::fs::remove_dir_all(&dir_entry);
}
#[test]
fn bind_and_connect_through_symlink_works() {
let tmp = tempfile::TempDir::new().unwrap();
let real_dir = tmp.path().join("real_sockets");
std::fs::create_dir_all(&real_dir).unwrap();
let short_link = tmp.path().join("s");
symlink(&real_dir, &short_link).unwrap();
let sock_path = short_link.join("test.sock");
let listener = std::os::unix::net::UnixListener::bind(&sock_path).unwrap();
assert!(real_dir.join("test.sock").exists());
let _stream = std::os::unix::net::UnixStream::connect(&sock_path).unwrap();
drop(listener);
}
#[test]
fn bind_through_symlink_with_long_real_path() {
let tmp = tempfile::TempDir::new().unwrap();
let deep = tmp
.path()
.join("very_long_directory_name_that_keeps_going")
.join("and_another_long_segment_here_too")
.join("sockets");
std::fs::create_dir_all(&deep).unwrap();
assert!(deep.join(LONGEST_SOCKET_NAME).as_os_str().len() >= MAX_SUN_PATH);
let short_link = tmp.path().join("s");
symlink(&deep, &short_link).unwrap();
let short_path = short_link.join("kernel_test.sock");
assert!(short_path.as_os_str().len() < MAX_SUN_PATH);
let listener = std::os::unix::net::UnixListener::bind(&short_path).unwrap();
let _stream = std::os::unix::net::UnixStream::connect(&short_path).unwrap();
assert!(deep.join("kernel_test.sock").exists());
drop(listener);
}
#[test]
fn vendored_libkrun_still_derives_krun_sock_suffix() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../deps/libkrun-sys/vendor/libkrun/src/devices/src/virtio/net/unixgram.rs"
);
let src = std::fs::read_to_string(path)
.unwrap_or_else(|e| panic!("cannot read vendored libkrun source {path}: {e}"));
assert!(
src.contains("-krun.sock"),
"vendored libkrun no longer derives a '-krun.sock' sibling socket; \
re-derive LONGEST_SOCKET_NAME in net/socket_path.rs from the new \
vendored behavior before trusting the sun_path sanity check"
);
}
}