Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions crates/muvm/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
use std::{
env::{self, VarError},
io,
env, io,
path::{Path, PathBuf},
};

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

use crate::utils::env::get_var_if_exists;

#[derive(Deserialize, Serialize, Default)]
pub struct Configuration {
pub execute_pre: Option<PathBuf>,
}

fn get_var_if_exists(name: &str) -> Option<Result<String>> {
match env::var(name) {
Ok(val) => Some(Ok(val)),
Err(VarError::NotPresent) => None,
Err(e) => Some(Err(e.into())),
}
}

fn get_user_config_path() -> Result<PathBuf> {
let mut path = get_var_if_exists("XDG_CONFIG_DIR").map_or_else(
|| {
Expand Down
34 changes: 25 additions & 9 deletions crates/muvm/src/guest/bin/muvm-guest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use muvm::guest::server::server_main;
use muvm::guest::socket::setup_socket_proxy;
use muvm::guest::user::setup_user;
use muvm::guest::x11::setup_x11_forwarding;
use muvm::utils::env::get_var_if_exists;
use muvm::utils::launch::{Emulator, GuestConfiguration, PULSE_SOCKET};
use nix::unistd::{Gid, Uid};
use rustix::process::{getrlimit, setrlimit, Resource};
Expand Down Expand Up @@ -77,27 +78,42 @@ fn main() -> Result<ExitCode> {
rustix::stdio::dup2_stdout(console.as_fd())?;
rustix::stdio::dup2_stderr(console.as_fd())?;

Command::new("/usr/lib/systemd/systemd-udevd").spawn()?;
const DEFAULT_UDEVD_PATH: &str = match std::option_env!("MUVM_UDEVD_PATH") {
Some(path) => path,
None => "/usr/lib/systemd/systemd-udevd",
};
Command::new(
get_var_if_exists("MUVM_UDEVD_PATH")
.unwrap_or_else(|| Ok(DEFAULT_UDEVD_PATH.to_owned()))?,
)
.spawn()
.context("Failed to execute `systemd-udevd` as a child process")?;
// SAFETY: We are single-threaded at this point
env::remove_var("MUVM_UDEVD_PATH");

if let Some(emulator) = options.emulator {
match emulator {
Emulator::Box => setup_box()?,
Emulator::Fex => setup_fex()?,
};
} else if let Err(err) = setup_fex() {
eprintln!("Error setting up FEX in binfmt_misc: {err}");
eprintln!("Failed to find or configure FEX, falling back to Box");

if let Err(err) = setup_box() {
eprintln!("Error setting up Box in binfmt_misc: {err}");
eprintln!("No emulators were configured, x86 emulation may not work");
} else {
#[cfg(target_arch = "aarch64")]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably also kill x11 forwarding in this case, it is aarch64 only. (Or fix it to work on x86 too, shouldn't be that hard)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, what's broken about it? I'll test it..

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, should do something like this: https://gist.github.com/WhatAmISupposedToPutHere/3b51d4266ca5d7b75f0571adafba031a

(build tested on x86 only, may or may not work)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a particular test case that triggers the ptrace code path? I've tried launching a bunch of stuff and it never got triggered:

Screenshot From 2025-09-19 02-39-36-min

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete this line, it should force it to use the ptrace method: https://github.com/AsahiLinux/muvm/blob/main/crates/muvm/src/guest/x11.rs#L24

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, now I do see my debug prints!

SYSC [9]
[user_regs_struct { r15: 657918200, r14: 0, r13: 0, r12: 140733902978120, rbp: 140733902978080, rbx: 657918176, r11: 582, r10: 17, r9: 0, r8: 9, rax: 9, rcx: 140671840240701, rdx: 3, rsi: 4096, rdi: 140671841247232, orig_rax: 7, rip: 140671848946042, cs: 51, eflags: 582, rsp: 140733902978048, ss: 43, fs_base: 140671842342720, gs_base: 0, ds: 0, es: 0, fs: 0, gs: 0 }]
SYSC [140671841247232]
[user_regs_struct { r15: 657918200, r14: 0, r13: 0, r12: 140733902978120, rbp: 140733902978080, rbx: 657918176, r11: 582, r10: 0, r9: 0, r8: 0, rax: 11, rcx: 140671840240701, rdx: 0, rsi: 4096, rdi: 140671783059456, orig_rax: 7, rip: 140671848946042, cs: 51, eflags: 582, rsp: 140733902978048, ss: 43, fs_base: 140671842342720, gs_base: 0, ds: 0, es: 0, fs: 0, gs: 0 }]
SYSC [0]

during initialization/resizing of GPU programs, and it still works fine \o/

if let Err(err) = setup_fex() {
eprintln!("Error setting up FEX in binfmt_misc: {err}");
eprintln!("Failed to find or configure FEX, falling back to Box64");

if let Err(err) = setup_box() {
eprintln!("Error setting up Box64 in binfmt_misc: {err}");
eprintln!("No emulators were configured, x86 emulation may not work");
}
}
}

for init_command in options.init_commands {
let code = Command::new(&init_command)
.current_dir(&options.cwd)
.spawn()?
.spawn()
.with_context(|| format!("Failed to execute init command {init_command:?}"))?
.wait()?;
if !code.success() {
return Err(anyhow!("Executing `{}` failed", init_command.display()));
Expand Down
85 changes: 70 additions & 15 deletions crates/muvm/src/guest/bridge/x11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,71 @@ const DRI3_OPCODE_PIXMAP_FROM_BUFFERS: u8 = 7;
const PRESENT_OPCODE_PRESENT_PIXMAP: u8 = 1;
pub const SHM_TEMPLATE: &str = "/dev/shm/krshm-XXXXXX";
pub const SHM_DIR: &str = "/dev/shm/";
const SYSCALL_INSTR: u32 = 0xd4000001;
static SYSCALL_OFFSET: OnceLock<usize> = OnceLock::new();
const CROSS_DOMAIN_CHANNEL_TYPE_X11: u32 = 0x11;
const CROSS_DOMAIN_ID_TYPE_SHM: u32 = 5;
const CROSS_DOMAIN_CMD_FUTEX_NEW: u8 = 8;
const CROSS_DOMAIN_CMD_FUTEX_SIGNAL: u8 = 9;
const CROSS_DOMAIN_CMD_FUTEX_DESTROY: u8 = 10;

#[cfg(target_arch = "aarch64")]
mod arch {
use nix::libc::{c_long, c_ulonglong, user_regs_struct};
pub type SyscallInstr = u32;
pub const SYSCALL_INSTR: SyscallInstr = 0xd4000001;

pub fn set_syscall_addr(regs: &mut user_regs_struct, syscall_addr: usize) {
regs.pc = syscall_addr as u64;
}

pub fn fill_syscall_args(
regs: &mut user_regs_struct,
syscall_no: c_long,
args: &[c_ulonglong; 6],
) {
regs.regs[..6].copy_from_slice(args);
regs.regs[8] = syscall_no as c_ulonglong;
}

pub fn get_syscall_result(regs: &user_regs_struct) -> c_ulonglong {
regs.regs[0]
}
}

#[cfg(target_arch = "x86_64")]
mod arch {
use nix::libc::{c_long, c_ulonglong, user_regs_struct};
pub type SyscallInstr = u16;
pub const SYSCALL_INSTR: SyscallInstr = 0x05_0f;

pub fn set_syscall_addr(regs: &mut user_regs_struct, syscall_addr: usize) {
regs.rip = syscall_addr as u64;
}

pub fn fill_syscall_args(
regs: &mut user_regs_struct,
syscall_no: c_long,
args: &[c_ulonglong; 6],
) {
regs.rdi = args[0];
regs.rsi = args[1];
regs.rdx = args[2];
regs.r10 = args[3];
regs.r8 = args[4];
regs.r9 = args[5];
regs.rax = syscall_no as c_ulonglong;
}

pub fn get_syscall_result(regs: &user_regs_struct) -> c_ulonglong {
regs.rax
}
}

#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
mod arch {
pub const SYSCALL_INSTR: u32 = 0xffffffff;
}

#[repr(C)]
#[derive(Debug, Default)]
struct ExportedHandle {
Expand Down Expand Up @@ -458,7 +515,7 @@ impl X11ProtocolHandler {
// Allow everything in /dev/shm (including paths with trailing '(deleted)')
let shmem_file = if filename.starts_with(SHM_DIR) {
File::from(memfd)
} else if cfg!(not(target_arch = "aarch64")) {
} else if cfg!(not(any(target_arch = "aarch64", target_arch = "x86_64"))) {
return Err(Errno::EOPNOTSUPP.into());
} else {
let (fd, shmem_path) = mkstemp(SHM_TEMPLATE)?;
Expand Down Expand Up @@ -638,8 +695,7 @@ struct RemoteCaller {
}

impl RemoteCaller {
// This is arch-specific, so gate it off of x86_64 builds done for CI purposes
#[cfg(target_arch = "aarch64")]
#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
fn with<R, F>(pid: Pid, f: F) -> Result<R>
where
F: FnOnce(&RemoteCaller) -> Result<R>,
Expand All @@ -651,7 +707,7 @@ impl RemoteCaller {
let syscall_addr = vdso_start + SYSCALL_OFFSET.get().unwrap();

let mut regs = old_regs;
regs.pc = syscall_addr as u64;
arch::set_syscall_addr(&mut regs, syscall_addr);
ptrace::setregs(pid, regs)?;
let res = f(&RemoteCaller { regs, pid })?;
ptrace::setregs(pid, old_regs)?;
Expand Down Expand Up @@ -711,30 +767,29 @@ impl RemoteCaller {
.map(|x| x as i32)
}

// This is arch-specific, so gate it off of x86_64 builds done for CI purposes
#[cfg(target_arch = "aarch64")]
#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
fn syscall(&self, syscall_no: c_long, args: [c_ulonglong; 6]) -> Result<c_ulonglong> {
let mut regs = self.regs;
regs.regs[..6].copy_from_slice(&args);
regs.regs[8] = syscall_no as c_ulonglong;
arch::fill_syscall_args(&mut regs, syscall_no, &args);
ptrace::setregs(self.pid, regs)?;
ptrace::step(self.pid, None)?;
let evt = waitpid(self.pid, Some(WaitPidFlag::__WALL))?;
if !matches!(evt, WaitStatus::Stopped(_, _)) {
unimplemented!();
}
regs = ptrace::getregs(self.pid)?;
Ok(regs.regs[0])
Ok(arch::get_syscall_result(&regs))
}

#[cfg(not(target_arch = "aarch64"))]
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
fn with<R, F>(_pid: Pid, _f: F) -> Result<R>
where
F: FnOnce(&RemoteCaller) -> Result<R>,
{
Err(Errno::EOPNOTSUPP.into())
}
#[cfg(not(target_arch = "aarch64"))]

#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
fn syscall(&self, _syscall_no: c_long, _args: [c_ulonglong; 6]) -> Result<c_ulonglong> {
Err(Errno::EOPNOTSUPP.into())
}
Expand Down Expand Up @@ -799,10 +854,10 @@ pub fn start_x11bridge(display: u32) {
// the same vDSO (which should be true if they are running under the same
// kernel!)
let (vdso_start, vdso_end) = find_vdso(None).unwrap();
for off in (0..(vdso_end - vdso_start)).step_by(4) {
for off in (0..(vdso_end - vdso_start)).step_by(mem::size_of::<arch::SyscallInstr>()) {
let addr = vdso_start + off;
let val = unsafe { std::ptr::read(addr as *const u32) };
if val == SYSCALL_INSTR {
let val = unsafe { std::ptr::read(addr as *const arch::SyscallInstr) };
if val == arch::SYSCALL_INSTR {
SYSCALL_OFFSET.set(off).unwrap();
break;
}
Expand Down
80 changes: 47 additions & 33 deletions crates/muvm/src/guest/server_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use crate::utils::stdio::make_stdout_stderr;
use crate::utils::tty::*;

pub enum ConnRequest {
DropCaches,
HandledByBuiltin,
ExecuteCommand {
command: PathBuf,
child: Child,
Expand Down Expand Up @@ -83,7 +83,7 @@ impl Worker {

match handle_connection(stream).await {
Ok(request) => match request {
ConnRequest::DropCaches => {},
ConnRequest::HandledByBuiltin => {},
ConnRequest::ExecuteCommand {command, mut child, stop_pipe } => {
self.child_set.spawn(async move { (command, child.wait().await, stop_pipe) });
self.set_child_processes(self.child_set.len());
Expand Down Expand Up @@ -190,6 +190,41 @@ async fn read_request(stream: &mut BufStream<UnixStream>) -> Result<Launch> {
}
}

async fn write_to(
mut stream: BufStream<UnixStream>,
path: &'static std::ffi::CStr,
data: &[u8],
) -> Result<ConnRequest> {
// SAFETY: `open` and `write` are async signal safe
let code = unsafe {
user::run_as_root(|| {
let fd = nix::libc::open(path.as_ptr(), nix::libc::O_WRONLY);
if fd < 0 {
return 1;
}
let written = nix::libc::write(fd, data.as_ptr() as *const _, data.len()) as usize;
if written == data.len() {
0
} else {
2
}
})
.with_context(|| format!("Failed to write to {path:?}"))?
};
match code {
0 => {
stream.write_all(b"OK").await.ok();
stream.flush().await.ok();
Ok(ConnRequest::HandledByBuiltin)
},
1 => Err(anyhow!("Failed to open {path:?} for writing")),
2 => Err(anyhow!("Failed to write to {path:?}")),
err => Err(anyhow!(
"Unexpected return status when attempting to write to {path:?}: {err}"
)),
}
}

async fn handle_connection(mut stream: BufStream<UnixStream>) -> Result<ConnRequest> {
let mut envs: HashMap<String, String> = env::vars().collect();

Expand All @@ -204,38 +239,17 @@ async fn handle_connection(mut stream: BufStream<UnixStream>) -> Result<ConnRequ
debug!(command:?, command_args:?, env:?; "received launch request");

if command == Path::new("/muvmdropcaches") {
// SAFETY: everything below should be async signal safe
let code = unsafe {
user::run_as_root(|| {
let fd = nix::libc::open(c"/proc/sys/vm/drop_caches".as_ptr(), nix::libc::O_WRONLY);
if fd < 0 {
return 1;
}
let data = b"1";
let written = nix::libc::write(fd, data.as_ptr() as *const _, data.len()) as usize;
if written == data.len() {
0
} else {
2
}
})
.context("Failed to drop caches")?
};
return match code {
0 => {
stream.write_all(b"OK").await.ok();
stream.flush().await.ok();
Ok(ConnRequest::DropCaches)
},
1 => Err(anyhow!(
"Failed to open /proc/sys/vm/drop_caches for writing"
)),
2 => Err(anyhow!("Failed to write to /proc/sys/vm/drop_caches")),
e => Err(anyhow!(
"Unexpected return status when attempting to drop caches: {}",
e
)),
return write_to(stream, c"/proc/sys/vm/drop_caches", b"1").await;
} else if command == Path::new("/muvmwatermarkscalefactor") {
let Some(data) = command_args.first() else {
return Err(anyhow!("muvmwatermarkscalefactor: missing arg"));
};
return write_to(
stream,
c"/proc/sys/vm/watermark_scale_factor",
data.as_bytes(),
)
.await;
}

envs.extend(env);
Expand Down
2 changes: 1 addition & 1 deletion crates/muvm/src/guest/x11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ where

if let Ok(xauthority) = std::env::var("XAUTHORITY") {
let src_path = format!("/run/muvm-host/{xauthority}");
let mut rdr = File::open(src_path)?;
let mut rdr = File::open(src_path).context("Failed to open XAUTHORITY")?;

let dst_path = run_path.as_ref().join("xauth");
let mut wtr = File::options()
Expand Down
4 changes: 2 additions & 2 deletions crates/muvm/src/monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ fn set_guest_pressure(pressure: GuestPressure) -> Result<()> {
let wsf: u32 = pressure.into();
debug!("setting watermark_scale_factor to {wsf}");

let command = PathBuf::from("/sbin/sysctl");
let command_args = vec![format!("vm.watermark_scale_factor={}", wsf)];
let command = PathBuf::from("/muvmwatermarkscalefactor");
let command_args = vec![format!("{wsf}")];
let env = HashMap::new();
request_launch(command, command_args, env, 0, false, true)
}
Expand Down
8 changes: 8 additions & 0 deletions crates/muvm/src/utils/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,11 @@ where

Ok(None)
}

pub fn get_var_if_exists(name: &str) -> Option<Result<String>> {
match env::var(name) {
Ok(val) => Some(Ok(val)),
Err(env::VarError::NotPresent) => None,
Err(e) => Some(Err(e.into())),
}
}
Loading