Skip to content

Commit 69e1825

Browse files
authored
Merge pull request #114 from multikernel/cow-symlink-confinement
sandlock-core: confine COW supervisor against symlink escape
2 parents d2bdca0 + c722368 commit 69e1825

6 files changed

Lines changed: 1229 additions & 288 deletions

File tree

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ use std::sync::Arc;
4747

4848
use tokio::sync::Mutex;
4949

50-
use crate::chroot::resolve::{confine, openat2_in_root, resolve_existing_in_root, resolve_in_root, to_virtual_path};
50+
use crate::chroot::resolve::{confine, resolve_existing_in_root, resolve_in_root, to_virtual_path};
51+
use crate::sys::fs::openat2_in_root;
5152
use crate::seccomp::notif::{read_child_mem, write_child_mem, NotifAction};
5253
use crate::seccomp::state::{ChrootState, CowState};
5354
use crate::sys::structs::{SeccompNotif, SeccompNotifAddfd, SECCOMP_IOCTL_NOTIF_ADDFD};

crates/sandlock-core/src/chroot/resolve.rs

Lines changed: 2 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use std::ffi::CString;
2-
use std::os::unix::io::RawFd;
31
use std::path::{Path, PathBuf};
42

3+
use crate::sys::fs::openat2_in_root;
4+
55
/// Collapse `..` components clamping at `/` (pivot_root semantics).
66
/// Always returns an absolute path under `/`.
77
pub fn confine(virtual_path: &str) -> PathBuf {
@@ -36,86 +36,6 @@ pub fn to_virtual_path(chroot_root: &Path, host_path: &Path) -> Option<PathBuf>
3636
.map(|rel| PathBuf::from("/").join(rel))
3737
}
3838

39-
// ============================================================
40-
// openat2(RESOLVE_IN_ROOT) based resolution
41-
// ============================================================
42-
43-
/// openat2 syscall number, sourced from the `syscalls` crate via `arch`.
44-
const SYS_OPENAT2: libc::c_long = crate::arch::SYS_OPENAT2;
45-
46-
/// RESOLVE_IN_ROOT — treat the dirfd as the filesystem root for resolution.
47-
const RESOLVE_IN_ROOT: u64 = 0x10;
48-
49-
/// Kernel `struct open_how` for openat2().
50-
#[repr(C)]
51-
struct OpenHow {
52-
flags: u64,
53-
mode: u64,
54-
resolve: u64,
55-
}
56-
57-
fn last_errno(fallback: i32) -> i32 {
58-
std::io::Error::last_os_error()
59-
.raw_os_error()
60-
.unwrap_or(fallback)
61-
}
62-
63-
/// Open a path confined within `chroot_root` using `openat2(RESOLVE_IN_ROOT)`.
64-
///
65-
/// The kernel handles symlink resolution, `..` traversal, and prevents
66-
/// escapes above the root — eliminating TOCTOU races and edge cases
67-
/// inherent in userspace path walking.
68-
pub fn openat2_in_root(
69-
chroot_root: &Path,
70-
path: &str,
71-
flags: i32,
72-
mode: u32,
73-
) -> Result<RawFd, i32> {
74-
let c_root =
75-
CString::new(chroot_root.to_str().unwrap_or("")).map_err(|_| libc::EINVAL)?;
76-
let root_fd = unsafe {
77-
libc::open(
78-
c_root.as_ptr(),
79-
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC,
80-
)
81-
};
82-
if root_fd < 0 {
83-
return Err(last_errno(libc::EIO));
84-
}
85-
86-
let rel_path = path.strip_prefix('/').unwrap_or(path);
87-
// Empty path means the root itself — use "."
88-
let rel_path = if rel_path.is_empty() { "." } else { rel_path };
89-
let c_path = CString::new(rel_path).map_err(|_| {
90-
unsafe { libc::close(root_fd) };
91-
libc::EINVAL
92-
})?;
93-
94-
let how = OpenHow {
95-
flags: flags as u64,
96-
mode: mode as u64,
97-
resolve: RESOLVE_IN_ROOT,
98-
};
99-
100-
let fd = unsafe {
101-
libc::syscall(
102-
SYS_OPENAT2,
103-
root_fd,
104-
c_path.as_ptr(),
105-
&how as *const OpenHow,
106-
std::mem::size_of::<OpenHow>(),
107-
)
108-
} as i32;
109-
110-
unsafe { libc::close(root_fd) };
111-
112-
if fd < 0 {
113-
Err(last_errno(libc::ENOENT))
114-
} else {
115-
Ok(fd)
116-
}
117-
}
118-
11939
/// Resolve a virtual path within the chroot using `openat2(RESOLVE_IN_ROOT)`.
12040
///
12141
/// The kernel resolves all symlinks and `..` components, keeping the result

0 commit comments

Comments
 (0)