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
2 changes: 1 addition & 1 deletion crates/muvm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ serde = { version = "1.0.203", default-features = false, features = ["derive"] }
serde_json = { version = "1.0.117", default-features = false, features = ["std"] }
shell-words = { version = "1.1.1", default-features = false, features = ["std"] }
tempfile = { version = "3.10.1", default-features = false, features = [] }
tokio = { version = "1.38.0", default-features = false, features = ["io-util", "macros", "net", "process", "rt-multi-thread", "sync"] }
tokio = { version = "1.38.0", default-features = false, features = ["io-util", "macros", "net", "process", "rt-multi-thread", "sync", "time"] }
tokio-stream = { version = "0.1.15", default-features = false, features = ["net", "sync"] }
udev = { version = "0.9.0", default-features = false, features = [] }
uuid = { version = "1.10.0", default-features = false, features = ["serde", "std", "v7"] }
Expand Down
211 changes: 211 additions & 0 deletions crates/muvm/src/atspi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
use std::collections::HashMap;
use std::env;
use std::ffi::OsString;
use std::fmt::Write as _;
use std::fs;
use std::io::ErrorKind;
use std::os::fd::AsRawFd;
use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
use std::os::unix::fs::symlink;
use std::path::{Component, Path, PathBuf};
use std::process::Command;

use anyhow::{bail, Context, Result};
use log::debug;
use nix::sys::socket::{connect, socket, AddressFamily, SockFlag, SockType, UnixAddr};

pub struct HostBus {
path: PathBuf,
alias: bool,
}

impl HostBus {
fn new(path: PathBuf, runtime_dir: &Path) -> Result<Self> {
if path.to_str().is_some() {
return Ok(Self { path, alias: false });
}

// libkrun requires UTF-8 even though Unix sockets accept arbitrary bytes.
// Its normal shutdown uses _exit, so reuse one runtime-directory alias
// under the launcher's VM lock instead of leaking a temporary directory.
let dir = runtime_dir.join("krun");
let alias_path = dir.join("atspi");
if alias_path.to_str().is_none() {
bail!("AT-SPI host alias path is not UTF-8");
}
fs::create_dir_all(dir)?;
match fs::symlink_metadata(&alias_path) {
Ok(meta) if meta.file_type().is_symlink() => fs::remove_file(&alias_path)?,
Ok(_) => bail!("AT-SPI host alias {alias_path:?} already exists and is not a symlink"),
Err(err) if err.kind() == ErrorKind::NotFound => {},
Err(err) => return Err(err.into()),
}
symlink(std::path::absolute(&path)?, &alias_path)?;
Ok(Self {
path: alias_path,
alias: true,
})
}

pub fn path(&self) -> &Path {
&self.path
}
}

impl Drop for HostBus {
fn drop(&mut self) {
if self.alias {
// Clean up on startup errors; normal shutdown leaves the reusable
// symlink for the next launch or XDG_RUNTIME_DIR session cleanup.
let _ = fs::remove_file(&self.path);
}
}
}

fn parse_unix_path_address(address: &str) -> Option<PathBuf> {
if address.contains(';') {
return None;
}
let pairs = address.strip_prefix("unix:")?;
let mut path = None;
for pair in pairs.split(',') {
let (key, value) = pair.split_once('=')?;
if key == "path" {
path = Some(percent_decode(value)?);
}
}
path.filter(|p| !p.is_empty()).map(PathBuf::from)
}

fn percent_decode(value: &str) -> Option<OsString> {
let bytes = value.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' {
let hex = bytes.get(i + 1..i + 3)?;
let hex = std::str::from_utf8(hex).ok()?;
out.push(u8::from_str_radix(hex, 16).ok()?);
i += 3;
} else {
out.push(bytes[i]);
i += 1;
}
}
Some(OsString::from_vec(out))
}

pub fn percent_encode(path: &Path) -> String {
let mut out = String::new();
for &b in path.as_os_str().as_bytes() {
if b.is_ascii_alphanumeric() || b"-_/.\\*".contains(&b) {
out.push(b as char);
} else {
write!(out, "%{b:02X}").unwrap();
}
}
out
}

pub fn guest_socket_path(host_path: &Path, uid: u32) -> PathBuf {
// Preserve the path advertised by the host's AT_SPI_BUS X11 property.
// Non-UTF-8 paths need an alias because the guest configuration uses JSON.
let runtime_dir = PathBuf::from(format!("/run/user/{uid}"));
if host_path.starts_with(&runtime_dir)
&& host_path.to_str().is_some()
&& !host_path
.components()
.any(|part| part == Component::ParentDir)
{
host_path.to_path_buf()
} else {
runtime_dir.join("at-spi").join("bus_0")
}
}

pub fn configure_guest_env(env: &mut HashMap<String, String>, guest_path: &Path) {
env.entry("AT_SPI_BUS_ADDRESS".to_owned())
.or_insert_with(|| format!("unix:path={}", percent_encode(guest_path)));
// The host cannot reach guest application sockets.
env.entry("ATSPI_DISABLE_P2P".to_owned())
.or_insert_with(|| "1".to_owned());
// Firefox otherwise waits for org.a11y.Status on the unbridged session bus.
env.entry("GNOME_ACCESSIBILITY".to_owned())
.or_insert_with(|| "1".to_owned());
}

