Skip to content

Commit a83cd38

Browse files
committed
sandlock-core: recreate symlinks verbatim on COW commit to avoid escape
Signed-off-by: Cong Wang <cwang@multikernel.io>
1 parent 0145c5b commit a83cd38

1 file changed

Lines changed: 44 additions & 0 deletions

File tree

crates/sandlock-core/src/cow/seccomp.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,9 @@ impl SeccompCowBranch {
283283
fs::File::create(upper)?;
284284
return Ok(());
285285
}
286+
// On a kernel without openat2 (ENOSYS) the copy fails and the caller
287+
// rolls back / returns Continue; the child then hits Landlock, which
288+
// is the backstop. Do not "fix" this with an unconfined fs::copy.
286289
Err(e) => return Err(std::io::Error::from_raw_os_error(e)),
287290
};
288291

@@ -795,6 +798,21 @@ impl SeccompCowBranch {
795798
if entry.file_type().is_dir() {
796799
fs::create_dir_all(&dest)
797800
.map_err(|e| BranchError::Operation(format!("mkdir: {}", e)))?;
801+
} else if entry.file_type().is_symlink() {
802+
// Recreate the symlink verbatim. fs::copy would follow it and
803+
// read the target outside any root or Landlock (issue #112),
804+
// and dereferencing would also lose the link in the workdir.
805+
if let Some(parent) = dest.parent() {
806+
let _ = fs::create_dir_all(parent);
807+
}
808+
let target = fs::read_link(entry.path())
809+
.map_err(|e| BranchError::Operation(format!("readlink: {}", e)))?;
810+
if dest.exists() || dest.is_symlink() {
811+
let _ = fs::remove_file(&dest);
812+
}
813+
std::os::unix::fs::symlink(&target, &dest)
814+
.map_err(|e| BranchError::Operation(format!("symlink: {}", e)))?;
815+
synced_dirs.insert(dest.parent().unwrap().to_path_buf());
798816
} else {
799817
if let Some(parent) = dest.parent() {
800818
let _ = fs::create_dir_all(parent);
@@ -1510,4 +1528,30 @@ mod tests {
15101528
let upper = branch.ensure_cow_copy("existing.txt").unwrap();
15111529
assert_eq!(std::fs::read_to_string(&upper).unwrap(), "hello");
15121530
}
1531+
1532+
#[test]
1533+
fn commit_does_not_dereference_escaping_symlink() {
1534+
// A pre-existing workdir symlink with an absolute target gets copied
1535+
// verbatim into upper by prepare_copy; commit() must recreate it as a
1536+
// symlink, never read the target's content (issue #112, commit path).
1537+
let (workdir, storage) = setup_workdir();
1538+
std::os::unix::fs::symlink("/etc/group", workdir.path().join("secret")).unwrap();
1539+
let mut branch =
1540+
SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
1541+
// Trigger copy-up of the symlink into upper.
1542+
let upper = branch.ensure_cow_copy("secret").unwrap();
1543+
assert!(upper.is_symlink(), "precondition: upper holds a verbatim symlink");
1544+
1545+
branch.commit().unwrap();
1546+
1547+
let committed = workdir.path().join("secret");
1548+
assert!(
1549+
committed.is_symlink(),
1550+
"commit dereferenced the symlink instead of recreating it"
1551+
);
1552+
assert_eq!(std::fs::read_link(&committed).unwrap(), std::path::Path::new("/etc/group"));
1553+
// The workdir entry must not have become a regular file holding the host content.
1554+
let meta = std::fs::symlink_metadata(&committed).unwrap();
1555+
assert!(meta.file_type().is_symlink());
1556+
}
15131557
}

0 commit comments

Comments
 (0)