1use std::ffi::CString;
6use std::os::fd::AsRawFd;
7use std::os::unix::ffi::OsStrExt;
8use std::path::Path;
9
10use anyhow::{Context, Result, bail};
11
12pub fn layers(overlay: &crate::Overlay, slot: usize) -> [std::path::PathBuf; 3] {
16 let command = overlay.directory.join(format!("command-{slot}"));
17 ["upper", "work", "real"].map(|name| command.join(name))
18}
19
20fn c_path(path: &Path) -> Result<CString> {
21 CString::new(path.as_os_str().as_bytes()).context("a path holds a NUL byte")
22}
23
24pub struct Setup {
27 uid_map: CString,
28 gid_map: CString,
29 workspace: CString,
30 real: CString,
31 options: CString,
32 aside: std::os::fd::OwnedFd,
37 passthrough: Vec<(CString, CString)>,
40 cwd: CString,
41 handled: u64,
46 file_rights: u64,
47 outside: Vec<(i32, u64)>,
48 workspace_rights: u64,
49 inside: Vec<(CString, u64, bool)>,
50}
51
52#[repr(C)]
53struct RulesetAttr {
54 handled_access_fs: u64,
55}
56
57#[repr(C, packed)]
58struct PathBeneathAttr {
59 allowed_access: u64,
60 parent_fd: i32,
61}
62
63const RULE_PATH_BENEATH: i32 = 1;
64
65impl Setup {
66 pub fn new(overlay: &crate::Overlay, slot: usize, cwd: &Path) -> Result<Self> {
67 let [upper, work, real] = layers(overlay, slot);
68 for directory in [&upper, &work, &real] {
69 std::fs::create_dir_all(directory)
70 .with_context(|| format!("cannot create {}", directory.display()))?;
71 }
72 let text = |path: &Path| -> Result<String> {
73 let text = path.to_str().context("an overlay's paths must be UTF-8")?;
74 if text.contains([',', ':', '\\']) {
76 bail!("an overlay cannot be mounted at {text}");
77 }
78 Ok(text.to_owned())
79 };
80 let options = format!(
81 "lowerdir={},upperdir={},workdir={},userxattr",
82 text(&real)?,
83 text(&upper)?,
84 text(&work)?
85 );
86 let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
89 let aside: std::os::fd::OwnedFd = std::fs::File::open("/dev/null")
90 .context("cannot reserve a descriptor")?
91 .into();
92 let mut passthrough = Vec::new();
93 for path in &overlay.passthrough {
94 let relative = path
95 .strip_prefix(&overlay.workspace)
96 .context("a path bound through lies outside the workspace")?;
97 let source = Path::new("/proc/self/fd")
98 .join(aside.as_raw_fd().to_string())
99 .join(relative);
100 passthrough.push((c_path(&source)?, c_path(path)?));
101 }
102 let rules = &overlay.rules;
103 let mut inside = Vec::new();
104 for (path, rights, unlinked) in &rules.inside {
105 let Ok(relative) = path.strip_prefix(&overlay.workspace) else {
106 continue;
107 };
108 let relative = if relative.as_os_str().is_empty() {
109 Path::new(".")
110 } else {
111 relative
112 };
113 inside.push((c_path(relative)?, *rights, *unlinked));
114 }
115 Ok(Self {
116 aside,
117 handled: rules.handled,
118 file_rights: rules.file_rights,
119 outside: rules
120 .outside
121 .iter()
122 .map(|(fd, rights)| (fd.as_raw_fd(), *rights))
123 .collect(),
124 workspace_rights: rules.workspace,
125 inside,
126 uid_map: CString::new(format!("{uid} {uid} 1"))?,
127 gid_map: CString::new(format!("{gid} {gid} 1"))?,
128 workspace: c_path(&overlay.workspace)?,
129 real: c_path(&real)?,
130 options: CString::new(options)?,
131 passthrough,
132 cwd: c_path(cwd)?,
133 })
134 }
135
136 pub fn enter(&self) -> std::io::Result<()> {
139 let fail = || Err(std::io::Error::last_os_error());
140 unsafe {
141 if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNS) != 0 {
142 return fail();
143 }
144 for (file, text) in [
145 (c"/proc/self/setgroups", c"deny"),
146 (c"/proc/self/uid_map", self.uid_map.as_c_str()),
147 (c"/proc/self/gid_map", self.gid_map.as_c_str()),
148 ] {
149 let fd = libc::open(file.as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC);
150 if fd < 0 {
151 return fail();
152 }
153 let bytes = text.to_bytes();
154 let written = libc::write(fd, bytes.as_ptr().cast(), bytes.len());
155 libc::close(fd);
156 if written < 0 {
157 return fail();
158 }
159 }
160 let none = std::ptr::null();
161 if libc::mount(
162 none,
163 c"/".as_ptr(),
164 none,
165 libc::MS_REC | libc::MS_PRIVATE,
166 none.cast(),
167 ) != 0
168 || libc::mount(
169 self.workspace.as_ptr(),
170 self.real.as_ptr(),
171 none,
172 libc::MS_BIND | libc::MS_REC,
173 none.cast(),
174 ) != 0
175 {
176 return fail();
177 }
178 let aside = libc::open(
179 self.real.as_ptr(),
180 libc::O_PATH | libc::O_DIRECTORY | libc::O_CLOEXEC,
181 );
182 let held = self.aside.as_raw_fd();
184 if aside < 0 || libc::dup3(aside, held, libc::O_CLOEXEC) < 0 {
185 return fail();
186 }
187 libc::close(aside);
188 if libc::mount(
189 c"overlay".as_ptr(),
190 self.workspace.as_ptr(),
191 c"overlay".as_ptr(),
192 0,
193 self.options.as_ptr().cast(),
194 ) != 0
195 {
196 return fail();
197 }
198 for (source, target) in &self.passthrough {
199 if libc::mount(
200 source.as_ptr(),
201 target.as_ptr(),
202 none,
203 libc::MS_BIND | libc::MS_REC,
204 none.cast(),
205 ) != 0
206 {
207 return fail();
208 }
209 }
210 libc::close(held);
211 if libc::chdir(self.cwd.as_ptr()) != 0 {
213 return fail();
214 }
215 self.restrict()?;
216 }
217 Ok(())
218 }
219
220 unsafe fn restrict(&self) -> std::io::Result<()> {
223 let fail = || Err(std::io::Error::last_os_error());
224 unsafe {
225 let attr = RulesetAttr {
226 handled_access_fs: self.handled,
227 };
228 let ruleset = libc::syscall(
229 libc::SYS_landlock_create_ruleset,
230 &attr as *const RulesetAttr,
231 std::mem::size_of::<RulesetAttr>(),
232 0u32,
233 ) as i32;
234 if ruleset < 0 {
235 return fail();
236 }
237 let add = |fd: i32, rights: u64| -> std::io::Result<()> {
238 let mut stat: libc::stat = std::mem::zeroed();
239 let directory =
240 libc::fstat(fd, &mut stat) == 0 && stat.st_mode & libc::S_IFMT == libc::S_IFDIR;
241 let mut rights = rights & self.handled;
242 if !directory {
243 rights &= self.file_rights;
244 }
245 if rights == 0 {
246 return Ok(());
247 }
248 let attr = PathBeneathAttr {
249 allowed_access: rights,
250 parent_fd: fd,
251 };
252 if libc::syscall(
253 libc::SYS_landlock_add_rule,
254 ruleset,
255 RULE_PATH_BENEATH,
256 &attr as *const PathBeneathAttr,
257 0u32,
258 ) != 0
259 {
260 return Err(std::io::Error::last_os_error());
261 }
262 Ok(())
263 };
264 for (fd, rights) in &self.outside {
265 add(*fd, *rights)?;
266 }
267 let root = libc::open(
268 self.workspace.as_ptr(),
269 libc::O_PATH | libc::O_DIRECTORY | libc::O_CLOEXEC,
270 );
271 if root < 0 {
272 return fail();
273 }
274 add(root, self.workspace_rights)?;
275 for (relative, rights, unlinked) in &self.inside {
276 let fd = if *unlinked {
277 let mut how: libc::open_how = std::mem::zeroed();
278 how.flags = (libc::O_PATH | libc::O_CLOEXEC) as u64;
279 how.resolve = libc::RESOLVE_BENEATH | libc::RESOLVE_NO_SYMLINKS;
280 libc::syscall(
281 libc::SYS_openat2,
282 root,
283 relative.as_ptr(),
284 &how as *const libc::open_how,
285 std::mem::size_of::<libc::open_how>(),
286 ) as i32
287 } else {
288 libc::openat(root, relative.as_ptr(), libc::O_PATH | libc::O_CLOEXEC)
289 };
290 if fd < 0 {
291 match *libc::__errno_location() {
294 libc::ENOENT | libc::ELOOP | libc::EXDEV | libc::ENOTDIR => continue,
295 _ => return fail(),
296 }
297 }
298 let added = add(fd, *rights);
299 libc::close(fd);
300 added?;
301 }
302 libc::close(root);
303 if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0
304 || libc::syscall(libc::SYS_landlock_restrict_self, ruleset, 0u32) != 0
305 {
306 return fail();
307 }
308 libc::close(ruleset);
309 }
310 Ok(())
311 }
312}
313
314pub fn probe(directory: &Path) -> Result<()> {
319 use std::os::unix::process::CommandExt;
320 let [lower, upper, work, merged] =
321 ["lower", "upper", "work", "merged"].map(|name| directory.join(name));
322 for path in [&lower, &upper, &work, &merged] {
323 std::fs::create_dir_all(path)
324 .with_context(|| format!("cannot create {}", path.display()))?;
325 }
326 let options = CString::new(format!(
327 "lowerdir={},upperdir={},workdir={},userxattr",
328 lower.display(),
329 upper.display(),
330 work.display()
331 ))?;
332 let merged_path = c_path(&merged)?;
333 let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
334 let uid_map = CString::new(format!("{uid} {uid} 1"))?;
335 let gid_map = CString::new(format!("{gid} {gid} 1"))?;
336 let mut pipe = [0i32; 2];
339 if unsafe { libc::pipe2(pipe.as_mut_ptr(), libc::O_CLOEXEC) } != 0 {
340 return Err(std::io::Error::last_os_error()).context("cannot make a pipe");
341 }
342 let (reader, writer) = (pipe[0], pipe[1]);
343 let mut command = std::process::Command::new("/bin/sh");
344 command
345 .args(["-c", "exit 0"])
346 .stdin(std::process::Stdio::null())
347 .stdout(std::process::Stdio::null())
348 .stderr(std::process::Stdio::null());
349 unsafe {
350 command.pre_exec(move || {
351 let fail = |step: u8| {
352 let error = std::io::Error::last_os_error();
353 libc::write(writer, (&step as *const u8).cast(), 1);
354 Err(error)
355 };
356 if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNS) != 0 {
357 return fail(0);
358 }
359 for (step, (file, text)) in [
360 (c"/proc/self/setgroups", c"deny"),
361 (c"/proc/self/uid_map", uid_map.as_c_str()),
362 (c"/proc/self/gid_map", gid_map.as_c_str()),
363 ]
364 .into_iter()
365 .enumerate()
366 {
367 let fd = libc::open(file.as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC);
368 if fd < 0 {
369 return fail(1 + step as u8);
370 }
371 let bytes = text.to_bytes();
372 let written = libc::write(fd, bytes.as_ptr().cast(), bytes.len());
373 libc::close(fd);
374 if written < 0 {
375 return fail(1 + step as u8);
376 }
377 }
378 if libc::mount(
379 c"overlay".as_ptr(),
380 merged_path.as_ptr(),
381 c"overlay".as_ptr(),
382 0,
383 options.as_ptr().cast(),
384 ) != 0
385 {
386 return fail(4);
387 }
388 Ok(())
389 });
390 }
391 let output = command.output();
392 unsafe { libc::close(writer) };
393 let mut step = [u8::MAX];
394 let read = unsafe { libc::read(reader, step.as_mut_ptr().cast(), 1) };
395 unsafe { libc::close(reader) };
396 clear(directory);
397 let what = match (read, step[0]) {
398 (1, 0) => "making a user and mount namespace",
399 (1, 1) => "denying setgroups",
400 (1, 2) => "mapping the user",
401 (1, 3) => "mapping the group",
402 (1, 4) => "mounting an overlay",
403 _ => "running a command over an overlay",
404 };
405 let where_ = directory.display();
406 match output {
407 Ok(output) if output.status.success() => Ok(()),
408 Ok(output) => bail!("{what} in {where_} failed: {}", output.status),
409 Err(error) => Err(error).with_context(|| format!("{what} in {where_} failed")),
410 }
411}
412
413pub fn clear(directory: &Path) {
416 use std::os::unix::fs::PermissionsExt;
417 let mut stack = vec![directory.to_owned()];
418 while let Some(path) = stack.pop() {
419 if std::fs::symlink_metadata(&path).is_ok_and(|metadata| metadata.is_dir()) {
420 let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700));
421 if let Ok(entries) = std::fs::read_dir(&path) {
422 stack.extend(entries.flatten().map(|entry| entry.path()));
423 }
424 }
425 }
426 let _ = std::fs::remove_dir_all(directory);
427}
428
429pub fn settle(overlay: &crate::Overlay) -> crate::Settled {
434 let mut settled = crate::Settled::default();
435 for slot in 0.. {
436 let [upper, ..] = layers(overlay, slot);
437 if !upper.parent().is_some_and(Path::exists) {
438 break;
439 }
440 if upper.is_dir() {
441 let layer = Layer {
442 upper: &upper,
443 root: &overlay.workspace,
444 keep: &overlay.keep,
445 passthrough: &overlay.passthrough,
446 };
447 layer.apply(Path::new(""), false, &mut settled);
448 }
449 }
450 settled
451}
452
453struct Layer<'a> {
455 upper: &'a Path,
456 root: &'a Path,
457 keep: &'a [std::path::PathBuf],
459 passthrough: &'a [std::path::PathBuf],
461}
462
463impl Layer<'_> {
464 fn allowed(&self, target: &Path) -> bool {
465 self.keep.iter().any(|keep| target.starts_with(keep))
466 }
467
468 fn holds_bound(&self, target: &Path) -> bool {
470 self.passthrough
471 .iter()
472 .any(|through| through.starts_with(target))
473 }
474
475 fn holds_kept(&self, target: &Path) -> bool {
477 self.keep.iter().any(|keep| keep.starts_with(target))
478 }
479
480 fn apply(&self, relative: &Path, hidden: bool, settled: &mut crate::Settled) -> bool {
493 use std::os::unix::fs::{FileTypeExt, MetadataExt};
494 let mut applied = hidden && self.prune(relative, settled);
495 let entries = match std::fs::read_dir(self.upper.join(relative)) {
496 Ok(entries) => entries,
497 Err(error) => {
498 settled.fail(&self.upper.join(relative), &error);
499 return applied;
500 }
501 };
502 for entry in entries {
503 let entry = match entry {
504 Ok(entry) => entry,
505 Err(error) => {
506 settled.fail(&self.upper.join(relative), &error);
507 continue;
508 }
509 };
510 let relative = relative.join(entry.file_name());
511 let source = self.upper.join(&relative);
512 let target = self.root.join(&relative);
513 let metadata = match std::fs::symlink_metadata(&source) {
514 Ok(metadata) => metadata,
515 Err(error) => {
516 settled.fail(&source, &error);
517 continue;
518 }
519 };
520 let name = relative.to_string_lossy().into_owned();
521 let kind = metadata.file_type();
522 let existing = std::fs::symlink_metadata(&target).ok();
523 let is_dir = existing.as_ref().is_some_and(|existing| existing.is_dir());
524 let is_link = existing
525 .as_ref()
526 .is_some_and(|existing| existing.is_symlink());
527 if kind.is_char_device() && metadata.rdev() == 0 {
528 if self.allowed(&target) {
530 settled.removed(&target);
531 applied = true;
532 } else if is_dir && self.holds_kept(&target) {
533 applied |= self.prune(&relative, settled);
536 } else {
537 settled.discarded.insert(name);
538 }
539 } else if kind.is_dir() {
540 let replaces = hidden || opaque(&source);
541 if self.allowed(&target) {
542 let prunes = is_dir && replaces && self.holds_bound(&target);
546 if existing.is_some() && (!is_dir || replaces) && !prunes {
547 settled.removed(&target);
548 }
549 let made = std::fs::create_dir_all(&target).and_then(|()| {
552 match std::fs::symlink_metadata(&target)?.is_dir() {
553 true => Ok(()),
554 false => Err(std::io::Error::other("not a directory")),
555 }
556 });
557 match made {
558 Ok(()) => {
559 self.apply(&relative, prunes, settled);
560 let _ = std::fs::set_permissions(&target, metadata.permissions());
563 applied = true;
564 }
565 Err(error) => settled.fail(&target, &error),
566 }
567 } else if is_link {
568 settled.discarded.insert(name);
569 } else if self.apply(&relative, replaces && is_dir, settled) {
570 applied = true;
571 } else if !is_dir {
572 settled.discarded.insert(name);
575 }
576 } else if self.allowed(&target) {
577 if let Some(parent) = target.parent() {
578 let _ = std::fs::create_dir_all(parent);
579 }
580 if is_dir {
581 settled.removed(&target);
582 }
583 if std::fs::rename(&source, &target).is_err() {
586 let _ = remove(&target);
587 let copied = if kind.is_symlink() {
591 std::fs::read_link(&source)
592 .and_then(|link| std::os::unix::fs::symlink(link, &target))
593 } else if kind.is_file() {
594 std::fs::copy(&source, &target).map(|_| ())
595 } else {
596 settled.discarded.insert(name);
597 continue;
598 };
599 if let Err(error) = copied {
600 settled.fail(&target, &error);
601 continue;
602 }
603 }
604 applied = true;
605 } else {
606 settled.discarded.insert(name);
607 }
608 }
609 applied
610 }
611
612 fn prune(&self, relative: &Path, settled: &mut crate::Settled) -> bool {
617 let entries = match std::fs::read_dir(self.root.join(relative)) {
618 Ok(entries) => entries,
619 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return false,
620 Err(error) => {
621 settled.fail(&self.root.join(relative), &error);
622 return false;
623 }
624 };
625 let mut removed = false;
626 for entry in entries {
627 let entry = match entry {
628 Ok(entry) => entry,
629 Err(error) => {
630 settled.fail(&self.root.join(relative), &error);
631 continue;
632 }
633 };
634 let relative = relative.join(entry.file_name());
635 if std::fs::symlink_metadata(self.upper.join(&relative)).is_ok() {
636 continue;
637 }
638 let target = self.root.join(&relative);
639 if self
640 .passthrough
641 .iter()
642 .any(|through| target.starts_with(through))
643 {
644 continue;
645 }
646 let real_dir = entry.file_type().is_ok_and(|kind| kind.is_dir());
647 if real_dir
648 && (self.holds_bound(&target)
649 || (!self.allowed(&target) && self.holds_kept(&target)))
650 {
651 removed |= self.prune(&relative, settled);
652 } else if self.allowed(&target) {
653 settled.removed(&target);
654 removed = true;
655 } else {
656 settled
657 .discarded
658 .insert(relative.to_string_lossy().into_owned());
659 }
660 }
661 removed
662 }
663}
664
665impl crate::Settled {
666 fn fail(&mut self, path: &Path, error: &std::io::Error) {
667 self.failed.push(format!("{}: {error}", path.display()));
668 }
669
670 fn removed(&mut self, target: &Path) {
672 if let Err(error) = remove(target)
673 && error.kind() != std::io::ErrorKind::NotFound
674 {
675 self.fail(target, &error);
676 }
677 }
678}
679
680fn remove(target: &Path) -> std::io::Result<()> {
683 match std::fs::symlink_metadata(target)? {
684 metadata if metadata.is_dir() => std::fs::remove_dir_all(target),
685 _ => std::fs::remove_file(target),
686 }
687}
688
689fn opaque(directory: &Path) -> bool {
692 let Ok(path) = c_path(directory) else {
693 return false;
694 };
695 let mut value = [0u8; 2];
696 let length = unsafe {
697 libc::lgetxattr(
698 path.as_ptr(),
699 c"user.overlay.opaque".as_ptr(),
700 value.as_mut_ptr().cast(),
701 value.len(),
702 )
703 };
704 length == 1 && value[0] == b'y'
705}