composefs_ctl/
mountcomposefs.rs1use std::ffi::OsString;
8use std::os::fd::{AsFd, OwnedFd};
9
10use anyhow::{Context, Result, bail};
11use clap::Parser;
12use rustix::fs::{CWD, Mode, OFlags};
13
14use composefs::fsverity::{FsVerityHashValue, MeasureVerityError, Sha256HashValue, measure_verity};
15use composefs::mount::{MountOptions, VerityRequirement, composefs_fsmount, mount_at};
16
17#[derive(Parser, Debug)]
22#[command(name = "mount.composefs")]
23struct MountArgs {
24 #[arg(short = 't', value_name = "TYPE")]
26 fstype: Option<String>,
27
28 #[arg(short = 'o', value_name = "OPTIONS")]
30 options: Option<String>,
31
32 image: String,
34
35 mountpoint: String,
37}
38
39fn unescape_option(s: &str) -> String {
40 let mut result = String::with_capacity(s.len());
41 let mut chars = s.chars();
42 while let Some(c) = chars.next() {
43 if c == '\\' {
44 if let Some(next) = chars.next() {
45 result.push(next);
46 }
47 } else {
48 result.push(c);
49 }
50 }
51 result
52}
53
54struct ParsedOption {
55 key: String,
56 value: Option<String>,
57}
58
59fn parse_mount_options(options: &str) -> Vec<ParsedOption> {
60 let mut result = Vec::new();
61 let mut rest = options;
62
63 while !rest.is_empty() {
64 let mut equal_pos = None;
65 let mut end_pos = rest.len();
66 let bytes = rest.as_bytes();
67 let mut i = 0;
68
69 while i < bytes.len() {
70 if bytes[i] == b'=' && equal_pos.is_none() {
71 equal_pos = Some(i);
72 } else if bytes[i] == b'\\' && i + 1 < bytes.len() {
73 i += 1;
74 } else if bytes[i] == b',' {
75 end_pos = i;
76 break;
77 }
78 i += 1;
79 }
80
81 let entry = &rest[..end_pos];
82 rest = if end_pos < rest.len() {
83 &rest[end_pos + 1..]
84 } else {
85 ""
86 };
87
88 let (key, value) = if let Some(eq) = equal_pos {
89 if eq < end_pos {
90 (&entry[..eq], Some(unescape_option(&entry[eq + 1..])))
91 } else {
92 (entry, None)
93 }
94 } else {
95 (entry, None)
96 };
97
98 result.push(ParsedOption {
99 key: key.to_string(),
100 value,
101 });
102 }
103
104 result
105}
106
107fn run_mount(args: impl IntoIterator<Item = OsString>) -> Result<()> {
108 let cli =
109 MountArgs::try_parse_from(std::iter::once(OsString::from("mount.composefs")).chain(args))?;
110
111 if let Some(ref fstype) = cli.fstype
112 && fstype != "composefs"
113 {
114 bail!("Unsupported fs type '{fstype}'");
115 }
116
117 let mut opt_basedir: Option<String> = None;
118 let mut opt_digest: Option<String> = None;
119 let mut opt_upperdir: Option<String> = None;
120 let mut opt_workdir: Option<String> = None;
121 let mut opt_idmap: Option<OwnedFd> = None;
122 let mut opt_verity = false;
123 let mut opt_tryverity = false;
124 let mut opt_ro = false;
125
126 if let Some(ref opts_str) = cli.options {
127 for opt in parse_mount_options(opts_str) {
128 match opt.key.as_str() {
129 "basedir" => {
130 opt_basedir = Some(opt.value.context("No value specified for basedir option")?);
131 }
132 "digest" => {
133 opt_digest = Some(opt.value.context("No value specified for digest option")?);
134 }
135 "verity" => opt_verity = true,
136 "tryverity" => opt_tryverity = true,
137 "upperdir" => {
138 opt_upperdir = Some(
139 opt.value
140 .context("No value specified for upperdir option")?,
141 );
142 }
143 "workdir" => {
144 opt_workdir = Some(opt.value.context("No value specified for workdir option")?);
145 }
146 "idmap" => {
147 let idmap_path = opt.value.context("No value specified for idmap option")?;
148 let idmap_fd = rustix::fs::open(
149 idmap_path.as_str(),
150 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOCTTY,
151 Mode::empty(),
152 )
153 .with_context(|| format!("Failed to open idmap {idmap_path}"))?;
154 opt_idmap = Some(idmap_fd);
155 }
156 "rw" => opt_ro = false,
157 "ro" => opt_ro = true,
158 other => bail!("Unsupported option: {other}"),
159 }
160 }
161 }
162
163 let basedir_str = match opt_basedir {
164 Some(ref s) => s.as_str(),
165 None => {
166 bail!("No object dirs specified");
167 }
168 };
169
170 let mut basedir_fds: Vec<OwnedFd> = Vec::new();
171 for dir in basedir_str.split(':') {
172 if dir.is_empty() {
173 continue;
174 }
175 let fd = rustix::fs::open(
176 dir,
177 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
178 Mode::empty(),
179 )
180 .with_context(|| format!("Failed to open basedir {dir}"))?;
181 basedir_fds.push(fd);
182 }
183 if basedir_fds.is_empty() {
184 bail!("No object dirs specified");
185 }
186
187 match (&opt_upperdir, &opt_workdir) {
188 (Some(_), None) | (None, Some(_)) => {
189 bail!("Both workdir and upperdir must be specified if used");
190 }
191 _ => {}
192 }
193
194 let verity = if opt_verity || opt_digest.is_some() {
195 VerityRequirement::Required
196 } else if opt_tryverity {
197 VerityRequirement::Try
198 } else {
199 VerityRequirement::Disabled
200 };
201
202 let image_fd = rustix::fs::open(
203 cli.image.as_str(),
204 OFlags::RDONLY | OFlags::CLOEXEC,
205 Mode::empty(),
206 )
207 .with_context(|| format!("Failed to open {}", cli.image))?;
208
209 if let Some(ref digest_hex) = opt_digest {
210 let expected = Sha256HashValue::from_hex(digest_hex).context("Invalid digest value")?;
211 match measure_verity::<Sha256HashValue>(&image_fd) {
212 Ok(measured) => {
213 if measured != expected {
214 bail!(
215 "Failed to mount composefs {}: Image has wrong fs-verity",
216 cli.image
217 );
218 }
219 }
220 Err(MeasureVerityError::VerityMissing) => {
221 bail!(
222 "Failed to mount composefs {}: Image has no fs-verity",
223 cli.image
224 );
225 }
226 Err(e) => {
227 bail!("Failed to mount composefs {}: {e}", cli.image);
228 }
229 }
230 }
231
232 let mut mount_opts = MountOptions::default();
233 if let (Some(upper), Some(work)) = (&opt_upperdir, &opt_workdir) {
234 let upper_fd = rustix::fs::open(
235 upper.as_str(),
236 OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
237 Mode::empty(),
238 )
239 .with_context(|| format!("Failed to open upperdir {upper}"))?;
240 let work_fd = rustix::fs::open(
241 work.as_str(),
242 OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
243 Mode::empty(),
244 )
245 .with_context(|| format!("Failed to open workdir {work}"))?;
246 mount_opts.set_overlay(upper_fd, work_fd);
247 }
248 if let Some(idmap_fd) = opt_idmap {
249 mount_opts.set_idmap(idmap_fd);
250 }
251 mount_opts.set_read_write(!opt_ro);
252
253 let borrowed: Vec<_> = basedir_fds.iter().map(|fd| fd.as_fd()).collect();
254 let fs_fd = composefs_fsmount(image_fd, "composefs", &borrowed, verity, &mount_opts)
255 .with_context(|| format!("Failed to mount composefs {}", cli.image))?;
256
257 mount_at(&fs_fd, CWD, cli.mountpoint.as_str())
258 .with_context(|| format!("Failed to mount at {}", cli.mountpoint))?;
259
260 Ok(())
261}
262
263pub fn run() -> Result<()> {
265 let args: Vec<OsString> = std::env::args_os().skip(1).collect();
266 run_mount(args)
267}
268
269pub fn run_from_args(args: Vec<OsString>) -> Result<()> {
271 run_mount(args)
272}