feat(krun): dlopen bindings to the stable ABI

Eleven calls from the stable-1.19.x headers plus krun_free_ctx, loaded
at runtime so a missing install names the remediation instead of failing
to link. Symbol lookup goes through a macro that carries the signature
from libkrun.h, every unsafe block states its invariant, and negative
returns map onto errno errors. The KVM probe ports ecr's open-then-
KVM_GET_API_VERSION check and reports KvmUnavailable with the full
remediation text.
This commit is contained in:
Valentin Haudiquet committed 2026-10-27 22:31:35 +01:00
1 parent 9855776847
commit 18d81c2e7d
2 files changed
+368

No files matched your search

+367
View File
@@ -0,0 +1,367 @@
//! Runtime bindings to libkrun's stable C ABI.
//!
//! The library (`libkrun.so.1`) loads through dlopen so a missing
//! install is a remediation message rather than a link error. The
//! symbol signatures mirror the `stable-1.19.x` headers; the main
//! branch is mid-rewrite to a vtable API and is not the target.
//!
//! The founding rule holds for this module too: libkrun's VMM runs in
//! our process and touches only files, sockets, and `/dev/kvm`, all
//! inside the confinement budget (SPEC.md).
use std::ffi::{c_char, CString};
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use anyhow::{anyhow, Context, Result};
use crate::Error;
/// The ABI version libkrun reports for a working `/dev/kvm`
/// (`KVM_GET_API_VERSION`, the first request of the KVMIO range).
const KVM_GET_API_VERSION: libc::c_ulong = 0xAE00;
const KVM_API_VERSION_EXPECTED: libc::c_int = 12;
pub const SONAME: &str = "libkrun.so.1";
/// Remediation shown when libkrun cannot be loaded: distro packaging is
/// still sparse (Fedora carries it; Debian and Ubuntu do not yet).
const LOAD_REMEDIATION: &str = "install libkrun, or point LD_LIBRARY_PATH \
at the directory holding it (a snap stages it next to the binary)";
macro_rules! symbol {
($lib:expr, $name:expr, $ty:ty) => {{
// Safety: the symbol is used through the library handle with the
// signature from libkrun.h written at the call site.
unsafe { $lib.get::<$ty>($name) }.with_context(|| {
format!(
"symbol {} missing: libkrun too old?",
String::from_utf8_lossy($name)
)
})?
}};
}
/// Map a negative libkrun return (a negated errno) onto an error.
fn krun_ret(ret: i32, name: &str) -> Result<()> {
if ret < 0 {
Err(anyhow::Error::from(std::io::Error::from_raw_os_error(-ret)))
.with_context(|| format!("{name} failed"))
} else {
Ok(())
}
}
/// An open libkrun library handle. Methods map one-to-one onto the C
/// API; negative returns become errno errors with the symbol named.
pub struct Krun {
lib: libloading::Library,
}
impl Krun {
/// dlopen libkrun, failing with the install remediation.
pub fn load() -> Result<Self> {
// Safety: dlopen of a fixed SONAME; the handle lives in `self`
// and every symbol below is used through it.
let lib = unsafe { libloading::Library::new(SONAME) }
.with_context(|| format!("cannot load {SONAME}: {LOAD_REMEDIATION}"))?;
Ok(Krun { lib })
}
/// `krun_set_log_level`
pub fn set_log_level(&self, level: u32) -> Result<()> {
let f = symbol!(
self.lib,
b"krun_set_log_level\0",
unsafe extern "C" fn(u32) -> i32
);
krun_ret(unsafe { f(level) }, "krun_set_log_level")
}
/// `krun_create_ctx`, returning the new context id.
pub fn create_ctx(&self) -> Result<u32> {
let f = symbol!(
self.lib,
b"krun_create_ctx\0",
unsafe extern "C" fn() -> i32
);
let ret = unsafe { f() };
if ret < 0 {
krun_ret(ret, "krun_create_ctx")?;
unreachable!("krun_create_ctx returns a ctx id or a negative errno");
}
Ok(ret as u32)
}
/// `krun_free_ctx`
pub fn free_ctx(&self, ctx_id: u32) -> Result<()> {
let f = symbol!(
self.lib,
b"krun_free_ctx\0",
unsafe extern "C" fn(u32) -> i32
);
krun_ret(unsafe { f(ctx_id) }, "krun_free_ctx")
}
/// `krun_set_vm_config`
pub fn set_vm_config(&self, ctx_id: u32, vcpus: u8, ram_mib: u32) -> Result<()> {
let f = symbol!(
self.lib,
b"krun_set_vm_config\0",
unsafe extern "C" fn(u32, u8, u32) -> i32
);
krun_ret(unsafe { f(ctx_id, vcpus, ram_mib) }, "krun_set_vm_config")
}
/// `krun_set_root`: the virtiofs share that becomes the guest root.
pub fn set_root(&self, ctx_id: u32, root: &Path) -> Result<()> {
let root_c = path_cstring(root)?;
let f = symbol!(
self.lib,
b"krun_set_root\0",
unsafe extern "C" fn(u32, *const c_char) -> i32
);
// Safety: root_c is NUL-terminated and outlives the call.
krun_ret(unsafe { f(ctx_id, root_c.as_ptr()) }, "krun_set_root")
}
/// `krun_add_virtiofs`: an extra tagged share, mounted guest-side.
pub fn add_virtiofs(&self, ctx_id: u32, tag: &str, path: &Path) -> Result<()> {
let tag_c = CString::new(tag).context("share tag contains a NUL")?;
let path_c = path_cstring(path)?;
let f = symbol!(
self.lib,
b"krun_add_virtiofs\0",
unsafe extern "C" fn(u32, *const c_char, *const c_char) -> i32
);
// Safety: both CStrings are NUL-terminated and outlive the call.
krun_ret(
unsafe { f(ctx_id, tag_c.as_ptr(), path_c.as_ptr()) },
"krun_add_virtiofs",
)
}
/// `krun_set_workdir`
pub fn set_workdir(&self, ctx_id: u32, workdir: &str) -> Result<()> {
let workdir_c = CString::new(workdir).context("workdir contains a NUL")?;
let f = symbol!(
self.lib,
b"krun_set_workdir\0",
unsafe extern "C" fn(u32, *const c_char) -> i32
);
// Safety: workdir_c is NUL-terminated and outlives the call.
krun_ret(unsafe { f(ctx_id, workdir_c.as_ptr()) }, "krun_set_workdir")
}
/// `krun_set_exec`: the payload path (relative to the guest root),
/// its argv, and its envp. A NULL envp would inherit the host
/// environment, which ecrou never does; callers always pass one.
pub fn set_exec(
&self,
ctx_id: u32,
exec_path: &str,
argv: &[String],
envp: &[String],
) -> Result<()> {
let path_c = CString::new(exec_path).context("exec path contains a NUL")?;
let argv_c = CArgs::build(argv)?;
let envp_c = CArgs::build(envp)?;
let f = symbol!(
self.lib,
b"krun_set_exec\0",
unsafe extern "C" fn(
u32,
*const c_char,
*const *const c_char,
*const *const c_char,
) -> i32
);
// Safety: path_c and both pointer arrays are NUL-terminated and
// outlive the call; the pointed-to CStrings live in the CArgs
// values held across it.
krun_ret(
unsafe { f(ctx_id, path_c.as_ptr(), argv_c.ptr(), envp_c.ptr()) },
"krun_set_exec",
)
}
/// `krun_set_env`: extra environment for the payload.
pub fn set_env(&self, ctx_id: u32, envp: &[String]) -> Result<()> {
let envp_c = CArgs::build(envp)?;
let f = symbol!(
self.lib,
b"krun_set_env\0",
unsafe extern "C" fn(u32, *const *const c_char) -> i32
);
// Safety: envp_c is NUL-terminated and outlives the call.
krun_ret(unsafe { f(ctx_id, envp_c.ptr()) }, "krun_set_env")
}
/// `krun_set_console_output`: redirect the guest console to a file.
pub fn set_console_output(&self, ctx_id: u32, path: &Path) -> Result<()> {
let path_c = path_cstring(path)?;
let f = symbol!(
self.lib,
b"krun_set_console_output\0",
unsafe extern "C" fn(u32, *const c_char) -> i32
);
// Safety: path_c is NUL-terminated and outlives the call.
krun_ret(
unsafe { f(ctx_id, path_c.as_ptr()) },
"krun_set_console_output",
)
}
/// `krun_start_enter`: enter the VM loop on the calling thread.
/// Blocks until the guest powers off.
pub fn start_enter(&self, ctx_id: u32) -> Result<()> {
let f = symbol!(
self.lib,
b"krun_start_enter\0",
unsafe extern "C" fn(u32) -> i32
);
krun_ret(unsafe { f(ctx_id) }, "krun_start_enter")
}
/// `krun_check_nested_virt`: true when nested virtualization is
/// available.
pub fn check_nested_virt(&self) -> Result<bool> {
let f = symbol!(
self.lib,
b"krun_check_nested_virt\0",
unsafe extern "C" fn() -> i32
);
let ret = unsafe { f() };
if ret < 0 {
krun_ret(ret, "krun_check_nested_virt")?;
unreachable!("krun_check_nested_virt returns 0/1 or a negative errno");
}
Ok(ret == 1)
}
}
/// NUL-terminated pointer array for argv/envp-style C arguments. The
/// strings live as long as the value, so the caller holds it across the
/// FFI call.
struct CArgs {
// Holds the CStrings the pointer array points into; never read
// directly, but dropping it would dangle every pointer.
#[allow(dead_code)]
strings: Vec<CString>,
ptrs: Vec<*const c_char>,
}
impl CArgs {
fn build(items: &[String]) -> Result<Self> {
let strings = items
.iter()
.map(|s| CString::new(s.as_str()).context("argument contains a NUL"))
.collect::<Result<Vec<_>>>()?;
let mut ptrs = strings.iter().map(|c| c.as_ptr()).collect::<Vec<_>>();
ptrs.push(std::ptr::null());
Ok(CArgs { strings, ptrs })
}
fn ptr(&self) -> *const *const c_char {
self.ptrs.as_ptr()
}
}
fn path_cstring(path: &Path) -> Result<CString> {
CString::new(path.as_os_str().as_bytes())
.with_context(|| format!("path contains a NUL: {}", path.display()))
}
/// Probe KVM the crun way: open `/dev/kvm` read-write, then check
/// `KVM_GET_API_VERSION` returns 12.
pub fn kvm_probe() -> Result<()> {
use std::os::unix::io::AsRawFd;
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/kvm")
.map_err(|err| anyhow::Error::from(err).context(Error::KvmUnavailable.to_string()))?;
// Safety: the fd is open and an ioctl on /dev/kvm with
// KVM_GET_API_VERSION reads nothing; only the kernel writes the int
// return.
let ret = unsafe { libc::ioctl(file.as_raw_fd(), KVM_GET_API_VERSION) };
if ret != KVM_API_VERSION_EXPECTED {
return Err(anyhow!(
"/dev/kvm reports API version {}, expected {}; the kernel module \
and the userspace disagree",
ret,
KVM_API_VERSION_EXPECTED
));
}
Ok(())
}
/// True when a KVM probe passes; `ecrou probe` and tests use this.
pub fn kvm_available() -> bool {
kvm_probe().is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kvm_probe_constant_matches_kvm_abi() {
// KVMIO base 0xAE; KVM_GET_API_VERSION is the first request.
assert_eq!(KVM_GET_API_VERSION, 0xAE00);
assert_eq!(KVM_API_VERSION_EXPECTED, 12);
}
#[test]
fn kvm_probe_reports_unavailability_or_works() {
match kvm_probe() {
Ok(()) => {}
Err(err) => {
let msg = format!("{err:#}");
assert!(msg.contains("/dev/kvm"), "unhelpful probe error: {msg}");
assert!(msg.contains("usermod"), "remediation missing: {msg}");
}
}
}
#[test]
fn load_fails_with_remediation_or_round_trips() {
match Krun::load() {
Ok(krun) => {
// A real libkrun: context create/free round-trips.
let ctx = krun.create_ctx().expect("create_ctx on real libkrun");
krun.free_ctx(ctx).expect("free_ctx");
}
Err(err) => {
let msg = format!("{err:#}");
assert!(msg.contains(SONAME), "bad load error: {msg}");
assert!(
msg.contains("LD_LIBRARY_PATH"),
"remediation missing: {msg}"
);
}
}
}
#[test]
fn ptr_array_is_null_terminated() {
let args = CArgs::build(&["a".to_string(), "b".to_string()]).unwrap();
assert_eq!(args.ptrs.len(), 3);
assert!(args.ptrs[2].is_null());
assert_eq!(args.strings[0].to_str().unwrap(), "a");
}
#[test]
fn ptr_array_rejects_nul_bytes() {
let items = vec!["bad\0arg".to_string()];
assert!(CArgs::build(&items).is_err());
}
#[test]
fn krun_ret_maps_negated_errno() {
let err = krun_ret(-2, "krun_set_root").unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("krun_set_root failed"), "{msg}");
assert!(msg.contains("No such file"), "{msg}");
}
}
+1
View File
@@ -26,6 +26,7 @@ pub mod distro;
pub mod download; pub mod download;
pub mod error; pub mod error;
pub mod extract; pub mod extract;
pub mod krun;
pub mod progress; pub mod progress;
pub mod rootfs; pub mod rootfs;
pub mod utils; pub mod utils;