From 8485e7272bdff57bbed395d75ec90e4f9cd527c9 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Fri, 2 Oct 2026 23:03:15 +0200 Subject: [PATCH] deb: sync upper entries through the privileged removal path A rootless build's upperdir entries are copy-up'd by whoever wrote them during the build: the unprivileged build user, which host-side is a subuid of the invoking user. The host-side sync of a forked record removes the stale upper copy of every host-modified file, and the plain fs removals failed with EPERM on those build-user-owned entries: the kernel incremental run died with a bare 'Permission denied (os error 13)' and no context, ten minutes into the run. Route the sync removals through the privileged path (the same sudo fallback the record teardowns use) and wrap the sync error at the call site, so a failure names the operation instead of surfacing a bare errno string. --- src/deb/mod.rs | 7 ++++++- src/deb/session.rs | 36 ++++++++++++++++++------------------ 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/deb/mod.rs b/src/deb/mod.rs index 5e4bf18..ccde698 100644 --- a/src/deb/mod.rs +++ b/src/deb/mod.rs @@ -627,7 +627,12 @@ async fn build_binary_package_impl( // not drag it into the chroot. let mut build_resume = false; if let Some(s) = &session { - build_resume = session::sync_upper_for_resume(s, &host_tree)?; + build_resume = session::sync_upper_for_resume(s, &host_tree).map_err(|e| { + format!( + "cannot sync the carried tree of the incremental build \ + with the host tree: {e}" + ) + })?; if build_resume { log::info!( "Reusing the build artifacts of the previous attempt (incremental build)" diff --git a/src/deb/session.rs b/src/deb/session.rs index fe6e558..5c526cf 100644 --- a/src/deb/session.rs +++ b/src/deb/session.rs @@ -2072,6 +2072,12 @@ pub(crate) fn plan_upper_sync( /// Apply sync ops to the upperdir, skipping paths already covered by a /// removed ancestor. Returns how many entries were actually removed. +/// +/// Removals go through the privileged path: the upperdir's entries were +/// copy-up'd by whoever wrote them during the build — in a rootless build +/// the unprivileged build user (a subuid of the invoking user host-side) — +/// so a plain removal by the invoking user fails with EPERM exactly when a +/// host-modified file's stale upper copy must go. pub(crate) fn apply_upper_sync(upper_root: &Path, ops: &[UpperOp]) -> io::Result { let mut removed_prefixes: Vec = Vec::new(); let mut removed = 0; @@ -2084,26 +2090,20 @@ pub(crate) fn apply_upper_sync(upper_root: &Path, ops: &[UpperOp]) -> io::Result continue; } let path = upper_root.join(rel.replace('/', std::path::MAIN_SEPARATOR_STR)); - let result = if *dir { - fs::remove_dir_all(&path) - } else { - fs::remove_file(&path) + let Ok(meta) = path.symlink_metadata() else { + continue; }; - match result { - Ok(()) => { - removed += 1; - if *dir { - let mut prefix = rel.clone(); - if !prefix.ends_with('/') { - prefix.push('/'); - } - removed_prefixes.push(prefix); - } + if !meta.is_dir() && !meta.is_file() { + continue; + } + crate::deb::ephemeral::privileged_remove(&path)?; + removed += 1; + if *dir { + let mut prefix = rel.clone(); + if !prefix.ends_with('/') { + prefix.push('/'); } - Err(e) if e.kind() != io::ErrorKind::NotFound => { - return Err(e); - } - Err(_) => {} + removed_prefixes.push(prefix); } } Ok(removed)