fn can_connect(path: &Path) -> bool {
// A full listen queue must not block startup.
let result = UnixAddr::new(path).and_then(|address| {
let fd = socket(
AddressFamily::Unix,
SockType::Stream,
SockFlag::SOCK_CLOEXEC | SockFlag::SOCK_NONBLOCK,
None,
)?;
connect(fd.as_raw_fd(), &address)
});
if let Err(err) = result {
debug!(path:?, err:?; "AT-SPI bus socket is unavailable");
}
result.is_ok()
}

fn query_host_bus_address() -> Result<String> {
let output = Command::new("busctl")
.args([
"--user",
"--timeout=5",
"--json=short",
"call",
"org.a11y.Bus",
"/org/a11y/bus",
"org.a11y.Bus",
"GetAddress",
])
.output()
.context("failed to run busctl")?;
if !output.status.success() {
bail!(
"GetAddress failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
);
}
let reply: serde_json::Value = serde_json::from_slice(&output.stdout)?;
reply["data"][0]
.as_str()
.map(str::to_owned)
.context("GetAddress did not return an address")
}

fn path_from_address(source: &str, address: &str) -> Option<PathBuf> {
let path = parse_unix_path_address(address);
if path.is_none() {
eprintln!("AT-SPI bus address {address:?} from {source} is not a single unix:path address");
}
path
}

pub fn discover_host_bus(runtime_dir: &Path) -> Option<HostBus> {
let (source, address) = match env::var("AT_SPI_BUS_ADDRESS")
.ok()
.filter(|a| !a.is_empty())
{
Some(address) => ("AT_SPI_BUS_ADDRESS", address),
None => match query_host_bus_address() {
Ok(address) => ("org.a11y.Bus.GetAddress", address),
Err(err) => {
debug!(err:?; "could not discover AT-SPI bus, accessibility bridge disabled");
return None;
},
},
};
let path = path_from_address(source, &address).filter(|path| can_connect(path))?;
HostBus::new(path, runtime_dir)
.inspect_err(|err| {
eprintln!(
"Failed to prepare host AT-SPI socket, accessibility bridge disabled: {err:#}"
);
})
.ok()
}
29 changes: 27 additions & 2 deletions crates/muvm/src/bin/muvm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::collections::HashMap;
use std::ffi::{c_char, CString};
use std::io::Write;
use std::os::fd::{IntoRawFd, OwnedFd};
use std::os::unix::ffi::OsStrExt as _;
use std::path::Path;
use std::process::ExitCode;
use std::{env, fs};
Expand All @@ -15,6 +16,7 @@ use krun_sys::{
VIRGLRENDERER_USE_ASYNC_FENCE_CB, VIRGLRENDERER_USE_EGL, VIRGLRENDERER_VENUS,
};
use log::debug;
use muvm::atspi::{configure_guest_env, discover_host_bus, guest_socket_path};
use muvm::cli_options::options;
use muvm::config::Configuration;
use muvm::cpu::{get_fallback_cores, get_performance_cores};
Expand All @@ -25,7 +27,7 @@ use muvm::monitor::spawn_monitor;
use muvm::net::{connect_to_passt, start_passt};
use muvm::types::MiB;
use muvm::utils::launch::{
GuestConfiguration, Launch, HIDPIPE_SOCKET, MUVM_GUEST_SOCKET, PULSE_SOCKET,
GuestConfiguration, Launch, ATSPI_SOCKET, HIDPIPE_SOCKET, MUVM_GUEST_SOCKET, PULSE_SOCKET,
};
use nix::sys::sysinfo::sysinfo;
use nix::unistd::User;
Expand Down Expand Up @@ -352,7 +354,7 @@ fn main() -> Result<ExitCode> {
}
}

if let Some(run_path) = xdg_runtime_dir {
if let Some(run_path) = &xdg_runtime_dir {
let hidpipe_path = Path::new(&run_path).join("hidpipe");
spawn_hidpipe_server(hidpipe_path.clone()).context("Failed to spawn hidpipe thread")?;
let hidpipe_path = CString::new(
Expand Down Expand Up @@ -406,6 +408,28 @@ fn main() -> Result<ExitCode> {
}

let uid = getuid().as_raw();

// Retain any host socket alias until the VM exits.
let host_bus = xdg_runtime_dir
.as_deref()
.and_then(|path| discover_host_bus(Path::new(path)));
let atspi_socket = if let Some(host_bus) = &host_bus {
let host_path = host_bus.path();
let host_path_c = CString::new(host_path.as_os_str().as_bytes())
.context("Failed to process AT-SPI bus path as it contains NUL character")?;
// SAFETY: host_path_c is a pointer to a CString with long enough lifetime.
let err = unsafe { krun_add_vsock_port(ctx_id, ATSPI_SOCKET, host_path_c.as_ptr()) };
if err < 0 {
let err = Errno::from_raw_os_error(-err);
return Err(err).context("Failed to configure vsock for AT-SPI socket");
}
let guest_path = guest_socket_path(host_path, uid);
configure_guest_env(&mut env, &guest_path);
debug!(host:? = host_path, guest:? = guest_path; "AT-SPI bridge");
Some(guest_path)
} else {
None
};
let user = User::from_uid(uid.into())
.map_err(Into::into)
.and_then(|user| user.ok_or_else(|| anyhow!("requested entry not found")))
Expand Down Expand Up @@ -458,6 +482,7 @@ fn main() -> Result<ExitCode> {
cwd,
init_commands,
user_init_commands: options.user_init_commands,
atspi_socket,
};
let mut muvm_config_file = NamedTempFile::new()
.context("Failed to create a temporary file to store the muvm guest config")?;
Expand Down
Loading
Loading