Skip to main content

bootc_lib/store/
mod.rs

1//! The [`Storage`] type holds references to three different types of
2//! storage:
3//!
4//! # OSTree
5//!
6//! The default backend for the bootable container store; this
7//! lives in `/ostree` in the physical root.
8//!
9//! # containers-storage:
10//!
11//! Later, bootc gained support for Logically Bound Images.
12//! On ostree systems this is a `containers-storage:` instance that
13//! lives in `/ostree/bootc/storage`.  On composefs systems the
14//! physical location is `/composefs/bootc/storage` with a compat
15//! symlink at `ostree/bootc -> ../composefs/bootc`.
16//!
17//! # composefs
18//!
19//! This lives in `/composefs` in the physical root.
20
21use std::cell::OnceCell;
22use std::ops::Deref;
23use std::sync::Arc;
24
25use anyhow::{Context, Result};
26use bootc_mount::tempmount::TempMount;
27use camino::Utf8PathBuf;
28use cap_std_ext::cap_std;
29use cap_std_ext::cap_std::fs::{
30    Dir, DirBuilder, DirBuilderExt as _, Permissions, PermissionsExt as _,
31};
32use cap_std_ext::dirext::CapStdExtDirExt;
33use fn_error_context::context;
34
35use ostree_ext::container_utils::ostree_booted;
36use ostree_ext::prelude::FileExt;
37use ostree_ext::sysroot::SysrootLock;
38use ostree_ext::{gio, ostree};
39use rustix::fs::Mode;
40
41use composefs::fsverity::Sha512HashValue;
42use composefs_ctl::composefs;
43
44use crate::bootc_composefs::backwards_compat::bcompat_boot::prepend_custom_prefix;
45use crate::bootc_composefs::boot::{EFI_LINUX, mount_esp};
46use crate::bootc_composefs::status::{ComposefsCmdline, composefs_booted, get_bootloader};
47use crate::lsm;
48use crate::podstorage::CStorage;
49use crate::spec::{Bootloader, ImageStatus};
50use crate::utils::{deployment_fd, open_dir_remount_rw};
51
52/// See <https://github.com/containers/composefs-rs/issues/159>
53pub type ComposefsRepository = composefs::repository::Repository<Sha512HashValue>;
54
55/// Path to the physical root
56pub const SYSROOT: &str = "sysroot";
57
58/// The toplevel composefs directory path
59pub const COMPOSEFS: &str = "composefs";
60
61/// The mode for the composefs directory; this is intentionally restrictive
62/// to avoid leaking information.
63pub(crate) const COMPOSEFS_MODE: Mode = Mode::from_raw_mode(0o700);
64
65/// Ensure the composefs directory exists in the given physical root
66/// with the correct permissions (mode 0700).
67pub(crate) fn ensure_composefs_dir(physical_root: &Dir) -> Result<()> {
68    let mut db = DirBuilder::new();
69    db.mode(COMPOSEFS_MODE.as_raw_mode());
70    physical_root
71        .ensure_dir_with(COMPOSEFS, &db)
72        .context("Creating composefs directory")?;
73    // Always update permissions, in case the directory pre-existed
74    // with incorrect mode (e.g. from an older version of bootc).
75    physical_root
76        .set_permissions(
77            COMPOSEFS,
78            Permissions::from_mode(COMPOSEFS_MODE.as_raw_mode()),
79        )
80        .context("Setting composefs directory permissions")?;
81    Ok(())
82}
83
84/// The path to the bootc root directory, relative to the physical
85/// system root.  On ostree systems this is a real directory; on composefs
86/// systems it is a symlink to `../composefs/bootc` (see
87/// [`ensure_composefs_bootc_link`]).
88pub(crate) const BOOTC_ROOT: &str = "ostree/bootc";
89
90/// The "real" bootc root for composefs-native systems, relative to the
91/// physical system root.
92pub(crate) const COMPOSEFS_BOOTC_ROOT: &str = "composefs/bootc";
93
94/// On a composefs install the containers-storage lives under
95/// `composefs/bootc/storage`.  To keep the rest of the code (and the
96/// `/usr/lib/bootc/storage` symlink which points through `ostree/bootc`)
97/// working, we create:
98///
99///   `ostree/bootc -> ../composefs/bootc`
100///
101/// This function is idempotent.
102pub(crate) fn ensure_composefs_bootc_link(physical_root: &Dir) -> Result<()> {
103    // Ensure the real directory exists
104    physical_root
105        .create_dir_all(COMPOSEFS_BOOTC_ROOT)
106        .with_context(|| format!("Creating {COMPOSEFS_BOOTC_ROOT}"))?;
107
108    // Create the `ostree/` parent if needed (it won't exist on a pure
109    // composefs install that never touched ostree).
110    physical_root
111        .create_dir_all("ostree")
112        .context("Creating ostree directory")?;
113
114    // If ostree/bootc already exists as a real directory (e.g. from an
115    // older install or from the ostree path), leave it alone — this
116    // function is only for fresh composefs installs.
117    match physical_root.symlink_metadata(BOOTC_ROOT) {
118        Ok(meta) if meta.is_symlink() => {
119            // Already a symlink — nothing to do
120            return Ok(());
121        }
122        Ok(_meta) => {
123            // It's a real directory.  This shouldn't happen during a fresh
124            // composefs install, but if it does just leave it.
125            tracing::warn!(
126                "{BOOTC_ROOT} already exists as a directory, not replacing with symlink"
127            );
128            return Ok(());
129        }
130        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
131            // Good — doesn't exist yet, we'll create the symlink
132        }
133        Err(e) => return Err(e).context(format!("Querying {BOOTC_ROOT}")),
134    }
135
136    physical_root
137        .symlink_contents(format!("../{COMPOSEFS_BOOTC_ROOT}"), BOOTC_ROOT)
138        .with_context(|| format!("Creating {BOOTC_ROOT} -> ../{COMPOSEFS_BOOTC_ROOT} symlink"))?;
139
140    tracing::info!("Created {BOOTC_ROOT} -> ../{COMPOSEFS_BOOTC_ROOT}");
141    Ok(())
142}
143
144/// Storage accessor for a booted system.
145///
146/// This wraps [`Storage`] and can determine whether the system is booted
147/// via ostree or composefs, providing a unified interface for both.
148pub(crate) struct BootedStorage {
149    pub(crate) storage: Storage,
150}
151
152impl Deref for BootedStorage {
153    type Target = Storage;
154
155    fn deref(&self) -> &Self::Target {
156        &self.storage
157    }
158}
159
160/// Represents an ostree-based boot environment
161pub struct BootedOstree<'a> {
162    pub(crate) sysroot: &'a SysrootLock,
163    pub(crate) deployment: ostree::Deployment,
164}
165
166impl<'a> BootedOstree<'a> {
167    /// Get the ostree repository
168    pub(crate) fn repo(&self) -> ostree::Repo {
169        self.sysroot.repo()
170    }
171
172    /// Get the stateroot name
173    pub(crate) fn stateroot(&self) -> ostree::glib::GString {
174        self.deployment.osname()
175    }
176}
177
178/// Represents a composefs-based boot environment
179#[allow(dead_code)]
180pub struct BootedComposefs {
181    pub repo: Arc<ComposefsRepository>,
182    pub cmdline: &'static ComposefsCmdline,
183}
184
185/// Discriminated union representing the boot storage backend.
186///
187/// The runtime environment in which bootc is executing.
188pub(crate) enum Environment {
189    /// System booted via ostree
190    OstreeBooted,
191    /// System booted via composefs
192    ComposefsBooted(ComposefsCmdline),
193    /// Running in a container
194    Container,
195    /// Other (not booted via bootc)
196    Other,
197}
198
199impl Environment {
200    /// Detect the current runtime environment.
201    pub(crate) fn detect() -> Result<Self> {
202        if ostree_ext::container_utils::running_in_container() {
203            return Ok(Self::Container);
204        }
205
206        if let Some(cmdline) = composefs_booted()? {
207            return Ok(Self::ComposefsBooted(cmdline.clone()));
208        }
209
210        if ostree_booted()? {
211            return Ok(Self::OstreeBooted);
212        }
213
214        Ok(Self::Other)
215    }
216
217    /// Returns true if this environment requires entering a mount namespace
218    /// before loading storage (to avoid leaving /sysroot writable).
219    pub(crate) fn needs_mount_namespace(&self) -> bool {
220        matches!(self, Self::OstreeBooted | Self::ComposefsBooted(_))
221    }
222}
223
224/// A system can boot via either ostree or composefs; this enum
225/// allows code to handle both cases while maintaining type safety.
226pub(crate) enum BootedStorageKind<'a> {
227    Ostree(BootedOstree<'a>),
228    Composefs(BootedComposefs),
229}
230
231/// Open the physical root (/sysroot) and /run directories for a booted system.
232fn get_physical_root_and_run() -> Result<(Dir, Dir)> {
233    let physical_root = {
234        let d = Dir::open_ambient_dir("/sysroot", cap_std::ambient_authority())
235            .context("Opening /sysroot")?;
236        open_dir_remount_rw(&d, ".".into())?
237    };
238    let run =
239        Dir::open_ambient_dir("/run", cap_std::ambient_authority()).context("Opening /run")?;
240    Ok((physical_root, run))
241}
242
243impl BootedStorage {
244    /// Create a new booted storage accessor for the given environment.
245    ///
246    /// The caller must have already called `prepare_for_write()` if
247    /// `env.needs_mount_namespace()` is true.
248    pub(crate) async fn new(env: Environment) -> Result<Option<Self>> {
249        let r = match &env {
250            Environment::ComposefsBooted(cmdline) => {
251                let (physical_root, run) = get_physical_root_and_run()?;
252                let mut composefs = ComposefsRepository::open_path(&physical_root, COMPOSEFS)?;
253                if cmdline.allow_missing_fsverity {
254                    composefs.set_insecure();
255                }
256                let composefs = Arc::new(composefs);
257
258                // Locate ESP by walking up to the root disk(s)
259                let root_dev = bootc_blockdev::list_dev_by_dir(&physical_root)?;
260                let esp_dev = root_dev.find_first_colocated_esp()?;
261                let esp_mount = mount_esp(&esp_dev.path())?;
262
263                let boot_dir = match get_bootloader()? {
264                    Bootloader::Grub => physical_root.open_dir("boot").context("Opening boot")?,
265                    // NOTE: Handle XBOOTLDR partitions here if and when we use it
266                    Bootloader::Systemd => esp_mount.fd.try_clone().context("Cloning fd")?,
267                    Bootloader::None => unreachable!("Checked at install time"),
268                };
269
270                let storage = Storage {
271                    physical_root,
272                    physical_root_path: Utf8PathBuf::from("/sysroot"),
273                    run,
274                    boot_dir: Some(boot_dir),
275                    esp: Some(esp_mount),
276                    ostree: Default::default(),
277                    composefs: OnceCell::from(composefs.clone()),
278                    imgstore: Default::default(),
279                };
280
281                // prepend_custom_prefix is idempotent: it checks has_prefix on each
282                // entry and skips any that already have it, so it's safe to call on
283                // every boot. This handles upgrades from older bootc versions that
284                // lacked the prefix — we can't use meta.json presence as a trigger
285                // because open_upgrade() in the initramfs writes meta.json before
286                // userspace ever runs.
287                let cmdline = composefs_booted()?
288                    .ok_or_else(|| anyhow::anyhow!("Could not get booted composefs cmdline"))?;
289                prepend_custom_prefix(&storage, &cmdline).await?;
290
291                Some(Self { storage })
292            }
293            Environment::OstreeBooted => {
294                // The caller must have entered a private mount namespace before
295                // calling this function. This is because ostree's sysroot.load() will
296                // remount /sysroot as writable, and we call set_mount_namespace_in_use()
297                // to indicate we're in a mount namespace. Without actually being in a
298                // mount namespace, this would leave the global /sysroot writable.
299                let (physical_root, run) = get_physical_root_and_run()?;
300
301                let sysroot = ostree::Sysroot::new_default();
302                sysroot.set_mount_namespace_in_use();
303                let sysroot = ostree_ext::sysroot::SysrootLock::new_from_sysroot(&sysroot).await?;
304                sysroot.load(gio::Cancellable::NONE)?;
305
306                let storage = Storage {
307                    physical_root,
308                    physical_root_path: Utf8PathBuf::from("/sysroot"),
309                    run,
310                    boot_dir: None,
311                    esp: None,
312                    ostree: OnceCell::from(sysroot),
313                    composefs: Default::default(),
314                    imgstore: Default::default(),
315                };
316
317                Some(Self { storage })
318            }
319            // For container or non-bootc environments, there's no storage
320            Environment::Container | Environment::Other => None,
321        };
322        Ok(r)
323    }
324
325    /// Determine the boot storage backend kind.
326    ///
327    /// Returns information about whether the system booted via ostree or composefs,
328    /// along with the relevant sysroot/deployment or repository/cmdline data.
329    pub(crate) fn kind(&self) -> Result<BootedStorageKind<'_>> {
330        if let Some(cmdline) = composefs_booted()? {
331            // SAFETY: This must have been set above in new()
332            let repo = self.composefs.get().unwrap();
333            Ok(BootedStorageKind::Composefs(BootedComposefs {
334                repo: Arc::clone(repo),
335                cmdline,
336            }))
337        } else {
338            // SAFETY: This must have been set above in new()
339            let sysroot = self.ostree.get().unwrap();
340            let deployment = sysroot.require_booted_deployment()?;
341            Ok(BootedStorageKind::Ostree(BootedOstree {
342                sysroot,
343                deployment,
344            }))
345        }
346    }
347}
348
349/// A reference to a physical filesystem root, plus
350/// accessors for the different types of container storage.
351pub(crate) struct Storage {
352    /// Directory holding the physical root
353    pub physical_root: Dir,
354
355    /// Absolute path to the physical root directory.
356    /// This is `/sysroot` on a running system, or the target mount point during install.
357    pub physical_root_path: Utf8PathBuf,
358
359    /// The 'boot' directory, useful and `Some` only for composefs systems
360    /// For grub booted systems, this points to `/sysroot/boot`
361    /// For systemd booted systems, this points to the ESP
362    pub boot_dir: Option<Dir>,
363
364    /// The ESP mounted at a tmp location
365    pub esp: Option<TempMount>,
366
367    /// Our runtime state
368    run: Dir,
369
370    /// The OSTree storage
371    ostree: OnceCell<SysrootLock>,
372    /// The composefs storage
373    composefs: OnceCell<Arc<ComposefsRepository>>,
374    /// The containers-image storage used for LBIs
375    imgstore: OnceCell<CStorage>,
376}
377
378/// Cached image status data used for optimization.
379///
380/// This stores the current image status and any cached update information
381/// to avoid redundant fetches during status operations.
382#[derive(Default)]
383pub(crate) struct CachedImageStatus {
384    pub image: Option<ImageStatus>,
385    pub cached_update: Option<ImageStatus>,
386}
387
388impl Storage {
389    /// Create a new storage accessor from an existing ostree sysroot.
390    ///
391    /// This is used for non-booted scenarios (e.g., `bootc install`) where
392    /// we're operating on a target filesystem rather than the running system.
393    pub fn new_ostree(sysroot: SysrootLock, run: &Dir) -> Result<Self> {
394        let run = run.try_clone()?;
395
396        // ostree has historically always relied on
397        // having ostree -> sysroot/ostree as a symlink in the image to
398        // make it so that code doesn't need to distinguish between booted
399        // vs offline target. The ostree code all just looks at the ostree/
400        // directory, and will follow the link in the booted case.
401        //
402        // For composefs we aren't going to do a similar thing, so here
403        // we need to explicitly distinguish the two and the storage
404        // here hence holds a reference to the physical root.
405        let ostree_sysroot_dir = crate::utils::sysroot_dir(&sysroot)?;
406        let (physical_root, physical_root_path) = if sysroot.is_booted() {
407            (
408                ostree_sysroot_dir.open_dir(SYSROOT)?,
409                Utf8PathBuf::from("/sysroot"),
410            )
411        } else {
412            // For non-booted case (install), get the path from the sysroot
413            let path = sysroot.path();
414            let path_str = path.parse_name().to_string();
415            let path = Utf8PathBuf::from(path_str);
416            (ostree_sysroot_dir, path)
417        };
418
419        let ostree_cell = OnceCell::new();
420        let _ = ostree_cell.set(sysroot);
421
422        Ok(Self {
423            physical_root,
424            physical_root_path,
425            run,
426            boot_dir: None,
427            esp: None,
428            ostree: ostree_cell,
429            composefs: Default::default(),
430            imgstore: Default::default(),
431        })
432    }
433
434    /// Returns `boot_dir` if it exists
435    pub(crate) fn require_boot_dir(&self) -> Result<&Dir> {
436        self.boot_dir
437            .as_ref()
438            .ok_or_else(|| anyhow::anyhow!("Boot dir not found"))
439    }
440
441    /// Returns the mounted `esp` if it exists
442    pub(crate) fn require_esp(&self) -> Result<&TempMount> {
443        self.esp
444            .as_ref()
445            .ok_or_else(|| anyhow::anyhow!("ESP not found"))
446    }
447
448    /// Returns the Directory where the Type1 boot binaries are stored
449    /// `/sysroot/boot` for Grub, and ESP/EFI/Linux for systemd-boot
450    pub(crate) fn bls_boot_binaries_dir(&self) -> Result<Dir> {
451        let boot_dir = self.require_boot_dir()?;
452
453        // boot dir in case of systemd-boot points to the ESP, but we store
454        // the actual binaries inside ESP/EFI/Linux
455        let boot_dir = match get_bootloader()? {
456            Bootloader::Grub => boot_dir.try_clone()?,
457            Bootloader::Systemd => {
458                let boot_dir = boot_dir
459                    .open_dir(EFI_LINUX)
460                    .with_context(|| format!("Opening {EFI_LINUX}"))?;
461
462                boot_dir
463            }
464            Bootloader::None => anyhow::bail!("Unknown bootloader"),
465        };
466
467        Ok(boot_dir)
468    }
469
470    /// Access the underlying ostree repository
471    pub(crate) fn get_ostree(&self) -> Result<&SysrootLock> {
472        self.ostree
473            .get()
474            .ok_or_else(|| anyhow::anyhow!("OSTree storage not initialized"))
475    }
476
477    /// Get a cloned reference to the ostree sysroot.
478    ///
479    /// This is used when code needs an owned `ostree::Sysroot` rather than
480    /// a reference to the `SysrootLock`.
481    pub(crate) fn get_ostree_cloned(&self) -> Result<ostree::Sysroot> {
482        let r = self.get_ostree()?;
483        Ok((*r).clone())
484    }
485
486    /// Access the image storage; will automatically initialize it if necessary.
487    ///
488    /// Works on both ostree and composefs-only systems.  On ostree the
489    /// SELinux policy is loaded from the booted deployment; on composefs
490    /// (where ostree isn't initialized) we fall back to the host root policy.
491    pub(crate) fn get_ensure_imgstore(&self) -> Result<&CStorage> {
492        if let Some(imgstore) = self.imgstore.get() {
493            return Ok(imgstore);
494        }
495
496        let (sysroot_dir, sepolicy) = if let Ok(ostree) = self.get_ostree() {
497            let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
498            let sepolicy = if ostree.booted_deployment().is_none() {
499                tracing::trace!("falling back to container root's selinux policy");
500                let container_root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
501                lsm::new_sepolicy_at(&container_root)?
502            } else {
503                tracing::trace!("loading sepolicy from booted ostree deployment");
504                let dep = ostree.booted_deployment().unwrap();
505                let dep_fs = deployment_fd(ostree, &dep)?;
506                lsm::new_sepolicy_at(&dep_fs)?
507            };
508            (sysroot_dir, sepolicy)
509        } else {
510            // Composefs-only: ostree is not initialized. Use the physical
511            // root directly and load SELinux policy from the host root.
512            let sysroot_dir = self.physical_root.try_clone()?;
513            let root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
514            let sepolicy = lsm::new_sepolicy_at(&root)?;
515            (sysroot_dir, sepolicy)
516        };
517
518        tracing::trace!("sepolicy in get_ensure_imgstore: {sepolicy:?}");
519
520        let imgstore = CStorage::create(&sysroot_dir, &self.run, sepolicy.as_ref())?;
521        Ok(self.imgstore.get_or_init(|| imgstore))
522    }
523
524    /// Ensure the image storage is properly SELinux-labeled. This should be
525    /// called after all image pulls are complete.
526    pub(crate) fn ensure_imgstore_labeled(&self) -> Result<()> {
527        if let Some(imgstore) = self.imgstore.get() {
528            imgstore.ensure_labeled()?;
529        }
530        Ok(())
531    }
532
533    /// Access the composefs repository; will automatically initialize it if necessary.
534    ///
535    /// This lazily opens the composefs repository, creating the directory if needed
536    /// and bootstrapping verity settings from the ostree configuration.
537    pub(crate) fn get_ensure_composefs(&self) -> Result<Arc<ComposefsRepository>> {
538        if let Some(composefs) = self.composefs.get() {
539            return Ok(Arc::clone(composefs));
540        }
541
542        ensure_composefs_dir(&self.physical_root)?;
543
544        // Bootstrap verity off of the ostree state. In practice this means disabled by
545        // default right now.
546        let ostree = self.get_ostree()?;
547        let ostree_repo = &ostree.repo();
548        let ostree_verity = ostree_ext::fsverity::is_verity_enabled(ostree_repo)?;
549        let (mut composefs, _created) = ComposefsRepository::init_path(
550            self.physical_root.open_dir(COMPOSEFS)?,
551            ".",
552            composefs::fsverity::Algorithm::SHA512,
553            ostree_verity.enabled,
554        )?;
555        if !ostree_verity.enabled {
556            tracing::debug!("Setting insecure mode for composefs repo");
557            composefs.set_insecure();
558        }
559        let composefs = Arc::new(composefs);
560        let r = Arc::clone(self.composefs.get_or_init(|| composefs));
561        Ok(r)
562    }
563
564    /// Update the mtime on the storage root directory.
565    ///
566    /// This touches `ostree/bootc` (or its symlink target on composefs
567    /// systems) so that `bootc-status-updated.path` fires.
568    #[context("Updating storage root mtime")]
569    pub(crate) fn update_mtime(&self) -> Result<()> {
570        // On composefs-only systems ostree is not initialized, so fall
571        // back to the physical root directly.
572        let sysroot_dir = if let Ok(ostree) = self.get_ostree() {
573            crate::utils::sysroot_dir(ostree).context("Reopen sysroot directory")?
574        } else {
575            self.physical_root.try_clone()?
576        };
577
578        sysroot_dir
579            .update_timestamps(std::path::Path::new(BOOTC_ROOT))
580            .context("update_timestamps")
581    }
582}
583
584#[cfg(test)]
585mod tests {
586    use super::*;
587
588    /// The raw mode returned by metadata includes file type bits (S_IFDIR,
589    /// etc.) in addition to permission bits. This constant masks to only
590    /// the permission bits (owner/group/other rwx).
591    const PERMS: Mode = Mode::from_raw_mode(0o777);
592
593    #[test]
594    fn test_ensure_composefs_dir_mode() -> Result<()> {
595        use cap_std_ext::cap_primitives::fs::PermissionsExt as _;
596
597        let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
598
599        let assert_mode = || -> Result<()> {
600            let perms = td.metadata(COMPOSEFS)?.permissions();
601            let mode = Mode::from_raw_mode(perms.mode());
602            assert_eq!(mode & PERMS, COMPOSEFS_MODE);
603            Ok(())
604        };
605
606        ensure_composefs_dir(&td)?;
607        assert_mode()?;
608
609        // Calling again should be a no-op (ensure is idempotent)
610        ensure_composefs_dir(&td)?;
611        assert_mode()?;
612
613        Ok(())
614    }
615
616    #[test]
617    fn test_ensure_composefs_dir_fixes_existing() -> Result<()> {
618        use cap_std_ext::cap_primitives::fs::PermissionsExt as _;
619
620        let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
621
622        // Create with overly permissive mode (simulating old bootc behavior)
623        let mut db = DirBuilder::new();
624        db.mode(0o755);
625        td.create_dir_with(COMPOSEFS, &db)?;
626
627        // Verify it starts with wrong permissions
628        let perms = td.metadata(COMPOSEFS)?.permissions();
629        let mode = Mode::from_raw_mode(perms.mode());
630        assert_eq!(mode & PERMS, Mode::from_raw_mode(0o755));
631
632        // ensure_composefs_dir should fix the permissions
633        ensure_composefs_dir(&td)?;
634
635        let perms = td.metadata(COMPOSEFS)?.permissions();
636        let mode = Mode::from_raw_mode(perms.mode());
637        assert_eq!(mode & PERMS, COMPOSEFS_MODE);
638
639        Ok(())
640    }
641}