Skip to main content

composefs_ctl/
fuse.rs

1//! FUSE mount support for cfsctl.
2//!
3//! This module is only compiled when the `fuse` feature is enabled.
4
5use std::os::fd::OwnedFd;
6use std::sync::Arc;
7
8use anyhow::{Context as _, Result};
9use clap::ValueEnum;
10use rustix::fs::{CWD, Mode, OFlags};
11
12use composefs::fsverity::FsVerityHashValue;
13use composefs::mount::MountOptions;
14use composefs::repository::Repository;
15
16/// How to mount: kernel composefs driver, FUSE, or auto-detect.
17#[derive(Debug, Clone, Copy, Default, ValueEnum)]
18pub(crate) enum FuseMode {
19    /// Auto-detect based on privileges
20    #[default]
21    Auto,
22    /// Force FUSE mount
23    Yes,
24    /// Force kernel mount
25    No,
26}
27
28pub(crate) enum MountMode {
29    Kernel,
30    Fuse,
31    FuseOverlay,
32}
33
34fn in_init_user_namespace() -> bool {
35    std::fs::read_to_string("/proc/self/uid_map")
36        .map(|s| s.trim() == "0          0 4294967295")
37        .unwrap_or(false)
38}
39
40fn has_cap_sys_admin() -> bool {
41    if let Ok(caps) = rustix::thread::capabilities(None) {
42        caps.effective
43            .contains(rustix::thread::CapabilitySet::SYS_ADMIN)
44    } else {
45        false
46    }
47}
48
49pub(crate) fn detect_mount_mode(fuse_mode: FuseMode, has_upper: bool) -> MountMode {
50    let use_fuse = match fuse_mode {
51        FuseMode::Yes => true,
52        FuseMode::No => false,
53        FuseMode::Auto => !(rustix::process::getuid().is_root() && in_init_user_namespace()),
54    };
55
56    if !use_fuse {
57        return MountMode::Kernel;
58    }
59
60    if (has_upper || has_cap_sys_admin()) && composefs_fuse::user_overlay_supported() {
61        MountMode::FuseOverlay
62    } else {
63        MountMode::Fuse
64    }
65}
66
67pub(crate) fn run_fuse_foreground(
68    image_fd: OwnedFd,
69    objects_fd: Arc<OwnedFd>,
70    mountpoint: &str,
71    mode: MountMode,
72    mount_options: MountOptions,
73    enable_verity: bool,
74    ready_fd: Option<OwnedFd>,
75) -> Result<()> {
76    match mode {
77        MountMode::Kernel => unreachable!(),
78        MountMode::Fuse => {
79            let options = composefs_fuse::ServeFuseOptions::default();
80            composefs_fuse::serve_fuse(mountpoint, image_fd, objects_fd, &options, ready_fd)
81                .context("FUSE server error")?;
82        }
83        MountMode::FuseOverlay => {
84            let dev_fuse = composefs_fuse::open_fuse()?;
85            let fuse_options = composefs_fuse::FuseMountOptions::default();
86            let fuse_mnt =
87                composefs_fuse::mount_fuse(&dev_fuse, &fuse_options).context("FUSE mount")?;
88
89            let mut serve_options = composefs_fuse::ServeFuseOptions::default();
90            serve_options.set_overlay_xattr(Some(composefs_fuse::OverlayXattrMode::User));
91
92            let serve_objects = Arc::clone(&objects_fd);
93            let serve_dev = dev_fuse;
94            let join_handle = std::thread::spawn(move || {
95                composefs_fuse::serve_fuse_fd(serve_dev, image_fd, serve_objects, &serve_options)
96            });
97
98            let read_write = mount_options.read_write();
99            let mut overlay_options = composefs_fuse::OverlayMountOptions::default();
100            if let Some((upper_fd, work_fd)) = mount_options.into_overlay() {
101                overlay_options.set_overlay(upper_fd, work_fd);
102            }
103            overlay_options.set_read_write(read_write);
104            overlay_options.set_enable_verity(enable_verity);
105
106            let overlay_mnt =
107                composefs_fuse::mount_fuse_overlay(fuse_mnt, &*objects_fd, &overlay_options)
108                    .context("overlay mount")?;
109            composefs::mount::mount_at(overlay_mnt, CWD, mountpoint)?;
110
111            if let Some(fd) = ready_fd {
112                let _ = rustix::io::write(&fd, b"r");
113            }
114
115            join_handle
116                .join()
117                .map_err(|_| anyhow::anyhow!("FUSE server thread panicked"))?
118                .context("FUSE server error")?;
119        }
120    }
121    Ok(())
122}
123
124/// Re-exec ourselves as `--internal-fuse-serve` to run the FUSE server in a
125/// clean process without the tokio runtime. File descriptors are passed via
126/// the systemd socket activation protocol (LISTEN_FDS/LISTEN_FDNAMES) for
127/// safe and easy fd transfer.
128#[allow(unsafe_code)]
129pub(crate) fn run_fuse_mount<ObjectID: FsVerityHashValue>(
130    repo: &Arc<Repository<ObjectID>>,
131    name: &str,
132    mountpoint: &str,
133    mode: MountMode,
134    mount_options: MountOptions,
135    foreground: bool,
136) -> Result<()> {
137    if foreground {
138        let (image_fd, enable_verity) = repo.open_image(name)?;
139        let objects_fd = Arc::new(repo.objects_dir()?.try_clone()?);
140        return run_fuse_foreground(
141            image_fd,
142            objects_fd,
143            mountpoint,
144            mode,
145            mount_options,
146            enable_verity,
147            None,
148        );
149    }
150
151    use cap_std_ext::cmdext::{CapStdExtCommandExt as _, CmdFds, SystemdFdName};
152    use std::os::unix::process::CommandExt;
153
154    let (image_fd, enable_verity) = repo.open_image(name)?;
155    let (read_pipe, write_pipe) = rustix::pipe::pipe_with(rustix::pipe::PipeFlags::CLOEXEC)?;
156    let repo_fd = repo.repo_fd().try_clone_to_owned()?;
157
158    let read_write = mount_options.read_write();
159    let mut sd_fds: Vec<(Arc<OwnedFd>, SystemdFdName<'_>)> = vec![
160        (Arc::new(image_fd), SystemdFdName::new("image")),
161        (Arc::new(repo_fd), SystemdFdName::new("repo")),
162        (Arc::new(write_pipe), SystemdFdName::new("ready")),
163    ];
164
165    if let Some((upper_fd, work_fd)) = mount_options.into_overlay() {
166        sd_fds.push((Arc::new(upper_fd), SystemdFdName::new("upper")));
167        sd_fds.push((Arc::new(work_fd), SystemdFdName::new("work")));
168    }
169
170    let fds = CmdFds::new_systemd_fds(sd_fds);
171
172    let self_exe = std::env::current_exe().context("resolving own binary path")?;
173    let mut cmd = std::process::Command::new(&self_exe);
174    cmd.arg("--internal-fuse-serve");
175    cmd.arg("--mountpoint").arg(mountpoint);
176
177    match mode {
178        MountMode::Kernel => unreachable!(),
179        MountMode::Fuse => cmd.arg("--mode").arg("fuse"),
180        MountMode::FuseOverlay => cmd.arg("--mode").arg("fuse-overlay"),
181    };
182
183    if enable_verity {
184        cmd.arg("--enable-verity");
185    }
186    if read_write {
187        cmd.arg("--read-write");
188    }
189
190    cmd.take_fds(fds);
191
192    unsafe {
193        cmd.pre_exec(|| {
194            let _ = rustix::process::setsid();
195            Ok(())
196        });
197    }
198
199    cmd.stdin(std::process::Stdio::null());
200    cmd.stdout(std::process::Stdio::null());
201    cmd.stderr(std::process::Stdio::inherit());
202
203    let _child = cmd.spawn().context("spawning FUSE server process")?;
204
205    let mut buf = [0u8; 1];
206    let _ = rustix::io::read(&read_pipe, &mut buf);
207
208    Ok(())
209}
210
211/// Arguments for the internal FUSE server process.
212/// File descriptors are received via the systemd activation protocol
213/// (LISTEN_FDS/LISTEN_FDNAMES), not via raw fd number arguments.
214#[derive(Debug, clap::Parser)]
215pub struct InternalFuseServeArgs {
216    #[arg(long)]
217    mountpoint: String,
218    #[arg(long, value_parser = ["fuse", "fuse-overlay"])]
219    mode: String,
220    #[arg(long)]
221    enable_verity: bool,
222    #[arg(long)]
223    read_write: bool,
224}
225
226/// Entry point for the internal FUSE server process, called from main()
227/// before the tokio runtime is created.
228#[allow(unsafe_code)]
229pub fn run_internal_fuse_serve(args: InternalFuseServeArgs) -> Result<()> {
230    use std::os::fd::{FromRawFd, IntoRawFd};
231
232    let fds = libsystemd::activation::receive_descriptors_with_names(true)
233        .map_err(|e| anyhow::anyhow!("receiving activation fds: {e}"))?;
234
235    let mut image_fd: Option<OwnedFd> = None;
236    let mut repo_fd: Option<OwnedFd> = None;
237    let mut ready_fd: Option<OwnedFd> = None;
238    let mut upper_fd: Option<OwnedFd> = None;
239    let mut work_fd: Option<OwnedFd> = None;
240
241    for (fd, name) in fds {
242        let owned = unsafe { OwnedFd::from_raw_fd(fd.into_raw_fd()) };
243        match name.as_str() {
244            "image" => image_fd = Some(owned),
245            "repo" => repo_fd = Some(owned),
246            "ready" => ready_fd = Some(owned),
247            "upper" => upper_fd = Some(owned),
248            "work" => work_fd = Some(owned),
249            other => log::warn!("unexpected activation fd name: {other}"),
250        }
251    }
252
253    let image_fd = image_fd.context("missing 'image' activation fd")?;
254    let repo_fd = repo_fd.context("missing 'repo' activation fd")?;
255    let ready_fd = ready_fd.context("missing 'ready' activation fd")?;
256
257    let objects_fd = Arc::new(
258        rustix::fs::openat(
259            &repo_fd,
260            "objects",
261            OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
262            Mode::empty(),
263        )
264        .context("opening objects dir")?,
265    );
266
267    let mode = match args.mode.as_str() {
268        "fuse" => MountMode::Fuse,
269        "fuse-overlay" => MountMode::FuseOverlay,
270        _ => unreachable!(),
271    };
272
273    let mut mount_options = MountOptions::default();
274    if let (Some(upper), Some(work)) = (upper_fd, work_fd) {
275        mount_options.set_overlay(upper, work);
276    }
277    mount_options.set_read_write(args.read_write);
278
279    run_fuse_foreground(
280        image_fd,
281        objects_fd,
282        &args.mountpoint,
283        mode,
284        mount_options,
285        args.enable_verity,
286        Some(ready_fd),
287    )
288}