From c8515c9c854b039dcf73afa46701d48b64035e9c Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Mon, 6 Jul 2026 13:41:23 +0530 Subject: [PATCH] etc-merge: Use `symlink_metadata_optional` cap_std's `metadata_optional` follows symlinks and errors out if the symlink target is outside the current root Use `symlink_metadata_optional` when checking for removed files in new etc so that symlinks that resolve to a path outside of new_etc don't get followed Fixes: #2278 Signed-off-by: Pragyan Poudyal --- crates/etc-merge/src/lib.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/etc-merge/src/lib.rs b/crates/etc-merge/src/lib.rs index 0b78a0af5..1ea0d1f72 100644 --- a/crates/etc-merge/src/lib.rs +++ b/crates/etc-merge/src/lib.rs @@ -794,7 +794,9 @@ pub fn merge( .context("Merging modified files")?; for removed in &diff.removed { - let stat = new_etc_fd.metadata_optional(&removed)?; + // Use symlink_metadata_optional so that symlinks that resolve to a path + // outside the new_etc_fd don't get followed + let stat = new_etc_fd.symlink_metadata_optional(&removed)?; let Some(stat) = stat else { // File/dir doesn't exist in new_etc @@ -1071,6 +1073,12 @@ mod tests { n.create_dir_all("dir/perms")?; n.write("dir/perms/some-file", "Some-file")?; + // File exists in pristine and new_etc (as a symlink pointing outside the root), + // but was deleted in current_etc. The merge should remove it from new_etc + // without following the symlink. + p.write("ext-symlink", "pristine content")?; + symlinkat("/usr/bin/bash", &n, "ext-symlink")?; + let (pristine_etc_files, current_etc_files, new_etc_files) = traverse_etc(&p, &c, Some(&n))?; let diff = compute_diff( @@ -1128,6 +1136,9 @@ mod tests { DIR_BITS | 0o775 ); + // External symlink should be removed without following it + assert!(!n.exists("ext-symlink")); + Ok(()) }