summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHermes Agent <hermes@localhost>2026-08-12 04:26:44 +0000
committerHermes Agent <hermes@localhost>2026-08-12 04:26:44 +0000
commitca9e5044a1678a3ff33f13582bd6498632113c3e (patch)
treeeeb4cd51faa52510ffe7f0bf27d3cf9dcd9b241d
parent39a23d9035ab99191e305db3a55a8df1606f63b3 (diff)
Prevent rootfs copy from following symlinks
-rw-r--r--src/rootfs.rs16
-rw-r--r--tests/rootfs_finalization.rs32
2 files changed, 46 insertions, 2 deletions
diff --git a/src/rootfs.rs b/src/rootfs.rs
index 9246aa8..ea7d87b 100644
--- a/src/rootfs.rs
+++ b/src/rootfs.rs
@@ -173,8 +173,7 @@ fn copy_entry(
if let Some(parent) = output.parent() {
fs::create_dir_all(parent)?;
}
- fs::copy(source, &output)
- .with_context(|| format!("copy {} to {}", source.display(), output.display()))?;
+ replace_with_file(source, &output)?;
fs::set_permissions(&output, metadata.permissions())?;
manifest.record(destination);
} else {
@@ -213,6 +212,19 @@ fn enable_service(rootfs: &Path, service: &ServiceName, manifest: &mut MutationM
Ok(())
}
+fn replace_with_file(source: &Path, output: &Path) -> Result<()> {
+ match fs::symlink_metadata(output) {
+ Ok(metadata) if metadata.file_type().is_dir() => {
+ bail!("cannot replace directory with file: {}", output.display());
+ }
+ Ok(_) => fs::remove_file(output).with_context(|| format!("replace {}", output.display()))?,
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
+ Err(error) => return Err(error).with_context(|| format!("inspect {}", output.display())),
+ }
+ fs::copy(source, output).with_context(|| format!("copy {} to {}", source.display(), output.display()))?;
+ Ok(())
+}
+
fn replace_with_symlink(target: &Path, output: &Path) -> Result<()> {
match fs::symlink_metadata(output) {
Ok(metadata) if metadata.file_type().is_dir() => {
diff --git a/tests/rootfs_finalization.rs b/tests/rootfs_finalization.rs
index 3a98290..a450b7e 100644
--- a/tests/rootfs_finalization.rs
+++ b/tests/rootfs_finalization.rs
@@ -115,3 +115,35 @@ fn finalization_is_idempotent_for_matching_symlinks() {
.iter()
.any(|path| path == "etc/controller/controller-link"));
}
+
+#[test]
+fn finalization_replaces_a_destination_symlink_instead_of_following_it() {
+ let fixture = tempdir().expect("fixture directory");
+ let source = fixture.path().join("overlay");
+ fs::create_dir_all(&source).expect("create source tree");
+ fs::write(source.join("controller.conf"), "new controller configuration\n")
+ .expect("write source file");
+ let rootfs = tempdir().expect("rootfs directory");
+ let outside = fixture.path().join("outside.conf");
+ fs::write(&outside, "must not change\n").expect("write outside file");
+ fs::create_dir_all(rootfs.path().join("etc/controller")).expect("create destination directory");
+ symlink(&outside, rootfs.path().join("etc/controller/controller.conf"))
+ .expect("create destination symlink");
+ let finalization = RootfsFinalization::new(
+ vec![CopyTree::new(&source, "etc/controller").expect("valid copy destination")],
+ InitrdOem::new(std::iter::empty::<&str>(), std::iter::empty::<&str>()),
+ vec![],
+ );
+
+ finalization.apply(rootfs.path()).expect("finalization replaces symlink safely");
+
+ assert_eq!(fs::read_to_string(&outside).expect("read outside file"), "must not change\n");
+ assert!(fs::symlink_metadata(rootfs.path().join("etc/controller/controller.conf"))
+ .expect("inspect copied file")
+ .file_type()
+ .is_file());
+ assert_eq!(
+ fs::read_to_string(rootfs.path().join("etc/controller/controller.conf")).expect("read copied file"),
+ "new controller configuration\n"
+ );
+}