Skip to content

Commit 0145c5b

Browse files
committed
sandlock-core: confine COW copy-up read against symlink escape
Signed-off-by: Cong Wang <cwang@multikernel.io>
1 parent 5cd91f4 commit 0145c5b

1 file changed

Lines changed: 61 additions & 15 deletions

File tree

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

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -260,31 +260,48 @@ impl SeccompCowBranch {
260260

261261
/// Execute a file copy synchronously. Used by `ensure_cow_copy` and the
262262
/// async dispatch (via `spawn_blocking`).
263-
pub fn execute_copy(upper: &Path, lower: &Path) -> Result<(), std::io::Error> {
264-
match fs::copy(lower, upper) {
265-
Ok(_) => {
266-
if let Ok(meta) = lower.metadata() {
267-
let _ = fs::set_permissions(upper, meta.permissions());
268-
}
269-
Ok(())
270-
}
271-
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
272-
// Can't read the lower file (e.g. root-owned 0640).
273-
// Create an empty file in upper so writes can proceed.
263+
pub fn execute_copy(
264+
workdir_root: &Path,
265+
rel: &str,
266+
upper: &Path,
267+
) -> Result<(), std::io::Error> {
268+
use std::os::unix::io::FromRawFd;
269+
270+
// Read the lower source confined to the workdir root: a symlink or
271+
// `..` in `rel` cannot escape the tree (issue #112).
272+
let fd = match crate::sys::fs::openat2_in_root(
273+
workdir_root,
274+
rel,
275+
libc::O_RDONLY | libc::O_CLOEXEC,
276+
0,
277+
) {
278+
Ok(fd) => fd,
279+
// Unreadable (EACCES) or confined-out / absent (ENOENT): give the
280+
// child an empty COW file so writes proceed, never leaking the
281+
// escape target.
282+
Err(libc::EACCES) | Err(libc::ENOENT) => {
274283
fs::File::create(upper)?;
275-
Ok(())
284+
return Ok(());
276285
}
277-
Err(e) => Err(e),
286+
Err(e) => return Err(std::io::Error::from_raw_os_error(e)),
287+
};
288+
289+
let mut src = unsafe { fs::File::from_raw_fd(fd) };
290+
let mut dst = fs::File::create(upper)?;
291+
std::io::copy(&mut src, &mut dst)?;
292+
if let Ok(meta) = src.metadata() {
293+
let _ = fs::set_permissions(upper, meta.permissions());
278294
}
295+
Ok(())
279296
}
280297

281298
/// Ensure a COW copy exists in upper (synchronous). Returns the upper path.
282299
/// For callers that don't need async two-phase behavior.
283300
pub fn ensure_cow_copy(&mut self, rel_path: &str) -> Result<PathBuf, BranchError> {
284301
match self.prepare_copy(rel_path)? {
285302
CowCopyPlan::Ready(upper) => Ok(upper),
286-
CowCopyPlan::NeedsCopy { upper, lower, file_size } => {
287-
match Self::execute_copy(&upper, &lower) {
303+
CowCopyPlan::NeedsCopy { upper, lower: _lower, file_size } => {
304+
match Self::execute_copy(&self.workdir, rel_path, &upper) {
288305
Ok(()) => Ok(upper),
289306
Err(e) => {
290307
self.rollback_copy(file_size);
@@ -1464,4 +1481,33 @@ mod tests {
14641481
let path = abs(&branch, "existing.txt");
14651482
assert!(branch.handle_unlink(&path, false).unwrap());
14661483
}
1484+
1485+
#[test]
1486+
fn copy_up_does_not_follow_symlinked_parent() {
1487+
// workdir/evil -> /etc ; writing evil/group must not copy /etc/group.
1488+
let (workdir, storage) = setup_workdir();
1489+
std::os::unix::fs::symlink("/etc", workdir.path().join("evil")).unwrap();
1490+
let mut branch =
1491+
SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
1492+
1493+
// ensure_cow_copy on a path reached through the symlinked parent.
1494+
let upper = branch.ensure_cow_copy("evil/group").unwrap();
1495+
1496+
// The upper file must NOT contain the host /etc/group contents.
1497+
let host = std::fs::read_to_string("/etc/group").unwrap_or_default();
1498+
let copied = std::fs::read_to_string(&upper).unwrap_or_default();
1499+
assert!(
1500+
copied.is_empty() || copied != host,
1501+
"copy-up leaked /etc/group into the upper layer"
1502+
);
1503+
}
1504+
1505+
#[test]
1506+
fn copy_up_copies_legitimate_in_tree_file() {
1507+
let (workdir, storage) = setup_workdir();
1508+
let mut branch =
1509+
SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
1510+
let upper = branch.ensure_cow_copy("existing.txt").unwrap();
1511+
assert_eq!(std::fs::read_to_string(&upper).unwrap(), "hello");
1512+
}
14671513
}

0 commit comments

Comments
 (0)