Skip to content

Commit

Permalink
mount: Add support for FEX merged rootfs mode
Browse files Browse the repository at this point in the history
In this mode, the FEX rootfs passed to FEX is already overlaid on top of
the real root filesystem, so FEX directs all guest accesses to it
instead of doing its own overlay logic. This, together with a bunch of
fixes on the FEX side, fixes Wine.

Opt-in for now, since this actively *breaks* things with the current
FEX. May become the default in the future once all that is sorted out.

Signed-off-by: Asahi Lina <[email protected]>
  • Loading branch information
asahilina committed Dec 19, 2024
1 parent 13b10f7 commit 8d7b1de
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 41 deletions.
9 changes: 9 additions & 0 deletions crates/muvm/src/bin/muvm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,15 @@ fn main() -> Result<ExitCode> {
.collect()
};

if options.merged_rootfs {
if disks.is_empty() {
return Err(anyhow!(
"Merged RootFS mode requires one or more RootFS images"
));
}
env.insert("FEX_MERGEDROOTFS".to_owned(), "1".to_owned());
}

for path in disks {
add_ro_disk(ctx_id, &path, &path).context("Failed to configure disk")?;
}
Expand Down
6 changes: 6 additions & 0 deletions crates/muvm/src/cli_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub struct Options {
pub root_server_port: u32,
pub server_port: u32,
pub fex_images: Vec<String>,
pub merged_rootfs: bool,
pub sommelier: bool,
pub interactive: bool,
pub tty: bool,
Expand Down Expand Up @@ -90,6 +91,10 @@ pub fn options() -> OptionParser<Options> {
)
.argument::<String>("FEX_IMAGE")
.many();
let merged_rootfs = long("merged-rootfs")
.short('m')
.help("Use merged rootfs for FEX (experimental)")
.switch();
let passt_socket = long("passt-socket")
.help("Instead of starting passt, connect to passt socket at PATH")
.argument("PATH")
Expand Down Expand Up @@ -133,6 +138,7 @@ pub fn options() -> OptionParser<Options> {
root_server_port,
server_port,
fex_images,
merged_rootfs,
sommelier,
interactive,
tty,
Expand Down
214 changes: 173 additions & 41 deletions crates/muvm/src/guest/mount.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
use std::collections::HashSet;
use std::env;
use std::ffi::CString;
use std::fs::{read_dir, File};
use std::fs::{read_dir, read_link, File};
use std::io::Write;
use std::os::fd::AsFd;
use std::path::Path;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use rustix::fs::{mkdir, symlink, Mode, CWD};
use rustix::mount::{
mount2, mount_bind, move_mount, open_tree, unmount, MountFlags, MoveMountFlags, OpenTreeFlags,
UnmountFlags,
mount2, mount_bind, mount_recursive_bind, move_mount, open_tree, unmount, MountFlags,
MoveMountFlags, OpenTreeFlags, UnmountFlags,
};
use rustix::path::Arg;

fn make_tmpfs(dir: &str) -> Result<()> {
mount2(
Expand All @@ -31,6 +34,21 @@ fn mkdir_fex(dir: &str) {
.unwrap();
}

fn do_mount_recursive_bind(source: &str, target: PathBuf) -> Result<()> {
// Special case, do not recursively mount the FEX stuff itself, but do
// the /run/muvm-host thing.
if source == "/run" {
mount_bind(source, &target)
.context(format!("Failed to mount {:?} on {:?}", &source, &target))?;
let host = target.join("muvm-host");
mount_bind("/", &host).context(format!("Failed to mount / on {:?}", &host))?;
} else {
mount_recursive_bind(source, &target)
.context(format!("Failed to mount {:?} on {:?}", &source, &target))?;
}
Ok(())
}

fn mount_fex_rootfs() -> Result<()> {
let dir = "/run/fex-emu/";
let dir_rootfs = dir.to_string() + "rootfs";
Expand All @@ -41,6 +59,19 @@ fn mount_fex_rootfs() -> Result<()> {
let flags = MountFlags::RDONLY;
let mut images = Vec::new();

let merged_rootfs = env::var("FEX_MERGEDROOTFS")
.map(|a| a != "0")
.unwrap_or(false);

// In merged RootFS mode, make /run/fex-emu a tmpfs.
// This ensures that once /run is bind-mounted into the
// rootfs, /run/fex-emu/* isn't itself visible within the
// rootfs, so recursive RootFS lookups don't succeed and
// break things.
if merged_rootfs {
make_tmpfs(dir)?;
}

// Find /dev/vd*
for x in read_dir("/dev").unwrap() {
let file = x.unwrap();
Expand All @@ -60,33 +91,126 @@ fn mount_fex_rootfs() -> Result<()> {
images.push(dir);
}

if images.len() >= 2 {
// Overlay the mounts together.
let opts = format!(
"lowerdir={}",
images.into_iter().rev().collect::<Vec<String>>().join(":")
);
let opts = CString::new(opts).unwrap();
let overlay = "overlay".to_string();
let overlay_ = Some(&overlay);
#[allow(clippy::collapsible_else_if)]
if merged_rootfs {
// For merged rootfs mode, we need to overlay subtrees separately
// onto the real rootfs. First, insert the real rootfs as the
// bottom-most "image".
images.insert(0, "/".to_owned());

let mut merge_dirs = HashSet::new();
let mut non_dirs = HashSet::new();

mkdir_fex(&dir_rootfs);
mount2(overlay_, &dir_rootfs, overlay_, flags, Some(&opts)).context("Failed to overlay")?;
} else if images.len() == 1 {
// Just expose the one mount
symlink(&images[0], &dir_rootfs)?;
} else if images.is_empty() {
// If no images were passed, FEX is either managed by the host os
// or is not installed at all. Avoid clobbering the config in that case.
return Ok(());

// List all the merged root entries in each layer
// Go backwards, since the file type of the topmost layer "wins"
for image in images.iter().rev() {
for entry in read_dir(image).unwrap() {
let Ok(entry) = entry else { continue };
let Ok(file_type) = entry.file_type() else {
continue;
};
let source = entry.path();
let file_name = entry.file_name().to_str().unwrap().to_owned();
let target = Path::new(&dir_rootfs).join(&file_name);

if file_type.is_file() {
// File in the root fs, bind mount it from the uppermost layer
if non_dirs.insert(file_name) {
File::create(&target)?;
mount_bind(&source, &target)?;
}
} else if file_type.is_symlink() {
// Symlink in the root fs, create it from the uppermost layer
if non_dirs.insert(file_name) {
let symlink_target = read_link(source)?;
symlink(&symlink_target, &target)?;
}
} else {
// Directory, so we potentially have to overlayfs it
if merge_dirs.insert(file_name) {
mkdir_fex(target.as_str()?);
}
}
}
}

// Now, go through each potential merged dir and figure out which
// layers have it, then mount an overlayfs (or bind if one layer).
for dir in merge_dirs {
let target = Path::new(&dir_rootfs).join(&dir);
let mut layers = Vec::new();

for image in images.iter() {
let source = Path::new(image).join(&dir);
if source.is_dir() {
layers.push(source.as_str().unwrap().to_owned());
}
}
assert!(!layers.is_empty());
if layers.len() == 1 {
do_mount_recursive_bind(&layers[0], target)?;
} else {
if layers[0] == "/etc" {
// Special case: /etc has an overlaid mount for /etc/resolv.conf,
// which will confuse overlayfs. So grab the raw mount.
layers[0] = "/run/muvm-host/etc".to_owned();
}
let opts = format!(
"lowerdir={},metacopy=off,redirect_dir=nofollow,userxattr",
layers.into_iter().rev().collect::<Vec<String>>().join(":")
);
let opts = CString::new(opts).unwrap();
let overlay = "overlay".to_string();
let overlay_ = Some(&overlay);

mount2(overlay_, &target, overlay_, flags, Some(&opts))
.context("Failed to overlay")?;
}
}

// Special case: Put back the /etc/resolv.conf overlay on top
overlay_file(
"/etc/resolv.conf",
&(dir_rootfs.clone() + "/etc/resolv.conf"),
)?;
} else {
if images.len() >= 2 {
// Overlay the mounts together.
let opts = format!(
"lowerdir={}",
images.into_iter().rev().collect::<Vec<String>>().join(":")
);
let opts = CString::new(opts).unwrap();
let overlay = "overlay".to_string();
let overlay_ = Some(&overlay);

mkdir_fex(&dir_rootfs);
mount2(overlay_, &dir_rootfs, overlay_, flags, Some(&opts))
.context("Failed to overlay")?;
} else if images.len() == 1 {
assert!(!merged_rootfs);
// Just expose the one mount
symlink(&images[0], &dir_rootfs)?;
} else if images.is_empty() {
assert!(!merged_rootfs);
// If no images were passed, FEX is either managed by the host os
// or is not installed at all. Avoid clobbering the config in that case.
return Ok(());
}
}

// Now we need to tell FEX about this. One of the FEX share directories has an unmounted rootfs
// and a Config.json telling FEX to use FUSE. Neither should be visible to the guest. Instead,
// we want to replace the folders and tell FEX to use our mounted rootfs
for base in ["/usr/share/fex-emu", "/usr/local/share/fex-emu"] {
if Path::new(base).exists() {
let json = format!("{{\"Config\":{{\"RootFS\":\"{dir_rootfs}\"}}}}\n");
let json = if merged_rootfs {
format!("{{\"Config\":{{\"RootFS\":\"{dir_rootfs}\",\"MergedRootFS\":\"1\"}}}}\n")
} else {
format!("{{\"Config\":{{\"RootFS\":\"{dir_rootfs}\"}}}}\n")
};
let path = base.to_string() + "/Config.json";

make_tmpfs(base)?;
Expand All @@ -97,6 +221,24 @@ fn mount_fex_rootfs() -> Result<()> {
Ok(())
}

pub fn overlay_file(src: &str, dest: &str) -> Result<()> {
let fd = open_tree(
CWD,
src,
OpenTreeFlags::OPEN_TREE_CLONE | OpenTreeFlags::OPEN_TREE_CLOEXEC,
)
.context("Failed to open_tree src")?;

move_mount(
fd.as_fd(),
"",
CWD,
dest,
MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
)
.context("Failed to move_mount src to dest")
}

pub fn place_etc(file: &str, contents: Option<&str>) -> Result<()> {
let tmp = "/tmp/".to_string() + file;
let etc = "/etc/".to_string() + file;
Expand All @@ -115,30 +257,12 @@ pub fn place_etc(file: &str, contents: Option<&str>) -> Result<()> {
}
}

let fd = open_tree(
CWD,
&tmp,
OpenTreeFlags::OPEN_TREE_CLONE | OpenTreeFlags::OPEN_TREE_CLOEXEC,
)
.context("Failed to open_tree tmp")?;

move_mount(
fd.as_fd(),
"",
CWD,
etc,
MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
)
.context("Failed to move_mount tmp to etc")
overlay_file(&tmp, &etc)
}

pub fn mount_filesystems() -> Result<()> {
make_tmpfs("/var/run")?;

if mount_fex_rootfs().is_err() {
println!("Failed to mount FEX rootfs, carrying on without.")
}

place_etc("resolv.conf", None)?;

mount2(
Expand Down Expand Up @@ -173,5 +297,13 @@ pub fn mount_filesystems() -> Result<()> {
)
.context("Failed to mount `/dev/shm`")?;

// Do this last so it can pick up all the submounts made above.
if let Err(e) = mount_fex_rootfs() {
println!(
"Failed to mount FEX rootfs, carrying on without. Error: {}",
e
);
}

Ok(())
}

0 comments on commit 8d7b1de

Please sign in to comment.