Skip to content

Commit 38e6528

Browse files
authored
Merge pull request #1001 from cgwalters/wipe-no-xdev
install: Never traverse mount points with `--wipe`
2 parents 86a06ec + 84cd0f8 commit 38e6528

File tree

3 files changed

+54
-49
lines changed

3 files changed

+54
-49
lines changed

lib/src/install.rs

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use camino::Utf8Path;
2828
use camino::Utf8PathBuf;
2929
use cap_std::fs::{Dir, MetadataExt};
3030
use cap_std_ext::cap_std;
31+
use cap_std_ext::cap_std::fs::FileType;
3132
use cap_std_ext::cap_std::fs_utf8::DirEntry as DirEntryUtf8;
3233
use cap_std_ext::cap_tempfile::TempDir;
3334
use cap_std_ext::cmdext::CapStdExtCommandExt;
@@ -56,7 +57,7 @@ use crate::progress_jsonl::ProgressWriter;
5657
use crate::spec::ImageReference;
5758
use crate::store::Storage;
5859
use crate::task::Task;
59-
use crate::utils::sigpolicy_from_opts;
60+
use crate::utils::{open_dir_noxdev, sigpolicy_from_opts};
6061

6162
/// The toplevel boot directory
6263
const BOOT: &str = "boot";
@@ -1558,14 +1559,22 @@ fn require_empty_rootdir(rootfs_fd: &Dir) -> Result<()> {
15581559
}
15591560

