deb: skip unreadable paths while snapshotting the staged parent

The snapshot walk runs over the whole staged PARENT, which contains
trees unrelated to the build; one unreadable path anywhere under it —
a root-owned sysroot left there by the user's own setup script — made
fs::read_dir or symlink_metadata fail and aborted the whole build
seven seconds in, before any staging could happen. The walk is now
best-effort below its root: unreadable directories and entries lost to
a mid-walk race are warned about and skipped, and only the root itself
being unreadable propagates (the package tree being invisible is a
real error). Absent entries are already the handled case for the
whiteout sync: they are simply untracked.
This commit is contained in:
2026-09-29 14:57:39 +02:00
parent 6a8c301705
commit ab0c65dc37
+76 -2
View File
@@ -1518,13 +1518,40 @@ impl EntryMeta {
/// Walk a host tree, mapping slash-separated relative paths to metadata.
/// Symlinks are recorded as themselves (not followed).
///
/// Best-effort below the root: a directory that cannot be read or an
/// entry that cannot be stat'd (a root-owned sibling tree in the staged
/// parent, a path deleted by a race mid-walk) is warned about and
/// skipped — the walk runs over the staged PARENT, which contains trees
/// unrelated to this build, and one unreadable path must not abort the
/// build. Entries absent from the map are simply untracked for the
/// whiteout sync, the already-handled case. Only the ROOT itself being
/// unreadable propagates: the package tree being unreadable is a real
/// error.
pub(crate) fn walk_tree_meta(root: &Path) -> io::Result<BTreeMap<String, EntryMeta>> {
let mut map = BTreeMap::new();
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
for entry in fs::read_dir(&dir)?.flatten() {
let entries = match fs::read_dir(&dir) {
Ok(entries) => entries,
Err(e) => {
if dir == root {
return Err(e);
}
log::warn!("skipping unreadable path {}: {e}", dir.display());
continue;
}
};
for entry in entries.flatten() {
let path = entry.path();
let metadata = fs::symlink_metadata(&path)?;
let metadata = match fs::symlink_metadata(&path) {
Ok(metadata) => metadata,
// A race deleted the entry between read_dir and stat.
Err(e) => {
log::warn!("skipping unreadable path {}: {e}", path.display());
continue;
}
};
if metadata.is_dir() {
stack.push(path);
} else {
@@ -2039,6 +2066,53 @@ mod tests {
assert_eq!(load_snapshot(&path), Some(meta_map));
}
/// The snapshot walk is best-effort below the root: an unreadable
/// sibling directory in the staged parent (a root-owned tree the
/// user's own setup script left) is skipped with a warning instead
/// of aborting the build, and only the root's own unreadability
/// propagates. Deterministic as non-root: mode bits are checked with
/// no owner override, so a mode-000 directory blocks even its owner.
#[test]
fn walk_skips_unreadable_paths_but_fails_on_an_unreadable_root() {
use std::os::unix::fs::PermissionsExt;
let base = tempfile::tempdir().unwrap();
let root = base.path().join("parent");
let readable = root.join("linux/src");
let secret = root.join("demos/sysroot-riscv64");
fs::create_dir_all(&readable).unwrap();
fs::create_dir_all(&secret).unwrap();
fs::write(readable.join("a.c"), "src").unwrap();
fs::write(secret.join("sysroot.tar"), "root-owned").unwrap();
fs::set_permissions(&secret, fs::Permissions::from_mode(0o000)).unwrap();
// The unreadable subtree is absent from the map; the walk itself
// succeeds. The directory entry itself IS stat-able (its mode
// hides its CONTENT, not its existence), so it appears as a
// tracked empty subtree root only if it were a file — here it is
// skipped whole, its contents untracked.
let walked = walk_tree_meta(&root).expect("one unreadable sibling must not abort the walk");
// Restore before asserting, so a failure still cleans up.
fs::set_permissions(&secret, fs::Permissions::from_mode(0o755)).unwrap();
assert!(
walked.contains_key("linux/src/a.c"),
"the readable tree is tracked: {walked:?}"
);
assert!(
!walked.contains_key("demos/sysroot-riscv64/sysroot.tar"),
"the unreadable subtree's content is not tracked: {walked:?}"
);
// An unreadable ROOT is a real error: the package tree itself is
// invisible to this run.
fs::set_permissions(&secret, fs::Permissions::from_mode(0o000)).unwrap();
let err = walk_tree_meta(&secret).expect_err("walking an unreadable root must fail");
fs::set_permissions(&secret, fs::Permissions::from_mode(0o755)).unwrap();
assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);
}
/// Identity slugs are sanitized and collision-free, and the manifest
/// round-trips through JSON — lineage, tarball sha and the kept
/// upper included, and their absence (older manifests) parsing as