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
12use crate::landlock::{self, Rights};
13
14pub fn layers(overlay: &crate::Overlay, slot: usize) -> [std::path::PathBuf; 3] {
18 let command = overlay.directory.join(format!("command-{slot}"));
19 ["upper", "work", "real"].map(|name| command.join(name))
20}
21
22fn c_path(path: &Path) -> Result<CString> {
23 CString::new(path.as_os_str().as_bytes()).context("a path holds a NUL byte")
24}
25
26pub struct Setup {
29 uid_map: CString,
30 gid_map: CString,
31 workspace: CString,
32 real: CString,
33 options: CString,
34 aside: std::os::fd::OwnedFd,
39 passthrough: Vec<CString>,
44 cwd: CString,
45 handled: Rights,
50 outside: Vec<(i32, Rights)>,
51 workspace_rights: Rights,
52 inside: Vec<(CString, Rights, bool)>,
53}
54
55impl Setup {
56 pub fn new(overlay: &crate::Overlay, slot: usize, cwd: &Path) -> Result<Self> {
57 let [upper, work, real] = layers(overlay, slot);
58 for directory in [&upper, &work, &real] {
59 std::fs::create_dir_all(directory)
60 .with_context(|| format!("cannot create {}", directory.display()))?;
61 }
62 let text = |path: &Path| -> Result<String> {
63 let text = path.to_str().context("an overlay's paths must be UTF-8")?;
64 if text.contains([',', ':', '\\']) {
66 bail!("an overlay cannot be mounted at {text}");
67 }
68 Ok(text.to_owned())
69 };
70 let options = format!(
71 "lowerdir={},upperdir={},workdir={},userxattr",
72 text(&real)?,
73 text(&upper)?,
74 text(&work)?
75 );
76 let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
79 let aside: std::os::fd::OwnedFd = std::fs::File::open("/dev/null")
80 .context("cannot reserve a descriptor")?
81 .into();
82 let mut passthrough = Vec::new();
83 for path in &overlay.passthrough {
84 let relative = path
85 .strip_prefix(&overlay.workspace)
86 .context("a path bound through lies outside the workspace")?;
87 passthrough.push(
88 landlock::Open::Directory
89 .name(relative)
90 .context("a path holds a NUL byte")?,
91 );
92 }
93 let rules = &overlay.rules;
94 let mut inside = Vec::new();
95 for (path, rights, open) in &rules.inside {
96 let Ok(relative) = path.strip_prefix(&overlay.workspace) else {
97 continue;
98 };
99 let relative = if relative.as_os_str().is_empty() {
100 Path::new(".")
101 } else {
102 relative
103 };
104 let name = open.name(relative).context("a path holds a NUL byte")?;
105 inside.push((name, *rights, *open != landlock::Open::Following));
106 }
107 Ok(Self {
108 aside,
109 handled: rules.handled,
110 outside: rules
111 .outside
112 .iter()
113 .map(|(fd, rights)| (fd.as_raw_fd(), *rights))
114 .collect(),
115 workspace_rights: rules.workspace,
116 inside,
117 uid_map: CString::new(format!("{uid} {uid} 1"))?,
118 gid_map: CString::new(format!("{gid} {gid} 1"))?,
119 workspace: c_path(&overlay.workspace)?,
120 real: c_path(&real)?,
121 options: CString::new(options)?,
122 passthrough,
123 cwd: c_path(cwd)?,
124 })
125 }
126
127 pub fn enter(&self) -> std::io::Result<()> {
130 let fail = || Err(std::io::Error::last_os_error());
131 unsafe {
132 if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNS) != 0 {
133 return fail();
134 }
135 for (file, text) in [
136 (c"/proc/self/setgroups", c"deny"),
137 (c"/proc/self/uid_map", self.uid_map.as_c_str()),
138 (c"/proc/self/gid_map", self.gid_map.as_c_str()),
139 ] {
140 let fd = libc::open(file.as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC);
141 if fd < 0 {
142 return fail();
143 }
144 let bytes = text.to_bytes();
145 let written = libc::write(fd, bytes.as_ptr().cast(), bytes.len());
146 libc::close(fd);
147 if written < 0 {
148 return fail();
149 }
150 }
151 let none = std::ptr::null();
152 if libc::mount(
153 none,
154 c"/".as_ptr(),
155 none,
156 libc::MS_REC | libc::MS_PRIVATE,
157 none.cast(),
158 ) != 0
159 || libc::mount(
160 self.workspace.as_ptr(),
161 self.real.as_ptr(),
162 none,
163 libc::MS_BIND | libc::MS_REC,
164 none.cast(),
165 ) != 0
166 {
167 return fail();
168 }
169 let aside = libc::open(
170 self.real.as_ptr(),
171 libc::O_PATH | libc::O_DIRECTORY | libc::O_CLOEXEC,
172 );
173 let held = self.aside.as_raw_fd();
175 if aside < 0 || libc::dup3(aside, held, libc::O_CLOEXEC) < 0 {
176 return fail();
177 }
178 libc::close(aside);
179 if libc::mount(
180 c"overlay".as_ptr(),
181 self.workspace.as_ptr(),
182 c"overlay".as_ptr(),
183 0,
184 self.options.as_ptr().cast(),
185 ) != 0
186 {
187 return fail();
188 }
189 let merged = libc::open(
190 self.workspace.as_ptr(),
191 libc::O_PATH | libc::O_DIRECTORY | libc::O_CLOEXEC,
192 );
193 if merged < 0 {
194 return fail();
195 }
196 for relative in &self.passthrough {
197 let ends = landlock::open_beneath(held, relative)
201 .and_then(|source| Ok((source, landlock::open_beneath(merged, relative)?)));
202 let (source, target) = match ends {
203 Ok(ends) => ends,
204 Err(error)
205 if matches!(
206 error.raw_os_error(),
207 Some(libc::ENOENT | libc::ELOOP | libc::EXDEV | libc::ENOTDIR)
208 ) =>
209 {
210 continue;
211 }
212 Err(error) => return Err(error),
216 };
217 let (mut source_name, mut target_name) = ([0u8; 32], [0u8; 32]);
218 if libc::mount(
219 proc_fd(source.as_raw_fd(), &mut source_name).as_ptr(),
220 proc_fd(target.as_raw_fd(), &mut target_name).as_ptr(),
221 none,
222 libc::MS_BIND | libc::MS_REC,
223 none.cast(),
224 ) != 0
225 {
226 return fail();
227 }
228 }
229 libc::close(merged);
230 libc::close(held);
231 if libc::chdir(self.cwd.as_ptr()) != 0 {
233 return fail();
234 }
235 self.restrict()?;
236 }
237 Ok(())
238 }
239
240 unsafe fn restrict(&self) -> std::io::Result<()> {
243 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
244 let ruleset = landlock::create_ruleset(self.handled)?;
245 let add = |fd: i32, rights: Rights| {
246 landlock::add_rule(ruleset.as_raw_fd(), fd, rights, self.handled)
247 };
248 for (fd, rights) in &self.outside {
249 add(*fd, *rights)?;
250 }
251 let root = unsafe {
252 libc::open(
253 self.workspace.as_ptr(),
254 libc::O_PATH | libc::O_DIRECTORY | libc::O_CLOEXEC,
255 )
256 };
257 if root < 0 {
258 return Err(std::io::Error::last_os_error());
259 }
260 let root = unsafe { OwnedFd::from_raw_fd(root) };
262 add(root.as_raw_fd(), self.workspace_rights)?;
263 for (relative, rights, unlinked) in &self.inside {
264 let opened = if *unlinked {
265 landlock::open_beneath(root.as_raw_fd(), relative)
266 } else {
267 let fd = unsafe {
268 libc::openat(
269 root.as_raw_fd(),
270 relative.as_ptr(),
271 libc::O_PATH | libc::O_CLOEXEC,
272 )
273 };
274 if fd < 0 {
275 Err(std::io::Error::last_os_error())
276 } else {
277 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
279 }
280 };
281 let fd = match opened {
282 Ok(fd) => fd,
283 Err(error)
286 if matches!(
287 error.raw_os_error(),
288 Some(libc::ENOENT | libc::ELOOP | libc::EXDEV | libc::ENOTDIR)
289 ) =>
290 {
291 continue;
292 }
293 Err(error) => return Err(error),
294 };
295 add(fd.as_raw_fd(), *rights)?;
296 }
297 drop(root);
298 landlock::restrict_self(ruleset.as_raw_fd())
299 }
300}
301
302fn proc_fd(fd: i32, buffer: &mut [u8; 32]) -> &std::ffi::CStr {
306 const PREFIX: &[u8] = b"/proc/self/fd/";
307 buffer[..PREFIX.len()].copy_from_slice(PREFIX);
308 let mut digits = [0u8; 10];
309 let mut count = 0;
310 let mut rest = fd.unsigned_abs();
311 loop {
312 digits[count] = b'0' + (rest % 10) as u8;
313 count += 1;
314 rest /= 10;
315 if rest == 0 {
316 break;
317 }
318 }
319 for (index, digit) in digits[..count].iter().rev().enumerate() {
320 buffer[PREFIX.len() + index] = *digit;
321 }
322 buffer[PREFIX.len() + count] = 0;
323 std::ffi::CStr::from_bytes_until_nul(&buffer[..]).unwrap_or(c"/proc/self/fd")
324}
325
326pub fn probe(directory: &Path) -> Result<()> {
331 use std::os::unix::process::CommandExt;
332 let [lower, upper, work, merged] =
333 ["lower", "upper", "work", "merged"].map(|name| directory.join(name));
334 for path in [&lower, &upper, &work, &merged] {
335 std::fs::create_dir_all(path)
336 .with_context(|| format!("cannot create {}", path.display()))?;
337 }
338 let options = CString::new(format!(
339 "lowerdir={},upperdir={},workdir={},userxattr",
340 lower.display(),
341 upper.display(),
342 work.display()
343 ))?;
344 let merged_path = c_path(&merged)?;
345 let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
346 let uid_map = CString::new(format!("{uid} {uid} 1"))?;
347 let gid_map = CString::new(format!("{gid} {gid} 1"))?;
348 let mut pipe = [0i32; 2];
351 if unsafe { libc::pipe2(pipe.as_mut_ptr(), libc::O_CLOEXEC) } != 0 {
352 return Err(std::io::Error::last_os_error()).context("cannot make a pipe");
353 }
354 let (reader, writer) = (pipe[0], pipe[1]);
355 let mut command = std::process::Command::new("/bin/sh");
356 command
357 .args(["-c", "exit 0"])
358 .stdin(std::process::Stdio::null())
359 .stdout(std::process::Stdio::null())
360 .stderr(std::process::Stdio::null());
361 unsafe {
362 command.pre_exec(move || {
363 let fail = |step: u8| {
364 let error = std::io::Error::last_os_error();
365 libc::write(writer, (&step as *const u8).cast(), 1);
366 Err(error)
367 };
368 if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNS) != 0 {
369 return fail(0);
370 }
371 for (step, (file, text)) in [
372 (c"/proc/self/setgroups", c"deny"),
373 (c"/proc/self/uid_map", uid_map.as_c_str()),
374 (c"/proc/self/gid_map", gid_map.as_c_str()),
375 ]
376 .into_iter()
377 .enumerate()
378 {
379 let fd = libc::open(file.as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC);
380 if fd < 0 {
381 return fail(1 + step as u8);
382 }
383 let bytes = text.to_bytes();
384 let written = libc::write(fd, bytes.as_ptr().cast(), bytes.len());
385 libc::close(fd);
386 if written < 0 {
387 return fail(1 + step as u8);
388 }
389 }
390 if libc::mount(
391 c"overlay".as_ptr(),
392 merged_path.as_ptr(),
393 c"overlay".as_ptr(),
394 0,
395 options.as_ptr().cast(),
396 ) != 0
397 {
398 return fail(4);
399 }
400 Ok(())
401 });
402 }
403 let output = command.output();
404 unsafe { libc::close(writer) };
405 let mut step = [u8::MAX];
406 let read = unsafe { libc::read(reader, step.as_mut_ptr().cast(), 1) };
407 unsafe { libc::close(reader) };
408 clear(directory);
409 let what = match (read, step[0]) {
410 (1, 0) => "making a user and mount namespace",
411 (1, 1) => "denying setgroups",
412 (1, 2) => "mapping the user",
413 (1, 3) => "mapping the group",
414 (1, 4) => "mounting an overlay",
415 _ => "running a command over an overlay",
416 };
417 let where_ = directory.display();
418 match output {
419 Ok(output) if output.status.success() => Ok(()),
420 Ok(output) => bail!("{what} in {where_} failed: {}", output.status),
421 Err(error) => Err(error).with_context(|| format!("{what} in {where_} failed")),
422 }
423}
424
425pub fn clear(directory: &Path) {
428 use std::os::unix::fs::PermissionsExt;
429 let mut stack = vec![directory.to_owned()];
430 while let Some(path) = stack.pop() {
431 if std::fs::symlink_metadata(&path).is_ok_and(|metadata| metadata.is_dir()) {
432 let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700));
433 if let Ok(entries) = std::fs::read_dir(&path) {
434 stack.extend(entries.flatten().map(|entry| entry.path()));
435 }
436 }
437 }
438 let _ = std::fs::remove_dir_all(directory);
439}
440
441pub fn settle(overlay: &crate::Overlay) -> crate::Settled {
446 let mut settled = crate::Settled::default();
447 for slot in 0.. {
448 let [upper, ..] = layers(overlay, slot);
449 if !upper.parent().is_some_and(Path::exists) {
450 break;
451 }
452 if upper.is_dir() {
453 let layer = Layer {
454 upper: &upper,
455 root: &overlay.workspace,
456 keep: &overlay.keep,
457 passthrough: &overlay.passthrough,
458 };
459 layer.apply(Path::new(""), false, &mut settled);
460 }
461 }
462 settled
463}
464
465struct Layer<'a> {
467 upper: &'a Path,
468 root: &'a Path,
469 keep: &'a crate::Keep,
471 passthrough: &'a [std::path::PathBuf],
473}
474
475impl Layer<'_> {
476 fn allowed(&self, target: &Path) -> bool {
477 self.keep.allows(self.root, target)
478 }
479
480 fn holds_bound(&self, target: &Path) -> bool {
482 self.passthrough
483 .iter()
484 .any(|through| through.starts_with(target))
485 }
486
487 fn holds_kept(&self, target: &Path) -> bool {
489 self.keep.holds(target)
490 }
491
492 fn names(&self, relative: &Path) -> bool {
495 let named = |part: &std::ffi::OsStr| self.keep.names.iter().any(|name| part == name);
496 relative.file_name().is_some_and(named)
497 && relative
498 .parent()
499 .is_none_or(|parent| !parent.components().any(|part| named(part.as_os_str())))
500 }
501
502 fn apply(&self, relative: &Path, hidden: bool, settled: &mut crate::Settled) -> bool {
515 use std::os::unix::fs::{FileTypeExt, MetadataExt};
516 let mut applied = hidden && self.prune(relative, settled);
517 let entries = match std::fs::read_dir(self.upper.join(relative)) {
518 Ok(entries) => entries,
519 Err(error) => {
520 settled.fail(&self.upper.join(relative), &error);
521 return applied;
522 }
523 };
524 for entry in entries {
525 let entry = match entry {
526 Ok(entry) => entry,
527 Err(error) => {
528 settled.fail(&self.upper.join(relative), &error);
529 continue;
530 }
531 };
532 let relative = relative.join(entry.file_name());
533 let source = self.upper.join(&relative);
534 let target = self.root.join(&relative);
535 let metadata = match std::fs::symlink_metadata(&source) {
536 Ok(metadata) => metadata,
537 Err(error) => {
538 settled.fail(&source, &error);
539 continue;
540 }
541 };
542 let name = relative.to_string_lossy().into_owned();
543 let kind = metadata.file_type();
544 let existing = std::fs::symlink_metadata(&target).ok();
545 let is_dir = existing.as_ref().is_some_and(|existing| existing.is_dir());
546 let is_link = existing
547 .as_ref()
548 .is_some_and(|existing| existing.is_symlink());
549 if kind.is_char_device() && metadata.rdev() == 0 {
550 if self.allowed(&target) {
552 settled.removed(&target);
553 applied = true;
554 } else if is_dir && self.holds_kept(&target) {
555 applied |= self.prune(&relative, settled);
558 } else {
559 settled.discarded.insert(name);
560 }
561 } else if kind.is_dir() {
562 let replaces = hidden || opaque(&source);
563 if self.allowed(&target) {
564 let prunes = is_dir && replaces && self.holds_bound(&target);
568 if existing.is_some() && (!is_dir || replaces) && !prunes {
569 settled.removed(&target);
570 }
571 let made = std::fs::create_dir_all(&target).and_then(|()| {
574 match std::fs::symlink_metadata(&target)?.is_dir() {
575 true => Ok(()),
576 false => Err(std::io::Error::other("not a directory")),
577 }
578 });
579 match made {
580 Ok(()) => {
581 if !is_dir && self.names(&relative) {
582 settled.made.push(target.clone());
583 }
584 self.apply(&relative, prunes, settled);
585 let _ = std::fs::set_permissions(&target, metadata.permissions());
588 applied = true;
589 }
590 Err(error) => settled.fail(&target, &error),
591 }
592 } else if is_link {
593 settled.discarded.insert(name);
594 } else if self.apply(&relative, replaces && is_dir, settled) {
595 applied = true;
596 } else if !is_dir {
597 settled.discarded.insert(name);
600 }
601 } else if self.allowed(&target) {
602 if let Some(parent) = target.parent() {
603 let _ = std::fs::create_dir_all(parent);
604 }
605 if is_dir {
606 settled.removed(&target);
607 }
608 if std::fs::rename(&source, &target).is_err() {
611 let _ = remove(&target);
612 let copied = if kind.is_symlink() {
616 std::fs::read_link(&source)
617 .and_then(|link| std::os::unix::fs::symlink(link, &target))
618 } else if kind.is_file() {
619 std::fs::copy(&source, &target).map(|_| ())
620 } else {
621 settled.discarded.insert(name);
622 continue;
623 };
624 if let Err(error) = copied {
625 settled.fail(&target, &error);
626 continue;
627 }
628 }
629 applied = true;
630 } else {
631 settled.discarded.insert(name);
632 }
633 }
634 applied
635 }
636
637 fn prune(&self, relative: &Path, settled: &mut crate::Settled) -> bool {
642 let entries = match std::fs::read_dir(self.root.join(relative)) {
643 Ok(entries) => entries,
644 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return false,
645 Err(error) => {
646 settled.fail(&self.root.join(relative), &error);
647 return false;
648 }
649 };
650 let mut removed = false;
651 for entry in entries {
652 let entry = match entry {
653 Ok(entry) => entry,
654 Err(error) => {
655 settled.fail(&self.root.join(relative), &error);
656 continue;
657 }
658 };
659 let relative = relative.join(entry.file_name());
660 if std::fs::symlink_metadata(self.upper.join(&relative)).is_ok() {
661 continue;
662 }
663 let target = self.root.join(&relative);
664 if self
665 .passthrough
666 .iter()
667 .any(|through| target.starts_with(through))
668 {
669 continue;
670 }
671 let real_dir = entry.file_type().is_ok_and(|kind| kind.is_dir());
672 if real_dir
673 && (self.holds_bound(&target)
674 || (!self.allowed(&target) && self.holds_kept(&target)))
675 {
676 removed |= self.prune(&relative, settled);
677 } else if self.allowed(&target) {
678 settled.removed(&target);
679 removed = true;
680 } else {
681 settled
682 .discarded
683 .insert(relative.to_string_lossy().into_owned());
684 }
685 }
686 removed
687 }
688}
689
690impl crate::Settled {
691 fn fail(&mut self, path: &Path, error: &std::io::Error) {
692 self.failed.push(format!("{}: {error}", path.display()));
693 }
694
695 fn removed(&mut self, target: &Path) {
697 if let Err(error) = remove(target)
698 && error.kind() != std::io::ErrorKind::NotFound
699 {
700 self.fail(target, &error);
701 }
702 }
703}
704
705fn remove(target: &Path) -> std::io::Result<()> {
708 match std::fs::symlink_metadata(target)? {
709 metadata if metadata.is_dir() => std::fs::remove_dir_all(target),
710 _ => std::fs::remove_file(target),
711 }
712}
713
714fn opaque(directory: &Path) -> bool {
717 let Ok(path) = c_path(directory) else {
718 return false;
719 };
720 let mut value = [0u8; 2];
721 let length = unsafe {
722 libc::lgetxattr(
723 path.as_ptr(),
724 c"user.overlay.opaque".as_ptr(),
725 value.as_mut_ptr().cast(),
726 value.len(),
727 )
728 };
729 length == 1 && value[0] == b'y'
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735
736 fn write(upper: &Path, relative: &str) {
738 let path = upper.join(relative);
739 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
740 std::fs::write(path, relative).unwrap();
741 }
742
743 #[test]
744 fn writes_a_descriptors_path_without_allocating() {
745 let mut buffer = [0u8; 32];
746 assert_eq!(proc_fd(7, &mut buffer), c"/proc/self/fd/7");
747 assert_eq!(
748 proc_fd(1234567890, &mut buffer),
749 c"/proc/self/fd/1234567890"
750 );
751 }
752
753 #[test]
758 fn binds_no_link_put_in_place_of_a_directory_bound_through() {
759 let temp = tempfile::tempdir().unwrap();
760 let root = temp.path().canonicalize().unwrap();
761 if let Err(error) = probe(&root.join("probe")) {
762 eprintln!("skipped: an overlay cannot be mounted here: {error:#}");
763 return;
764 }
765 let workspace = root.join("workspace");
766 for directory in ["swapped", "kept", "secret"] {
767 std::fs::create_dir_all(workspace.join(directory)).unwrap();
768 }
769 let Ok(abi) = landlock::abi() else {
770 eprintln!("skipped: Landlock is not available");
771 return;
772 };
773 let everything: std::os::fd::OwnedFd = std::fs::File::open("/").unwrap().into();
774 let overlay = crate::Overlay {
775 workspace: workspace.clone(),
776 directory: root.join("overlay"),
777 passthrough: vec![workspace.join("swapped"), workspace.join("kept")],
778 rules: crate::OverlayRules {
779 handled: Rights::handled(abi),
780 outside: vec![(everything, Rights::ALL)],
781 workspace: Rights::ALL,
782 inside: Vec::new(),
783 },
784 keep: crate::Keep::default(),
785 settled: std::sync::OnceLock::new(),
786 };
787 let setup = Setup::new(&overlay, 0, &workspace).unwrap();
788 std::fs::remove_dir(workspace.join("swapped")).unwrap();
790 std::os::unix::fs::symlink("secret", workspace.join("swapped")).unwrap();
791 let mut command = std::process::Command::new("/bin/sh");
792 command
793 .args(["-c", "echo x > swapped/written && echo x > kept/written"])
794 .current_dir(&workspace);
795 unsafe {
796 std::os::unix::process::CommandExt::pre_exec(&mut command, move || setup.enter());
797 }
798 let status = command.status().unwrap();
799 assert!(status.success());
800 let [upper, ..] = layers(&overlay, 0);
801 assert!(!workspace.join("secret/written").exists());
802 assert!(upper.join("secret/written").is_file());
803 assert!(workspace.join("kept/written").is_file());
804 clear(&overlay.directory);
805 }
806
807 #[test]
810 fn keeps_what_is_written_in_any_node_modules() {
811 let temp = tempfile::tempdir().unwrap();
812 let workspace = temp.path().join("workspace");
813 for file in [
814 "lib/node_modules/stale/a",
815 "app/node_modules/old/a",
816 "filed/node_modules",
817 "stubbed",
818 "elsewhere/kept",
819 ] {
820 write(&workspace, file);
821 }
822 std::fs::create_dir_all(workspace.join("linked")).unwrap();
823 std::os::unix::fs::symlink(workspace.join("lib"), workspace.join("linked/node_modules"))
824 .unwrap();
825 std::os::unix::fs::symlink(workspace.join("elsewhere"), workspace.join("tied")).unwrap();
826 let overlay = crate::Overlay {
827 workspace: workspace.clone(),
828 directory: temp.path().join("overlay"),
829 passthrough: Vec::new(),
830 rules: crate::OverlayRules::default(),
831 keep: crate::Keep {
832 paths: vec![workspace.join("dist")],
833 names: vec!["node_modules".into()],
834 },
835 settled: std::sync::OnceLock::new(),
836 };
837 let [upper, ..] = layers(&overlay, 0);
838 for file in [
841 "pkg/node_modules/x/a",
842 "pkg/stray.js",
843 "dist/out.js",
844 "filed/node_modules/x/a",
845 "linked/node_modules/x/a",
846 "stubbed/node_modules/x/a",
848 "tied/node_modules/x/a",
849 ] {
850 write(&upper, file);
851 }
852 std::fs::create_dir_all(upper.join("app/node_modules")).unwrap();
855 let whiteout = c_path(&upper.join("app/node_modules/old")).unwrap();
856 let whited = unsafe { libc::mknod(whiteout.as_ptr(), libc::S_IFCHR, 0) } == 0;
857 write(&upper, "lib/node_modules/fresh/a");
858 let lib = c_path(&upper.join("lib/node_modules")).unwrap();
859 let opaque = unsafe {
860 libc::lsetxattr(
861 lib.as_ptr(),
862 c"user.overlay.opaque".as_ptr(),
863 b"y".as_ptr().cast(),
864 1,
865 0,
866 )
867 } == 0;
868 let settled = overlay.settle();
869 assert_eq!(settled.failed.len(), 1, "{:?}", settled.failed);
872 assert!(
873 settled.failed[0].contains("stubbed/node_modules"),
874 "{:?}",
875 settled.failed
876 );
877 assert_eq!(
878 settled.discarded,
879 std::collections::BTreeSet::from(
880 ["pkg/stray.js", "stubbed", "tied"].map(str::to_owned)
881 )
882 );
883 assert!(workspace.join("stubbed").is_file());
884 assert!(!workspace.join("elsewhere/node_modules").exists());
885 let mut made = settled.made.clone();
886 made.sort();
887 assert_eq!(
888 made,
889 [
890 "filed/node_modules",
891 "linked/node_modules",
892 "pkg/node_modules"
893 ]
894 .map(|path| workspace.join(path))
895 );
896 assert!(!workspace.join("lib/x").exists());
898 for file in [
899 "pkg/node_modules/x/a",
900 "dist/out.js",
901 "lib/node_modules/fresh/a",
902 "filed/node_modules/x/a",
903 "linked/node_modules/x/a",
904 ] {
905 assert!(workspace.join(file).is_file(), "{file}");
906 }
907 assert!(!workspace.join("pkg/stray.js").exists());
908 if whited {
909 assert!(!workspace.join("app/node_modules/old").exists());
910 } else {
911 eprintln!("skipped: a whiteout cannot be made here");
912 }
913 if opaque {
914 assert!(!workspace.join("lib/node_modules/stale").exists());
915 } else {
916 eprintln!("skipped: an opaque directory cannot be marked here");
917 }
918 }
919}