Skip to main content

qk_executor/
overlay.rs

1//! Running a command in its own user and mount namespace over an overlay of
2//! the workspace: what it writes there lands in an upper layer of its own,
3//! which the sandbox reads when the task ends.
4
5use 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
12/// The layers of one slot of commands: `upper` and `work`, the overlay's, and
13/// `real`, where the workspace is bound aside so that it can be the lower
14/// layer.
15pub 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
24/// What the child does between fork and exec, prepared beforehand: it may
25/// make system calls only.
26pub struct Setup {
27    uid_map: CString,
28    gid_map: CString,
29    workspace: CString,
30    real: CString,
31    options: CString,
32    /// The descriptor the workspace bound aside is held at while the
33    /// overlay is mounted, as `real` may lie in the workspace, in qk's
34    /// state, where the overlay hides it: one reserved here, so that no
35    /// descriptor the child inherits has its number.
36    aside: std::os::fd::OwnedFd,
37    /// Each path bound through, as it lies in the workspace bound aside,
38    /// reached through `aside`, and in the workspace.
39    passthrough: Vec<(CString, CString)>,
40    cwd: CString,
41    /// The Landlock rule set's handled and file rights, the descriptors
42    /// opened outside with their rights, the workspace's rights, and each
43    /// path in the workspace, relative to it, with its rights and whether to
44    /// reach it through no link.
45    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            // The mount options separate with `,` and lower layers with `:`.
75            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        // The same user and group inside as outside, so files keep their
87        // owner.
88        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    /// Enters the namespaces and mounts the overlay. Between fork and exec:
137    /// system calls only, no allocation.
138    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            // In place of the reservation, which `aside` cannot be.
183            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            // The directory it was in is now beneath the overlay.
212            if libc::chdir(self.cwd.as_ptr()) != 0 {
213                return fail();
214            }
215            self.restrict()?;
216        }
217        Ok(())
218    }
219
220    /// Builds the Landlock rule set over the mounted overlay and restricts
221    /// the command to it. Between fork and exec: system calls only.
222    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                    // One that does not exist, or that a link now stands in,
292                    // is not allowed, as outside the overlay.
293                    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
314/// Whether a command can run over an overlay here: in a user and mount
315/// namespace of its own, with an overlay mounted on a directory in
316/// `directory`, which lies on the workspace's file system. The reason it
317/// cannot, if not.
318pub 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    // Which step failed, as the child can only return an error code: written
337    // to a pipe the parent reads.
338    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
413/// Removes an overlay's directory, whose work directory overlayfs leaves
414/// unreadable.
415pub 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
429/// Applies to the workspace what each command's upper layer holds within
430/// the overlay's `keep`, entry by entry. Entry by entry, since an overlay
431/// another task has mounted would keep seeing a directory replaced whole as
432/// it was.
433pub 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
453/// One command's upper layer, applied to the workspace at `root`.
454struct Layer<'a> {
455    upper: &'a Path,
456    root: &'a Path,
457    /// Where the task may write.
458    keep: &'a [std::path::PathBuf],
459    /// Bound through the overlay: what lies there the layer never holds.
460    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    /// Whether something bound through the overlay lies beneath `target`.
469    fn holds_bound(&self, target: &Path) -> bool {
470        self.passthrough
471            .iter()
472            .any(|through| through.starts_with(target))
473    }
474
475    /// Whether something the task may write lies beneath `target`.
476    fn holds_kept(&self, target: &Path) -> bool {
477        self.keep.iter().any(|keep| keep.starts_with(target))
478    }
479
480    /// Applies what the layer holds under `relative`, adding what it may
481    /// not write to `settled.discarded` and what could not be applied to
482    /// `settled.failed`. A whiteout removes; an opaque directory, or one
483    /// beneath it, which hides what the workspace holds there as `hidden`
484    /// says, replaces what was there; anything else is moved into place.
485    /// Nothing is reached through a symbolic link in the workspace: a command
486    /// could leave one in an output pointing out of it, which a later
487    /// command's directory would be applied through. A link where the layer
488    /// holds a directory is replaced by it, as the command did, or, outside
489    /// what it may write, keeps the directory's entries out. `relative`
490    /// itself holds no link: each directory is checked before what it holds
491    /// is applied. Returns whether it applied anything.
492    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                // A whiteout.
529                if self.allowed(&target) {
530                    settled.removed(&target);
531                    applied = true;
532                } else if is_dir && self.holds_kept(&target) {
533                    // A directory above outputs is gone, and so is what it
534                    // held of them.
535                    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                    // A file or link in its place goes, as the command
543                    // removed it; a directory only when the layer's replaces
544                    // it whole. What is bound through beneath it stays.
545                    let prunes = is_dir && replaces && self.holds_bound(&target);
546                    if existing.is_some() && (!is_dir || replaces) && !prunes {
547                        settled.removed(&target);
548                    }
549                    // What lies above holds no link, so neither does what
550                    // this makes.
551                    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                            // After its entries, which a read-only mode
561                            // would keep out.
562                            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                    // A directory it made outside its outputs, holding
573                    // nothing of them.
574                    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                // A rename replaces a link at the target rather than follow
584                // it.
585                if std::fs::rename(&source, &target).is_err() {
586                    let _ = remove(&target);
587                    // Copied as it is: a link the command made is never
588                    // followed by qk, which Landlock does not hold, to a
589                    // file the command could not read.
590                    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    /// Beneath a directory the layer replaces whole or removes: removes what
613    /// the workspace holds under `relative` and the layer does not, where the
614    /// task may write, and adds the rest to `settled.discarded`. What is
615    /// bound through the overlay stays: the layer never holds it.
616    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    /// Removes `target`, noting a failure unless it is already gone.
671    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
680/// Removes a file, a link itself, or a directory with what it holds,
681/// following no link.
682fn 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
689/// Whether overlayfs marked a directory of its upper layer opaque: it hides
690/// what the lower layer holds there.
691fn 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}