composefs/
mountcompat.rs

1//! Compatibility helpers for older Linux kernel mount APIs.
2//!
3//! This module provides fallback implementations for mount operations
4//! on kernels that don't support the modern mount API, including
5//! loopback device setup and temporary mount handling.
6
7use std::{
8    io::Result,
9    os::fd::{AsFd, BorrowedFd, OwnedFd},
10};
11
12// This file contains a bunch of helpers that deal with the pre-6.15 mount API
13
14// First: the simple pass-through versions of all of our helpers, for 6.15 or later, along with
15// documentation about why they're required.
16
17/// Sets one of the "dir" mount options on an overlayfs to the given file descriptor.  This can
18/// either be a freshly-created mount or a O_PATH file descriptor.  On 6.15 kernels this can be
19/// done by directly calling `fsconfig_set_fd()`.  On pre-6.15 kernels, it needs to be done by
20/// reopening the file descriptor `O_RDONLY` and calling `fsconfig_set_fd()` because `O_PATH` fds
21/// are rejected.  On very old kernels this needs to be done by way of `fsconfig_set_string()` and
22/// `/proc/self/fd/`.
23#[cfg(not(feature = "pre-6.15"))]
24pub fn overlayfs_set_fd(fs_fd: BorrowedFd, key: &str, fd: BorrowedFd) -> rustix::io::Result<()> {
25    rustix::mount::fsconfig_set_fd(fs_fd, key, fd)
26}
27
28/// Sets the "lowerdir+" and "datadir+" mount options of an overlayfs mount to the provided file
29/// descriptors.  On 6.15 kernels this can be done by directly calling `fsconfig_set_fd()`.  On
30/// pre-6.15 kernels, it needs to be done by reopening the file descriptor `O_RDONLY` and calling
31/// `fsconfig_set_fd()` because `O_PATH` fds are rejected.  On very old kernels this needs to be
32/// done by calculating a `"lowerdir=lower::data"` string using `/proc/self/fd/` filenames and
33/// setting it via `fsconfig_set_string()`.
34#[cfg(not(feature = "rhel9"))]
35pub fn overlayfs_set_lower_and_data_fds(
36    fs_fd: impl AsFd,
37    lower: impl AsFd,
38    data: Option<impl AsFd>,
39) -> rustix::io::Result<()> {
40    overlayfs_set_fd(fs_fd.as_fd(), "lowerdir+", lower.as_fd())?;
41    if let Some(data) = data {
42        overlayfs_set_fd(fs_fd.as_fd(), "datadir+", data.as_fd())?;
43    }
44    Ok(())
45}
46
47/// Prepares an open erofs image file for mounting.  On kernels versions after 6.12 this is a
48/// simple passthrough.  On older kernels (like on RHEL 9) we need to create a loopback device.
49#[cfg(not(feature = "rhel9"))]
50pub fn make_erofs_mountable(image: OwnedFd) -> Result<OwnedFd> {
51    Ok(image)
52}
53
54/// Prepares a mounted filesystem for further use.  On 6.15 kernels this is a no-op, due to the
55/// expanded number of operations which can be performed on "detached" mounts.  On earlier kernels
56/// we need to create a temporary directory and mount the filesystem there to avoid failures,
57/// making sure to detach the mount and remove the directory later.  This function returns an `impl
58/// AsFd` which also implements the `Drop` trait in order to facilitate this cleanup.
59#[cfg(not(feature = "pre-6.15"))]
60pub fn prepare_mount(mnt_fd: OwnedFd) -> Result<impl AsFd> {
61    Ok(mnt_fd)
62}
63
64// Now: support for pre-6.15 kernels
65/// Sets one of the "dir" mount options on an overlayfs to the given file descriptor.
66///
67/// On pre-6.15 kernels, this implementation reopens O_PATH file descriptors as O_RDONLY
68/// before passing them to `fsconfig_set_fd()` because O_PATH fds are rejected by the kernel.
69#[cfg(feature = "pre-6.15")]
70#[cfg(not(feature = "rhel9"))]
71pub fn overlayfs_set_fd(fs_fd: BorrowedFd, key: &str, fd: BorrowedFd) -> rustix::io::Result<()> {
72    use rustix::fs::{openat, Mode, OFlags};
73    use rustix::mount::fsconfig_set_fd;
74
75    // We have support for setting fds but not O_PATH ones...
76    fsconfig_set_fd(
77        fs_fd,
78        key,
79        openat(
80            fd,
81            ".",
82            OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
83            Mode::empty(),
84        )?
85        .as_fd(),
86    )
87}
88
89/// Sets one of the "dir" mount options on an overlayfs to the given file descriptor.
90///
91/// On RHEL9 kernels, file descriptors cannot be set directly, so this implementation
92/// uses `fsconfig_set_string()` with a `/proc/self/fd/` path instead.
93#[cfg(feature = "rhel9")]
94pub fn overlayfs_set_fd(fs_fd: BorrowedFd, key: &str, fd: BorrowedFd) -> rustix::io::Result<()> {
95    rustix::mount::fsconfig_set_string(fs_fd, key, crate::util::proc_self_fd(fd))
96}
97
98/// Sets the "lowerdir+" and "datadir+" mount options of an overlayfs mount to the provided file
99/// descriptors.
100///
101/// On RHEL9 kernels, this constructs a `lowerdir` string using `/proc/self/fd/` paths and
102/// sets it via `fsconfig_set_string()` because file descriptors cannot be set directly.
103#[cfg(feature = "rhel9")]
104pub fn overlayfs_set_lower_and_data_fds(
105    fs_fd: impl AsFd,
106    lower: impl AsFd,
107    data: Option<impl AsFd>,
108) -> rustix::io::Result<()> {
109    use std::os::fd::AsRawFd;
110
111    let lower_fd = lower.as_fd().as_raw_fd().to_string();
112    let arg = if let Some(data) = data {
113        let data_fd = data.as_fd().as_raw_fd().to_string();
114        format!("/proc/self/fd/{lower_fd}::/proc/self/fd/{data_fd}")
115    } else {
116        format!("/proc/self/fd/{lower_fd}")
117    };
118    rustix::mount::fsconfig_set_string(fs_fd.as_fd(), "lowerdir", arg)
119}
120
121/// Prepares a mounted filesystem for further use.
122///
123/// On pre-6.15 kernels, this mounts the filesystem to a temporary directory and returns
124/// an O_PATH file descriptor to it. The temporary mount is automatically cleaned up when
125/// the returned value is dropped.
126#[cfg(feature = "pre-6.15")]
127pub fn prepare_mount(mnt_fd: OwnedFd) -> Result<impl AsFd> {
128    tmpmount::TmpMount::mount(mnt_fd)
129}
130
131/// Prepares an open erofs image file for mounting.
132///
133/// On RHEL9 kernels (before 6.12), this creates a loopback device because erofs
134/// cannot directly mount files on these older kernels.
135#[cfg(feature = "rhel9")]
136pub fn make_erofs_mountable(image: OwnedFd) -> Result<OwnedFd> {
137    loopback::loopify(image)
138}
139
140// Finally, we have two submodules which do the heavy lifting for loopback devices and temporary
141// mountpoints.
142
143// Required before Linux 6.15: it's not possible to use detached mounts with OPEN_TREE_CLONE or
144// overlayfs.  Convert them into a non-floating form by mounting them on a temporary directory and
145// reopening them as an O_PATH fd.
146#[cfg(feature = "pre-6.15")]
147mod tmpmount {
148    use std::{
149        io::Result,
150        os::fd::{AsFd, BorrowedFd, OwnedFd},
151    };
152
153    use rustix::fs::{open, Mode, OFlags};
154    use rustix::mount::{move_mount, unmount, MoveMountFlags, UnmountFlags};
155
156    pub(super) struct TmpMount {
157        dir: tempfile::TempDir,
158        fd: OwnedFd,
159    }
160
161    impl TmpMount {
162        pub(super) fn mount(mnt_fd: OwnedFd) -> Result<impl AsFd> {
163            let tmp = tempfile::TempDir::new()?;
164            move_mount(
165                mnt_fd.as_fd(),
166                "",
167                rustix::fs::CWD,
168                tmp.path(),
169                MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
170            )?;
171            let fd = open(
172                tmp.path(),
173                OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
174                Mode::empty(),
175            )?;
176            Ok(TmpMount { dir: tmp, fd })
177        }
178    }
179
180    impl AsFd for TmpMount {
181        fn as_fd(&self) -> BorrowedFd<'_> {
182            self.fd.as_fd()
183        }
184    }
185
186    impl Drop for TmpMount {
187        fn drop(&mut self) {
188            let _ = unmount(self.dir.path(), UnmountFlags::DETACH);
189        }
190    }
191}
192
193/// Required before 6.12: erofs can't directly mount files.
194#[cfg(feature = "rhel9")]
195mod loopback {
196    #![allow(unsafe_code)]
197    use std::{
198        io::Result,
199        os::fd::{AsFd, AsRawFd, OwnedFd},
200    };
201
202    use rustix::fs::{open, Mode, OFlags};
203
204    struct LoopCtlGetFree;
205
206    // Rustix seems to lack a built-in pattern for an ioctl that returns data by the syscall return
207    // value instead of the usual return-by-reference on the args parameter.  Bake our own.
208    unsafe impl rustix::ioctl::Ioctl for LoopCtlGetFree {
209        type Output = std::ffi::c_int;
210
211        const IS_MUTATING: bool = false;
212
213        fn opcode(&self) -> rustix::ioctl::Opcode {
214            LOOP_CTL_GET_FREE
215        }
216
217        fn as_ptr(&mut self) -> *mut std::ffi::c_void {
218            std::ptr::null_mut()
219        }
220
221        unsafe fn output_from_ptr(
222            out: rustix::ioctl::IoctlOutput,
223            _ptr: *mut std::ffi::c_void,
224        ) -> rustix::io::Result<std::ffi::c_int> {
225            Ok(out)
226        }
227    }
228
229    const LO_NAME_SIZE: usize = 64;
230    const LO_KEY_SIZE: usize = 32;
231
232    #[derive(Default)]
233    #[repr(C)]
234    struct LoopInfo {
235        lo_device: u64,
236        lo_inode: u64,
237        lo_rdevice: u64,
238        lo_offset: u64,
239        lo_sizelimit: u64,
240        lo_number: u32,
241        lo_encrypt_type: u32,
242        lo_encrypt_key_size: u32,
243        lo_flags: u32,
244        // HACK: default trait is only implemented up to [u8; 32]
245        lo_file_name: ([u8; LO_NAME_SIZE / 2], [u8; LO_NAME_SIZE / 2]),
246        lo_crypt_name: ([u8; LO_NAME_SIZE / 2], [u8; LO_NAME_SIZE / 2]),
247        lo_encrypt_key: [u8; LO_KEY_SIZE],
248        lo_init: [u64; 2],
249    }
250
251    #[derive(Default)]
252    #[repr(C)]
253    struct LoopConfig {
254        fd: u32,
255        block_size: u32,
256        info: LoopInfo,
257        reserved: [u64; 8],
258    }
259
260    const LOOP_CTL_GET_FREE: rustix::ioctl::Opcode = 0x4C82;
261    const LOOP_CONFIGURE: rustix::ioctl::Opcode = 0x4C0A;
262    const LO_FLAGS_READ_ONLY: u32 = 1;
263    const LO_FLAGS_AUTOCLEAR: u32 = 4;
264    const LO_FLAGS_DIRECT_IO: u32 = 16;
265
266    pub fn loopify(image: OwnedFd) -> Result<OwnedFd> {
267        let control = open(
268            "/dev/loop-control",
269            OFlags::RDWR | OFlags::CLOEXEC,
270            Mode::empty(),
271        )?;
272        let index = unsafe { rustix::ioctl::ioctl(&control, LoopCtlGetFree {})? };
273        let fd = open(
274            format!("/dev/loop{index}"),
275            OFlags::RDWR | OFlags::CLOEXEC,
276            Mode::empty(),
277        )?;
278        let config = LoopConfig {
279            fd: image.as_fd().as_raw_fd() as u32,
280            block_size: 4096,
281            info: LoopInfo {
282                lo_flags: LO_FLAGS_READ_ONLY | LO_FLAGS_AUTOCLEAR | LO_FLAGS_DIRECT_IO,
283                ..LoopInfo::default()
284            },
285            ..LoopConfig::default()
286        };
287        unsafe {
288            rustix::ioctl::ioctl(
289                &fd,
290                rustix::ioctl::Setter::<{ LOOP_CONFIGURE }, LoopConfig>::new(config),
291            )?;
292        };
293        Ok(fd)
294    }
295}