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