1use 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#[derive(Debug, Clone, Copy, Default, ValueEnum)]
18pub(crate) enum FuseMode {
19 #[default]
21 Auto,
22 Yes,
24 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#[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#[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#[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}