use std::ffi::{c_int, c_void};
use std::fs::{DirEntry, File, canonicalize};
use std::io::{self, BufRead, BufReader, Seek};
use std::os::fd::{AsRawFd, BorrowedFd};
use std::os::unix::net::UnixStream;
use std::os::unix::process::{CommandExt, ExitStatusExt};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::Arc;
use gio::glib;
use libseccomp::error::SeccompError;
use libseccomp::{ScmpAction, ScmpFilterContext, ScmpSyscall};
use memfd::{Memfd, MemfdOptions};
use nix::libc::siginfo_t;
use nix::sys::resource;
use crate::config::{ConfigEntry, ImageLoaderConfig};
use crate::util::{self, AsyncMutex, new_async_mutex, spawn_blocking};
use crate::{Error, SandboxMechanism};
type SystemSetupStore = Arc<Result<SystemSetup, Arc<io::Error>>>;
static SYSTEM_SETUP: AsyncMutex<Option<SystemSetupStore>> = new_async_mutex(None);
const ALLOWED_SYSCALLS: &[&str] = &[
"access",
"arch_prctl",
"arm_fadvise64_64",
"brk",
"capget",
"capset",
"chdir",
"clock_getres",
"clock_gettime",
"clock_gettime64",
"clone",
"clone3",
"close",
"connect",
"creat",
"dup",
"epoll_create",
"epoll_create1",
"epoll_ctl",
"epoll_pwait",
"epoll_wait",
"eventfd",
"eventfd2",
"execve",
"exit",
"exit_group",
"faccessat",
"fadvise64",
"fadvise64_64",
"fchdir",
"fcntl",
"fcntl",
"fcntl64",
"fstat",
"fstatfs",
"fstatfs64",
"ftruncate",
"ftruncate64",
"futex",
"futex_time64",
"get_mempolicy",
"getcwd",
"getdents64",
"getegid",
"getegid32",
"geteuid",
"geteuid32",
"getgid",
"getgid32",
"getpid",
"getppid",
"getpriority",
"getrandom",
"gettid",
"gettimeofday",
"getuid",
"getuid32",
"ioctl",
"madvise",
"membarrier",
"memfd_create",
"mmap",
"mmap2",
"mprotect",
"mremap",
"munmap",
"newfstatat",
"open",
"openat",
"pipe",
"pipe2",
"pivot_root",
"poll",
"ppoll",
"ppoll_time64",
"prctl",
"pread64",
"prlimit64",
"read",
"readlink",
"readlinkat",
"recv",
"recvfrom",
"recvmsg",
"restart_syscall",
"riscv_hwprobe",
"rseq",
"rt_sigaction",
"rt_sigprocmask",
"rt_sigreturn",
"sched_getaffinity",
"sched_yield",
"sendmsg",
"sendto",
"set_mempolicy",
"set_mempolicy",
"set_robust_list",
"set_thread_area",
"set_tid_address",
"set_tls",
"setpriority",
"sigaltstack",
"signalfd4",
"socket",
"socketcall",
"stat",
"statfs",
"statfs64",
"statx",
"sysinfo",
"tgkill",
"time",
"timerfd_create",
"timerfd_settime",
"timerfd_settime64",
"ugetrlimit",
"uname",
"unshare",
"wait4",
"write",
"writev",
];
const ALLOWED_SYSCALLS_FONTCONFIG: &[&str] = &[
"chmod",
"fchmodat",
"link",
"linkat",
"rename",
"renameat",
"renameat2",
"unlink",
"unlinkat",
];
const INHERITED_ENVIRONMENT_VARIABLES: &[&str] = &["RUST_BACKTRACE", "RUST_LOG", "XDG_RUNTIME_DIR"];
pub struct Sandbox {
sandbox_mechanism: SandboxMechanism,
config_entry: ConfigEntry,
dbus_socket: UnixStream,
ro_bind_extra: Vec<PathBuf>,
}
static_assertions::assert_impl_all!(Sandbox: Send, Sync);
pub struct SpawnedSandbox {
pub command: Command,
pub _seccomp_fd: Option<Memfd>,
pub _dbus_socket: UnixStream,
}
static_assertions::assert_impl_all!(SpawnedSandbox: Send, Sync);
impl Sandbox {
pub fn new(
sandbox_mechanism: SandboxMechanism,
config_entry: ConfigEntry,
dbus_socket: UnixStream,
) -> Self {
Self {
sandbox_mechanism,
config_entry,
dbus_socket,
ro_bind_extra: Vec::new(),
}
}
fn exec(&self) -> &Path {
self.config_entry.exec()
}
pub fn add_ro_bind(&mut self, path: PathBuf) {
self.ro_bind_extra.push(path);
}
pub async fn spawn(self) -> Result<SpawnedSandbox, Error> {
let dbus_fd = self.dbus_socket.as_raw_fd();
let mut shared_fds = Vec::new();
let (mut command, seccomp_fd) = match self.sandbox_mechanism {
SandboxMechanism::Bwrap => {
let seccomp_memfd = Self::seccomp_export_bpf(&self.seccomp_filter()?)?;
let command = self.bwrap_command(&seccomp_memfd).await?;
shared_fds.push(seccomp_memfd.as_raw_fd());
(command, Some(seccomp_memfd))
}
SandboxMechanism::FlatpakSpawn => {
let command = self.flatpak_spawn_command();
(command, None)
}
SandboxMechanism::NotSandboxed => {
eprintln!("WARNING: Glycin running without sandbox.");
let command = self.no_sandbox_command();
(command, None)
}
};
command.arg("--dbus-fd");
command.arg(dbus_fd.to_string());
command.stdin(Stdio::piped());
command.stderr(Stdio::piped());
command.stdout(Stdio::piped());
shared_fds.push(self.dbus_socket.as_raw_fd());
unsafe {
command.pre_exec(move || {
#[cfg(not(all(target_os = "linux", target_env = "musl")))]
{
libc::close_range(3, libc::c_uint::MAX, libc::CLOSE_RANGE_CLOEXEC as i32);
}
#[cfg(all(target_os = "linux", target_env = "musl"))]
{
libc::syscall(
libc::SYS_close_range,
3,
libc::c_uint::MAX,
libc::CLOSE_RANGE_CLOEXEC as libc::c_uint,
);
}
for raw_fd in &shared_fds {
let fd = BorrowedFd::borrow_raw(*raw_fd);
if let Ok(flags) = nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_GETFD) {
let mut flags = nix::fcntl::FdFlag::from_bits_truncate(flags);
flags.remove(nix::fcntl::FdFlag::FD_CLOEXEC);
let _ = nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_SETFD(flags));
}
}
Ok(())
});
}
Ok(SpawnedSandbox {
command,
_seccomp_fd: seccomp_fd,
_dbus_socket: self.dbus_socket,
})
}
async fn bwrap_command(&self, seccomp_memfd: &Memfd) -> Result<Command, Error> {
let mut command = Command::new("bwrap");
command.args([
"--unshare-all",
"--die-with-parent",
"--chdir",
"/",
"--ro-bind",
"/usr",
"/usr",
"--dev",
"/dev",
"--ro-bind-try",
"/etc/ld.so.cache",
"/etc/ld.so.cache",
"--ro-bind-try",
"/nix/store",
"/nix/store",
"--tmpfs",
"/tmp-home",
"--tmpfs",
"/tmp-run",
"--clearenv",
"--setenv",
"HOME",
"/tmp-home",
"--setenv",
"XDG_RUNTIME_DIR",
"/tmp-run",
]);
for key in INHERITED_ENVIRONMENT_VARIABLES {
if let Some(val) = std::env::var_os(key) {
command.arg("--setenv");
command.arg(key);
command.arg(val);
}
}
let system_setup_arc = SystemSetup::cached().await;
let system = match system_setup_arc.as_ref().as_ref() {
Err(err) => {
return Err(err.clone().into());
}
Ok(system) => system,
};
for (dest, src) in &system.lib_symlinks {
command.arg("--symlink");
command.arg(src);
command.arg(dest);
}
let mut mounted_paths = Vec::<PathBuf>::new();
let mut mount = |command: &mut Command, way: &str, path: &Path| {
if path.is_symlink() {
if !mounted_paths.iter().any(|x| path.starts_with(x)) {
match canonicalize(path) {
Ok(target) => {
if !mounted_paths.iter().any(|x| path.starts_with(x)) {
command.arg("--symlink");
command.arg(&target);
command.arg(path);
tracing::trace!("Symlink {path:?} -> {target:?}");
mounted_paths.push(path.to_owned());
} else {
tracing::trace!(
"Parent of symlink path {path:?} already mounted. Skipping."
);
}
}
Err(err) => tracing::debug!("Couldn't canonicalize path {path:?}: {err}"),
}
} else {
tracing::trace!("Parent of symlink {path:?} already mounted. Skipping.");
}
}
match canonicalize(path) {
Ok(path) => {
if !mounted_paths.iter().any(|x| path.starts_with(x)) {
command.arg(way);
command.arg(&path);
command.arg(&path);
tracing::trace!("Mounting {path:?}");
mounted_paths.push(path);
} else {
tracing::trace!("Parent of mount path {path:?} already mounted. Skipping.");
}
}
Err(err) => tracing::debug!("Couldn't canonicalize path {path:?}: {err}"),
}
};
let caps = get_caps();
let mut caps_reset_guard = None;
match caps {
Ok(caps) => {
const CAP_DAC_OVERRIDE_POSITION: u32 = 1_u32;
const CAP_DAC_READ_SEARCH_POSTION: u32 = 2_u32;
if caps[0].effective & (1 << CAP_DAC_OVERRIDE_POSITION) != 0
|| caps[0].effective & (1 << CAP_DAC_READ_SEARCH_POSTION) != 0
{
let mut new_caps = caps;
new_caps[0].effective &= !(1 << CAP_DAC_OVERRIDE_POSITION);
new_caps[0].effective &= !(1 << CAP_DAC_READ_SEARCH_POSTION);
if let Err(err) = set_caps(new_caps) {
tracing::error!("Failed to set caps: {err}");
} else {
caps_reset_guard = Some(CapsGuard(caps));
}
} else {
tracing::trace!("CAP_DAC_OVERRIDE not set. Not touching CAPs");
}
}
Err(ref err) => tracing::error!("Couldn't get Linux caps: {err}"),
}
for dir in &system.lib_dirs {
mount(&mut command, "--ro-bind", dir);
}
for dir in &self.ro_bind_extra {
mount(&mut command, "--ro-bind", dir);
}
if !self.exec().starts_with("/usr") {
mount(&mut command, "--ro-bind", self.exec());
}
if !self.config_entry.fontconfig() {
tracing::trace!("Fontconfig not enabled for loader/editor");
} else if let Some(fc_paths) = crate::fontconfig::cached_paths() {
for path in fc_paths {
mount(&mut command, "--ro-bind-try", path);
}
let cache_dir = PathBuf::from_iter([
glib::user_cache_dir(),
"glycin".into(),
self.exec().iter().skip(1).collect(),
]);
let fc_cache_dir = PathBuf::from_iter([cache_dir.clone(), "fontconfig".into()]);
match util::spawn_blocking(move || std::fs::create_dir_all(fc_cache_dir)).await {
Err(err) => tracing::warn!("Failed to create fontconfig cache dir: {err:?}"),
Ok(()) => {
command.arg("--bind-try");
command.arg(&cache_dir);
command.arg(&cache_dir);
command.arg("--setenv");
command.arg("XDG_CACHE_HOME");
command.arg(&cache_dir);
}
}
} else {
tracing::warn!("Failed to load fonftconfig environment");
}
drop(caps_reset_guard);
command.arg("--seccomp");
command.arg(seccomp_memfd.as_raw_fd().to_string());
command.arg(self.exec());
unsafe {
command.pre_exec(|| {
Self::set_memory_limit();
Ok(())
});
}
Ok(command)
}
fn flatpak_spawn_command(&self) -> Command {
let mut command = Command::new("flatpak-spawn");
let memory_limit = Self::memory_limit();
let dbus_fd = self.dbus_socket.as_raw_fd();
tracing::debug!("Setting prlimit to {memory_limit} bytes");
command.args([
"--sandbox",
"--watch-bus",
"--directory=/",
]);
command.env_clear();
for key in INHERITED_ENVIRONMENT_VARIABLES {
if let Some(val) = std::env::var_os(key) {
command.env(key, val);
}
}
command.arg(format!("--forward-fd={dbus_fd}"));
command.arg("prlimit");
command.arg(format!("--as={memory_limit}"));
command.arg(self.exec());
unsafe {
command.pre_exec(|| {
nix::sys::prctl::set_pdeathsig(nix::sys::signal::SIGKILL).map_err(Into::into)
});
}
command
}
fn no_sandbox_command(&self) -> Command {
let mut command = Command::new(self.exec());
command.env_clear();
for key in INHERITED_ENVIRONMENT_VARIABLES {
if let Some(val) = std::env::var_os(key) {
command.env(key, val);
}
}
unsafe {
command.pre_exec(|| {
nix::sys::prctl::set_pdeathsig(nix::sys::signal::SIGKILL).map_err(Into::into)
});
}
command
}
fn memory_limit() -> resource::rlim_t {
if let Some(mem_available) = Self::mem_available() {
Self::calculate_memory_limit(mem_available)
} else {
tracing::warn!("glycin: Unable to determine available memory via /proc/meminfo");
const { (1024 as resource::rlim_t).pow(3) }
}
}
fn mem_available() -> Option<resource::rlim_t> {
if let Ok(file) = File::open("/proc/meminfo") {
let meminfo = BufReader::new(file);
let mut total_avail_kb: Option<resource::rlim_t> = None;
for line in meminfo.lines().map_while(Result::ok) {
if line.starts_with("MemAvailable:") || line.starts_with("SwapFree:") {
tracing::trace!("Using /proc/meminfo: {line}");
if let Some(mem_avail_kb) = line
.split(' ')
.filter(|x| !x.is_empty())
.nth(1)
.and_then(|x| x.parse::<resource::rlim_t>().ok())
{
total_avail_kb =
Some(total_avail_kb.unwrap_or(0).saturating_add(mem_avail_kb));
}
}
}
if let Some(total_avail_kb) = total_avail_kb {
let mem_available = total_avail_kb.saturating_mul(1024);
return Some(mem_available);
}
}
None
}
fn calculate_memory_limit(mem_available: resource::rlim_t) -> resource::rlim_t {
let mem_considered = resource::rlim_t::min(
mem_available,
const { (1024 as resource::rlim_t).pow(3).saturating_mul(20) },
)
.saturating_sub(1024 * 1024 * 200);
(mem_considered as f64 * 0.8) as resource::rlim_t
}
fn set_memory_limit() {
let limit = Self::memory_limit();
let msg = b"Setting process memory limit\n";
unsafe {
let _ = libc::write(libc::STDERR_FILENO, msg.as_ptr() as *const _, msg.len());
}
if resource::setrlimit(resource::Resource::RLIMIT_AS, limit, limit).is_err() {
let msg = b"Error setrlimit(RLIMIT_AS)\n";
unsafe {
let _ = libc::write(libc::STDERR_FILENO, msg.as_ptr() as *const _, msg.len());
}
}
}
fn seccomp_filter(&self) -> Result<ScmpFilterContext, SeccompError> {
let mut filter = if std::env::var("GLYCIN_SECCOMP_DEFAULT_ACTION")
.ok()
.as_deref()
== Some("KILL_PROCESS")
{
ScmpFilterContext::new(ScmpAction::KillProcess)?
} else {
ScmpFilterContext::new(ScmpAction::Trap)?
};
let mut syscalls = vec![ALLOWED_SYSCALLS];
if self.config_entry.fontconfig() {
syscalls.push(ALLOWED_SYSCALLS_FONTCONFIG);
}
for syscall_name in syscalls.into_iter().flatten() {
match ScmpSyscall::from_name(syscall_name) {
Ok(syscall) => {
filter.add_rule(ScmpAction::Allow, syscall)?;
}
Err(err) => tracing::warn!("Failed to allow syscall '{syscall_name}': {err}"),
}
}
Ok(filter)
}
fn seccomp_export_bpf(filter: &ScmpFilterContext) -> Result<Memfd, Error> {
let memfd = MemfdOptions::default()
.close_on_exec(false)
.create("seccomp-bpf-filter")?;
let mut file = memfd.as_file();
filter.export_bpf(file)?;
file.rewind()?;
Ok(memfd)
}
pub async fn check_bwrap_syscalls_blocked() -> bool {
match Self::check_bwrap_syscalls_blocked_internal().await {
Err(err) => {
tracing::info!("Can't determine if bwrap syscalls are blocked: {err} ({err:?})");
false
}
Ok(blocked) => {
tracing::debug!("bwrap sandboxing available: {}", !blocked);
blocked
}
}
}
async fn check_bwrap_syscalls_blocked_internal() -> Result<bool, Error> {
let config_entry = ConfigEntry::Loader(ImageLoaderConfig {
exec: PathBuf::from("/usr/bin/true"),
expose_base_dir: false,
fontconfig: false,
});
let (dbus_socket, _) = UnixStream::pair()?;
let sandbox = Self::new(SandboxMechanism::Bwrap, config_entry, dbus_socket);
let seccomp_memfd = Self::seccomp_export_bpf(&sandbox.seccomp_filter()?)?;
let mut command = sandbox.bwrap_command(&seccomp_memfd).await?;
unsafe {
command.pre_exec(|| {
setup_sigsys_handler();
Ok(())
})
};
tracing::debug!("Testing bwrap availability with: {command:?}");
let output = spawn_blocking(move || command.output()).await?;
tracing::debug!(
"bwrap availability test returned: {output:?} (Signal: {signal:?}, Code: {code:?})",
signal = output.status.signal(),
code = output.status.code(),
);
if output.status.success() {
Ok(false)
} else if matches!(output.status.signal(), Some(libc::SIGSYS))
|| output.status.code() == Some(128 + libc::SIGSYS)
{
tracing::debug!("bwrap syscalls not available: Terminated with SIGSYS");
Ok(true)
} else if std::str::from_utf8(&output.stderr).is_ok_and(|x| {
[
"Creating new namespace failed",
"No permissions to create a new namespace",
"No permissions to creating new namespace",
"No permissions to create new namespace",
"bwrap: setting up uid map: Permission denied",
]
.iter()
.any(|y| x.contains(y))
}) {
tracing::debug!("bwrap syscalls not available: STDERR contains known string");
Ok(true)
} else {
Ok(false)
}
}
}
#[derive(Debug, Default)]
struct SystemSetup {
lib_symlinks: Vec<(PathBuf, PathBuf)>,
lib_dirs: Vec<PathBuf>,
}
impl SystemSetup {
async fn cached() -> SystemSetupStore {
let mut system_setup = SYSTEM_SETUP.lock().await;
if let Some(arc) = &*system_setup {
arc.clone()
} else {
let arc = Arc::new(Self::new().await.map_err(Arc::new));
*system_setup = Some(arc.clone());
arc
}
}
async fn new() -> io::Result<SystemSetup> {
let mut system = SystemSetup::default();
system.load_lib_dirs().await?;
Ok(system)
}
async fn load_lib_dirs(&mut self) -> io::Result<()> {
let dir_content = std::fs::read_dir("/");
match dir_content {
Ok(dir_content) => {
for entry in dir_content {
if let Err(err) = self.add_dir(entry).await {
tracing::warn!("Unable to access entry in root directory (/): {err}");
}
}
}
Err(err) => {
tracing::error!("Unable to list root directory (/) entries: {err}");
}
}
Ok(())
}
async fn add_dir(&mut self, entry: io::Result<DirEntry>) -> io::Result<()> {
let entry = entry?;
let path = entry.path();
if let Some(last_segment) = path.file_name()
&& last_segment.as_encoded_bytes().starts_with(b"lib")
{
let metadata = entry.metadata()?;
if metadata.is_dir() {
self.lib_dirs.push(entry.path());
} else if metadata.is_symlink() {
let target = canonicalize(&path)?;
if target.starts_with("/usr/") {
self.lib_symlinks.push((path, target));
}
}
};
Ok(())
}
}
#[allow(non_camel_case_types)]
extern "C" fn sigsys_handler(_: c_int, _info: *mut siginfo_t, _: *mut c_void) {
libc_eprint("glycin sandbox availability test: Blocked syscall used\n");
unsafe {
libc::exit(128 + libc::SIGSYS);
}
}
fn setup_sigsys_handler() {
let mut mask = nix::sys::signal::SigSet::empty();
mask.add(nix::sys::signal::Signal::SIGSYS);
let sigaction = nix::sys::signal::SigAction::new(
nix::sys::signal::SigHandler::SigAction(sigsys_handler),
nix::sys::signal::SaFlags::SA_SIGINFO,
mask,
);
unsafe {
if nix::sys::signal::sigaction(nix::sys::signal::Signal::SIGSYS, &sigaction).is_err() {
libc_eprint(
"glycin sandbox availability test: Failed to init syscall failure signal handler",
);
}
};
}
fn libc_eprint(s: &str) {
unsafe {
libc::write(
libc::STDERR_FILENO,
s.as_ptr() as *const libc::c_void,
s.len(),
);
}
}
#[repr(C)]
#[derive(Debug)]
struct CapHeader {
version: u32,
pid: i32,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct CapData {
effective: u32,
permitted: u32,
inheritable: u32,
}
const _LINUX_CAPABILITY_VERSION_3: u32 = 0x20080522;
fn capget(header: &mut CapHeader, data: &mut [CapData; 2]) -> std::io::Result<()> {
if unsafe {
libc::syscall(
libc::SYS_capget,
header as *mut CapHeader,
data as *mut CapData,
)
} != 0
{
Err(std::io::Error::last_os_error())
} else {
Ok(())
}
}
fn capset(header: &mut CapHeader, data: &mut [CapData; 2]) -> std::io::Result<()> {
if unsafe {
libc::syscall(
libc::SYS_capset,
header as *mut CapHeader,
data as *mut CapData,
) as i32
} != 0
{
Err(std::io::Error::last_os_error())
} else {
Ok(())
}
}
fn get_caps() -> std::io::Result<[CapData; 2]> {
let mut hdr = CapHeader {
version: _LINUX_CAPABILITY_VERSION_3,
pid: 0,
};
let mut data: [CapData; 2] = unsafe { std::mem::zeroed() };
capget(&mut hdr, &mut data)?;
Ok(data)
}
fn set_caps(mut caps: [CapData; 2]) -> std::io::Result<()> {
let mut hdr = CapHeader {
version: _LINUX_CAPABILITY_VERSION_3,
pid: 0,
};
capset(&mut hdr, &mut caps)
}
struct CapsGuard([CapData; 2]);
impl Drop for CapsGuard {
fn drop(&mut self) {
if let Err(err) = set_caps(self.0) {
tracing::error!("Failed to reset linux caps to original state: {err}")
}
}
}