15601561
/// Remove all entries in a directory, but do not traverse across distinct devices.
1562+
/// If mount_err is true, then an error is returned if a mount point is found;
1563+
/// otherwise it is silently ignored.
15611564
#[context("Removing entries (noxdev)")]
1562-
fn remove_all_in_dir_no_xdev(d: &Dir) -> Result<()> {
1563-
let parent_dev = d.dir_metadata()?.dev();
1565+
fn remove_all_in_dir_no_xdev(d: &Dir, mount_err: bool) -> Result<()> {
15641566
for entry in d.entries()? {
15651567
let entry = entry?;
1566-
let entry_dev = entry.metadata()?.dev();
1567-
if entry_dev == parent_dev {
1568-
d.remove_all_optional(entry.file_name())?;
1568+
let name = entry.file_name();
1569+
let etype = entry.file_type()?;
1570+
if etype == FileType::dir() {
1571+
if let Some(subdir) = open_dir_noxdev(d, &name)? {
1572+
remove_all_in_dir_no_xdev(&subdir, mount_err)?;
1573+
} else if mount_err {
1574+
anyhow::bail!("Found unexpected mount point {name:?}");
1575+
}
1576+
} else {
1577+
d.remove_file_optional(&name)?;
15691578
}
15701579
}
15711580
anyhow::Ok(())
@@ -1576,13 +1585,15 @@ fn clean_boot_directories(rootfs: &Dir) -> Result<()> {
15761585
let bootdir =
15771586
crate::utils::open_dir_remount_rw(rootfs, BOOT.into()).context("Opening /boot")?;
15781587
// This should not remove /boot/efi note.
1579-
remove_all_in_dir_no_xdev(&bootdir)?;
1588+
remove_all_in_dir_no_xdev(&bootdir, false)?;
1589+
// TODO: Discover the ESP the same way bootupd does it; we should also
1590+
// support not wiping the ESP.
15801591
if ARCH_USES_EFI {
15811592
if let Some(efidir) = bootdir
15821593
.open_dir_optional(crate::bootloader::EFI_DIR)
15831594
.context("Opening /boot/efi")?
15841595
{
1585-
remove_all_in_dir_no_xdev(&efidir)?;
1596+
remove_all_in_dir_no_xdev(&efidir, false)?;
15861597
}
15871598
}
15881599
Ok(())
@@ -1730,14 +1741,8 @@ pub(crate) async fn install_to_filesystem(
17301741
Some(ReplaceMode::Wipe) => {
17311742
let rootfs_fd = rootfs_fd.try_clone()?;
17321743
println!("Wiping contents of root");
1733-
tokio::task::spawn_blocking(move || {
1734-
for e in rootfs_fd.entries()? {
1735-
let e = e?;
1736-
rootfs_fd.remove_all_optional(e.file_name())?;
1737-
}
1738-
anyhow::Ok(())
1739-
})
1740-
.await??;
1744+
tokio::task::spawn_blocking(move || remove_all_in_dir_no_xdev(&rootfs_fd, true))
1745+
.await??;
17411746
}
17421747
Some(ReplaceMode::Alongside) => clean_boot_directories(&rootfs_fd)?,
17431748
None => require_empty_rootdir(&rootfs_fd)?,

lib/src/lints.rs

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ use cap_std_ext::cap_std;
1111
use cap_std_ext::dirext::CapStdExtDirExt as _;
1212
use fn_error_context::context;
1313

14-
use crate::utils::openat2_with_retry;
15-
1614
/// Reference to embedded default baseimage content that should exist.
1715
const BASEIMAGE_REF: &str = "usr/share/doc/bootc/baseimage/base";
1816

@@ -72,25 +70,6 @@ fn check_kernel(root: &Dir) -> Result<()> {
7270
Ok(())
7371
}
7472

75-
/// Open the target directory, but return Ok(None) if this would cross a mount point.
76-
fn open_dir_noxdev(
77-
parent: &Dir,
78-
path: impl AsRef<std::path::Path>,
79-
) -> std::io::Result<Option<Dir>> {
80-
use rustix::fs::{Mode, OFlags, ResolveFlags};
81-
match openat2_with_retry(
82-
parent,
83-
path,
84-
OFlags::CLOEXEC | OFlags::DIRECTORY | OFlags::NOFOLLOW,
85-
Mode::empty(),
86-
ResolveFlags::NO_XDEV | ResolveFlags::BENEATH,
87-
) {
88-
Ok(r) => Ok(Some(Dir::reopen_dir(&r)?)),
89-
Err(e) if e == rustix::io::Errno::XDEV => Ok(None),
90-
Err(e) => return Err(e.into()),
91-
}
92-
}
93-
9473
fn check_utf8(dir: &Dir) -> Result<()> {
9574
for entry in dir.entries()? {
9675
let entry = entry?;
@@ -109,7 +88,7 @@ fn check_utf8(dir: &Dir) -> Result<()> {
10988
"/{strname}: Found non-utf8 symlink target"
11089
);
11190
} else if ifmt.is_dir() {
112-
let Some(subdir) = open_dir_noxdev(dir, entry.file_name())? else {
91+
let Some(subdir) = crate::utils::open_dir_noxdev(dir, entry.file_name())? else {
11392
continue;
11493
};
11594
if let Err(err) = check_utf8(&subdir) {
@@ -181,17 +160,6 @@ mod tests {
181160
Ok(tempdir)
182161
}
183162

184-
#[test]
185-
fn test_open_noxdev() -> Result<()> {
186-
let root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
187-
// This hard requires the host setup to have /usr/bin on the same filesystem as /
188-
let usr = Dir::open_ambient_dir("/usr", cap_std::ambient_authority())?;
189-
assert!(open_dir_noxdev(&usr, "bin").unwrap().is_some());
190-
// Requires a mounted /proc, but that also seems ane.
191-
assert!(open_dir_noxdev(&root, "proc").unwrap().is_none());
192-
Ok(())
193-
}
194-
195163
#[test]
196164
fn test_var_run() -> Result<()> {
197165
let root = &fixture()?;

lib/src/utils.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,25 @@ pub(crate) fn open_dir_remount_rw(root: &Dir, target: &Utf8Path) -> Result<Dir>
110110
root.open_dir(target).map_err(anyhow::Error::new)
111111
}
112112

113+
/// Open the target directory, but return Ok(None) if this would cross a mount point.
114+
pub fn open_dir_noxdev(
115+
parent: &Dir,
116+
path: impl AsRef<std::path::Path>,
117+
) -> std::io::Result<Option<Dir>> {
118+
use rustix::fs::{Mode, OFlags, ResolveFlags};
119+
match openat2_with_retry(
120+
parent,
121+
path,
122+
OFlags::CLOEXEC | OFlags::DIRECTORY | OFlags::NOFOLLOW,
123+
Mode::empty(),
124+
ResolveFlags::NO_XDEV | ResolveFlags::BENEATH,
125+
) {
126+
Ok(r) => Ok(Some(Dir::reopen_dir(&r)?)),
127+
Err(e) if e == rustix::io::Errno::XDEV => Ok(None),
128+
Err(e) => return Err(e.into()),
129+
}
130+
}
131+
113132
/// Given a target path, remove its immutability if present
114133
#[context("Removing immutable flag from {target}")]
115134
pub(crate) fn remove_immutability(root: &Dir, target: &Utf8Path) -> Result<()> {
@@ -223,6 +242,8 @@ pub(crate) fn digested_pullspec(image: &str, digest: &str) -> String {
223242

224243
#[cfg(test)]
225244
mod tests {
245+
use cap_std_ext::cap_std;
246+
226247
use super::*;
227248

228249
#[test]
@@ -269,4 +290,15 @@ mod tests {
269290
SignatureSource::ContainerPolicyAllowInsecure
270291
);
271292
}
293+
294+
#[test]
295+
fn test_open_noxdev() -> Result<()> {
296+
let root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
297+
// This hard requires the host setup to have /usr/bin on the same filesystem as /
298+
let usr = Dir::open_ambient_dir("/usr", cap_std::ambient_authority())?;
299+
assert!(open_dir_noxdev(&usr, "bin").unwrap().is_some());
300+
// Requires a mounted /proc, but that also seems ane.
301+
assert!(open_dir_noxdev(&root, "proc").unwrap().is_none());
302+
Ok(())
303+
}
272304
}

0 commit comments

Comments
 (0)