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:
+76
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user