Skip to content

Commit c722368

Browse files
committed
sandlock-core: harden upper-write escape test and drop redundant re-lock
Signed-off-by: Cong Wang <cwang@multikernel.io>
1 parent 2d957d5 commit c722368

2 files changed

Lines changed: 22 additions & 28 deletions

File tree

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

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -550,8 +550,9 @@ pub(crate) async fn handle_cow_write(
550550
None => return NotifAction::Continue,
551551
};
552552

553-
// Phase 1: check if we need to pre-copy a file (under lock, no heavy I/O)
554-
let (copy_plan, copy_workdir, copy_rel) = {
553+
// Phase 1: check if we need to pre-copy a file (under lock, no heavy I/O).
554+
// Capture both layer roots here so Phase 2 needs no second lock.
555+
let (copy_plan, copy_workdir, copy_upper, copy_rel) = {
555556
let mut st = cow_state.lock().await;
556557
let cow = match st.branch.as_mut() {
557558
Some(c) => c,
@@ -562,30 +563,21 @@ pub(crate) async fn handle_cow_write(
562563
match cow_copy_rel(&op, cow) {
563564
Some((_match_path, ref rel)) => {
564565
let workdir = cow.workdir().to_path_buf();
566+
let upper = cow.upper_dir().to_path_buf();
565567
match cow.prepare_copy(rel) {
566-
Ok(plan) => (Some(plan), workdir, rel.clone()),
568+
Ok(plan) => (Some(plan), workdir, upper, rel.clone()),
567569
Err(crate::error::BranchError::QuotaExceeded) => return NotifAction::Errno(libc::ENOSPC),
568570
Err(_) => return NotifAction::Continue,
569571
}
570572
}
571-
None => (None, std::path::PathBuf::new(), String::new()),
573+
None => (None, std::path::PathBuf::new(), std::path::PathBuf::new(), String::new()),
572574
}
573575
};
574576
// Lock is released here
575577

576578
// Phase 2: execute the file copy outside the lock (if needed)
577579
if let Some(crate::cow::seccomp::CowCopyPlan::NeedsCopy { upper, lower: _lower, file_size }) = copy_plan {
578-
// copy_workdir is workdir_root; we need upper_root for the dest side.
579-
// Re-acquire it briefly: execute_deferred_copy needs both roots.
580-
let uroot = {
581-
let st = cow_state.lock().await;
582-
st.branch.as_ref().map(|c| c.upper_dir().to_path_buf())
583-
};
584-
let uroot = match uroot {
585-
Some(p) => p,
586-
None => return NotifAction::Continue,
587-
};
588-
if execute_deferred_copy(cow_state, copy_workdir, uroot, copy_rel, upper, file_size).await.is_none() {
580+
if execute_deferred_copy(cow_state, copy_workdir, copy_upper, copy_rel, upper, file_size).await.is_none() {
589581
return NotifAction::Continue;
590582
}
591583
}

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

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1631,32 +1631,34 @@ mod tests {
16311631

16321632
#[test]
16331633
fn upper_write_does_not_escape_through_symlink() {
1634-
// workdir/evil -> /etc (absolute symlink). Copy it up verbatim into
1635-
// upper so upper/evil is also an absolute symlink. Then invoke
1636-
// handle_mkdir with a path that would go through that symlink.
1637-
// The confined mkdirp_in_root must clamp the walk to the upper root
1638-
// and never create /etc/sandlock_escape_dir on the host.
1634+
// workdir/evil -> <outside> (absolute symlink to a writable dir
1635+
// outside the sandbox). Copy it up verbatim so upper/evil is also that
1636+
// absolute symlink, then mkdir through it. The confined mkdirp must
1637+
// clamp to the upper root, refuse, and never create the dir in
1638+
// <outside>. Pointing at a writable TempDir (not /etc) means the test
1639+
// distinguishes the fix from the old lexical code even when not root.
16391640
let (workdir, storage) = setup_workdir();
1640-
std::os::unix::fs::symlink("/etc", workdir.path().join("evil")).unwrap();
1641+
let outside = tempfile::tempdir().unwrap();
1642+
std::os::unix::fs::symlink(outside.path(), workdir.path().join("evil")).unwrap();
16411643
let mut branch =
16421644
SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
16431645

1644-
// Copy up the symlink so upper/evil -> /etc verbatim.
16451646
branch.ensure_cow_copy("evil").unwrap();
16461647
assert!(
16471648
branch.upper_dir().join("evil").is_symlink(),
16481649
"precondition: upper/evil must be a verbatim symlink"
16491650
);
16501651

1651-
// Attempt to mkdir via the symlinked parent component.
16521652
let wd = workdir.path().canonicalize().unwrap();
16531653
let escape_path = format!("{}/evil/sandlock_escape_dir", wd.display());
1654-
let _ = branch.handle_mkdir(&escape_path);
1655-
1656-
// The confined write must not have escaped.
1654+
// Confined: the write is clamped to the upper root and must be refused.
1655+
assert!(
1656+
!branch.handle_mkdir(&escape_path).unwrap(),
1657+
"handle_mkdir reported success writing through an escaping symlink"
1658+
);
16571659
assert!(
1658-
!std::path::Path::new("/etc/sandlock_escape_dir").exists(),
1659-
"upper write escaped through symlinked parent into /etc"
1660+
!outside.path().join("sandlock_escape_dir").exists(),
1661+
"upper write escaped through symlinked parent into the outside dir"
16601662
);
16611663
}
16621664
}

0 commit comments

Comments
 (0)