Skip to content

Add krun_set_rootfs_kernel_args API #346

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
21 changes: 21 additions & 0 deletions include/libkrun.h
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,27 @@ int32_t krun_set_kernel(uint32_t ctx_id,
*/
int32_t krun_set_env(uint32_t ctx_id, const char *const envp[]);

/**
* Sets the kernel boot arguments for the root filesystem.
*
* Arguments:
* "ctx_id" - the configuration context ID.
* "device" - the device on which the root filesystem should be found. If NULL, it will default
* to "/dev/vda1".
* "fs_type" - the type of the filesystem on the "device" device. If NULL, it will default to
* "virtiofs".
* "mount_flags" - additional mount flags to be used when mounting the root filesystem.
* "read_only" - when "true", mount the root filesystem read-only, otherwise mount rw.
*
* Returns:
* Zero on success or a negative error number on failure.
*/
int32_t krun_set_rootfs_kernel_args(uint32_t ctx_id,
const char *device,
const char *fs_type,
const char *mount_flags,
bool read_only);

/**
* Sets the file path to the TEE configuration file. Only available in libkrun-sev.
*
Expand Down
109 changes: 108 additions & 1 deletion src/libkrun/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ use vmm::vmm_config::kernel_bundle::{InitrdBundle, QbootBundle};
use vmm::vmm_config::machine_config::VmConfig;
#[cfg(feature = "net")]
use vmm::vmm_config::net::NetworkInterfaceConfig;
use vmm::vmm_config::rootfs::RootfsConfig;
use vmm::vmm_config::vsock::VsockDeviceConfig;

// Value returned on success. We use libc's errors otherwise.
Expand Down Expand Up @@ -151,6 +152,8 @@ struct ContextConfig {
console_output: Option<PathBuf>,
vmm_uid: Option<libc::uid_t>,
vmm_gid: Option<libc::gid_t>,
// Custom rootfs kernel arguments
rootfs_config: Option<RootfsConfig>,
}

impl ContextConfig {
Expand Down Expand Up @@ -249,6 +252,43 @@ impl ContextConfig {
self.mac = Some(mac);
}

fn set_rootfs_config(&mut self, config: RootfsConfig) {
self.rootfs_config = Some(config);
}

fn get_rootfs_config(&self) -> Option<&RootfsConfig> {
self.rootfs_config.as_ref()
}

fn get_effective_kernel_cmdline(&self) -> String {
if let Some(rootfs_config) = self.get_rootfs_config() {
// Replace rootfs-related parameters in DEFAULT_KERNEL_CMDLINE with custom ones
// DEFAULT_KERNEL_CMDLINE contains: "reboot=k panic=-1 panic_print=0 nomodule console=hvc0 rootfstype=virtiofs rw quiet no-kvmapf"
// We need to replace "rootfstype=virtiofs rw" with the custom args
let parts: Vec<&str> = DEFAULT_KERNEL_CMDLINE.split_whitespace().collect();
let mut filtered_parts = Vec::new();

for part in parts {
// Skip existing rootfs-related parameters that we want to override
if !part.starts_with("rootfstype=")
&& !part.starts_with("root=")
&& !part.starts_with("rootflags=")
&& part != "ro"
&& part != "rw"
{
filtered_parts.push(part);
}
}

// Add custom rootfs arguments
let rootfs_args = rootfs_config.to_kernel_args();
filtered_parts.push(&rootfs_args);
filtered_parts.join(" ")
} else {
DEFAULT_KERNEL_CMDLINE.to_string()
}
}

fn set_port_map(&mut self, new_port_map: HashMap<u16, u16>) -> Result<(), ()> {
match &mut self.net_cfg {
NetworkConfig::Tsi(tsi_config) => {
Expand Down Expand Up @@ -945,6 +985,73 @@ pub unsafe extern "C" fn krun_set_env(ctx_id: u32, c_envp: *const *const c_char)
KRUN_SUCCESS
}

#[allow(clippy::missing_safety_doc)]
#[no_mangle]
pub unsafe extern "C" fn krun_set_rootfs_kernel_args(
ctx_id: u32,
c_device: *const c_char,
c_fs_type: *const c_char,
c_mount_flags: *const c_char,
read_only: bool,
) -> i32 {
let mut config = RootfsConfig::new();

// Set device if provided
if !c_device.is_null() {
let device = match CStr::from_ptr(c_device).to_str() {
Ok(s) => s,
Err(_) => return -libc::EINVAL,
};
if !device.is_empty() {
config = match config.with_device(device) {
Ok(cfg) => cfg,
Err(_) => return -libc::EINVAL,
};
}
}

// Set filesystem type if provided
if !c_fs_type.is_null() {
let fs_type = match CStr::from_ptr(c_fs_type).to_str() {
Ok(s) => s,
Err(_) => return -libc::EINVAL,
};
if !fs_type.is_empty() {
config = match config.with_fs_type(fs_type) {
Ok(cfg) => cfg,
Err(_) => return -libc::EINVAL,
};
}
}

// Set mount flags if provided
if !c_mount_flags.is_null() {
let mount_flags = match CStr::from_ptr(c_mount_flags).to_str() {
Ok(s) => s,
Err(_) => return -libc::EINVAL,
};
if !mount_flags.is_empty() {
config = match config.with_mount_flags(mount_flags) {
Ok(cfg) => cfg,
Err(_) => return -libc::EINVAL,
};
}
}

// Set read-only flag
config = config.with_read_only(read_only);

match CTX_MAP.lock().unwrap().entry(ctx_id) {
Entry::Occupied(mut ctx_cfg) => {
let cfg = ctx_cfg.get_mut();
cfg.set_rootfs_config(config);
}
Entry::Vacant(_) => return -libc::ENOENT,
}

KRUN_SUCCESS
}

#[allow(clippy::missing_safety_doc)]
#[no_mangle]
#[cfg(feature = "tee")]
Expand Down Expand Up @@ -1464,7 +1571,7 @@ pub extern "C" fn krun_start_enter(ctx_id: u32) -> i32 {
let boot_source = BootSourceConfig {
kernel_cmdline_prolog: Some(format!(
"{} init={} {} {} {} {}",
DEFAULT_KERNEL_CMDLINE,
ctx_cfg.get_effective_kernel_cmdline(),
INIT_PATH,
ctx_cfg.get_exec_path(),
ctx_cfg.get_workdir(),
Expand Down
3 changes: 3 additions & 0 deletions src/vmm/src/vmm_config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ pub mod boot_source;
/// Wrapper for configuring an external kernel to be loaded in the microVM.
pub mod external_kernel;

/// Wrapper for configuring the root filesystem kernel arguments.
pub mod rootfs;

/// Wrapper for configuring the Fs devices attached to the microVM.
#[cfg(not(feature = "tee"))]
pub mod fs;
Expand Down
198 changes: 198 additions & 0 deletions src/vmm/src/vmm_config/rootfs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use std::fmt;

/// Errors associated with actions on `RootfsConfig`.
#[derive(Debug)]
pub enum RootfsConfigError {
/// Invalid device specification.
InvalidDevice(String),
/// Invalid filesystem type.
InvalidFsType(String),
/// Invalid mount flags.
InvalidMountFlags(String),
}

impl fmt::Display for RootfsConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::RootfsConfigError::*;
match *self {
InvalidDevice(ref e) => write!(f, "Invalid device specification: {}", e),
InvalidFsType(ref e) => write!(f, "Invalid filesystem type: {}", e),
InvalidMountFlags(ref e) => write!(f, "Invalid mount flags: {}", e),
}
}
}

type RootfsResult<T> = std::result::Result<T, RootfsConfigError>;

/// Strongly typed data structure used to configure the root filesystem
/// kernel arguments for the microvm.
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct RootfsConfig {
/// The device on which the root filesystem should be found.
/// Defaults to "/dev/vda1" if not specified.
pub device: Option<String>,
/// The type of the filesystem on the device.
/// Defaults to "virtiofs" if not specified.
pub fs_type: Option<String>,
/// Additional mount flags to be used when mounting the root filesystem.
pub mount_flags: Option<String>,
/// Whether to mount the root filesystem read-only.
/// If false, mount read-write.
pub read_only: bool,
}

impl RootfsConfig {
/// Create a new RootfsConfig with default values
pub fn new() -> Self {
Self::default()
}

/// Set the device for the root filesystem
pub fn with_device<S: Into<String>>(mut self, device: S) -> RootfsResult<Self> {
let device_str = device.into();
if device_str.is_empty() {
return Err(RootfsConfigError::InvalidDevice(
"Device cannot be empty".to_string(),
));
}
self.device = Some(device_str);
Ok(self)
}

/// Set the filesystem type
pub fn with_fs_type<S: Into<String>>(mut self, fs_type: S) -> RootfsResult<Self> {
let fs_type_str = fs_type.into();
if fs_type_str.is_empty() {
return Err(RootfsConfigError::InvalidFsType(
"Filesystem type cannot be empty".to_string(),
));
}
self.fs_type = Some(fs_type_str);
Ok(self)
}

/// Set the mount flags
pub fn with_mount_flags<S: Into<String>>(mut self, mount_flags: S) -> RootfsResult<Self> {
self.mount_flags = Some(mount_flags.into());
Ok(self)
}

/// Set whether the filesystem should be mounted read-only
pub fn with_read_only(mut self, read_only: bool) -> Self {
self.read_only = read_only;
self
}

/// Get the device, with default fallback
pub fn get_device(&self) -> &str {
self.device.as_deref().unwrap_or("/dev/vda1")
}

/// Get the filesystem type, with default fallback
pub fn get_fs_type(&self) -> &str {
self.fs_type.as_deref().unwrap_or("virtiofs")
}

/// Get the mount flags, if any
pub fn get_mount_flags(&self) -> Option<&str> {
self.mount_flags.as_deref()
}

/// Generate kernel command line arguments from this config
pub fn to_kernel_args(&self) -> String {
let mut args = Vec::new();

// Add root device
args.push(format!("root={}", self.get_device()));

// Add filesystem type
args.push(format!("rootfstype={}", self.get_fs_type()));

// Add mount flags if specified
if let Some(flags) = self.get_mount_flags() {
if !flags.is_empty() {
args.push(format!("rootflags={}", flags));
}
}

// Add read-only or read-write
if self.read_only {
args.push("ro".to_string());
} else {
args.push("rw".to_string());
}

args.join(" ")
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_default_config() {
let config = RootfsConfig::new();
assert_eq!(config.get_device(), "/dev/vda1");
assert_eq!(config.get_fs_type(), "virtiofs");
assert_eq!(config.get_mount_flags(), None);
assert!(!config.read_only);
}

#[test]
fn test_custom_config() {
let config = RootfsConfig::new()
.with_device("/dev/vdb1")
.unwrap()
.with_fs_type("ext4")
.unwrap()
.with_mount_flags("noatime,discard")
.unwrap()
.with_read_only(true);

assert_eq!(config.get_device(), "/dev/vdb1");
assert_eq!(config.get_fs_type(), "ext4");
assert_eq!(config.get_mount_flags(), Some("noatime,discard"));
assert!(config.read_only);
}

#[test]
fn test_to_kernel_args_default() {
let config = RootfsConfig::new();
let args = config.to_kernel_args();
assert_eq!(args, "root=/dev/vda1 rootfstype=virtiofs rw");
}

#[test]
fn test_to_kernel_args_custom() {
let config = RootfsConfig::new()
.with_device("/dev/vdb1")
.unwrap()
.with_fs_type("ext4")
.unwrap()
.with_mount_flags("noatime,discard")
.unwrap()
.with_read_only(true);

let args = config.to_kernel_args();
assert_eq!(
args,
"root=/dev/vdb1 rootfstype=ext4 rootflags=noatime,discard ro"
);
}

#[test]
fn test_invalid_device() {
let result = RootfsConfig::new().with_device("");
assert!(result.is_err());
}

#[test]
fn test_invalid_fs_type() {
let result = RootfsConfig::new().with_fs_type("");
assert!(result.is_err());
}
}
Loading