Skip to content

Commit f17e325

Browse files
committed
ptfs: implement perfile based chroot by using openat2
Implement perfile based chroot by using openat2 when available, to project path based attacks. Signed-off-by: Jiang Liu <[email protected]>
1 parent 8b03121 commit f17e325

File tree

2 files changed

+160
-7
lines changed

2 files changed

+160
-7
lines changed

src/passthrough/mod.rs

+4-7
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pub use self::config::{CachePolicy, Config};
3131
use self::file_handle::{FileHandle, OpenableFileHandle};
3232
use self::inode_store::{InodeId, InodeStore};
3333
use self::mount_fd::MountFds;
34+
use self::os_compat::SafeOpenAt;
3435
use self::statx::{statx, StatExt};
3536
use self::util::{
3637
ebadf, einval, enosys, eperm, is_dir, is_safe_inode, openat, reopen_fd_through_proc, stat_fd,
@@ -358,6 +359,7 @@ pub struct PassthroughFs<S: BitmapSlice + Send + Sync = ()> {
358359
ino_allocator: UniqueInodeGenerator,
359360
// Maps mount IDs to an open FD on the respective ID for the purpose of open_by_handle_at().
360361
mount_fds: MountFds,
362+
opener: SafeOpenAt,
361363

362364
// File descriptor pointing to the `/proc/self/fd` directory. This is used to convert an fd from
363365
// `inodes` into one that can go into `handles`. This is accomplished by reading the
@@ -439,6 +441,7 @@ impl<S: BitmapSlice + Send + Sync> PassthroughFs<S> {
439441

440442
mount_fds,
441443
proc_self_fd,
444+
opener: SafeOpenAt::new(),
442445

443446
writeback: AtomicBool::new(false),
444447
no_open: AtomicBool::new(false),
@@ -564,13 +567,7 @@ impl<S: BitmapSlice + Send + Sync> PassthroughFs<S> {
564567
mode: u32,
565568
) -> io::Result<File> {
566569
let flags = libc::O_NOFOLLOW | libc::O_CLOEXEC | flags;
567-
568-
// TODO
569-
//if self.os_facts.has_openat2 {
570-
// oslib::do_open_relative_to(dir, pathname, flags, mode)
571-
//} else {
572-
openat(dir, pathname, flags, mode)
573-
//}
570+
self.opener.openat(dir, pathname, flags, mode)
574571
}
575572

576573
/// Create a File or File Handle for `name` under directory `dir_fd` to support `lookup()`.

src/passthrough/os_compat.rs

+156
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@
33
// found in the LICENSE-BSD-3-Clause file.
44
// SPDX-License-Identifier: Apache-2.0
55

6+
use std::io;
7+
use std::ffi::{CString, CStr};
8+
use std::os::fd::{AsRawFd, FromRawFd, RawFd};
9+
use std::fs::File;
10+
611
use vm_memory::ByteValued;
712

13+
use super::util::openat;
14+
815
#[repr(C, packed)]
916
#[derive(Clone, Copy, Debug, Default)]
1017
pub struct LinuxDirent64 {
@@ -66,3 +73,152 @@ pub const STATX_BASIC_STATS: libc::c_uint = 0x07ff;
6673

6774
#[cfg(not(target_env = "gnu"))]
6875
pub const STATX_MNT_ID: libc::c_uint = 0x1000;
76+
77+
pub struct SafeOpenAt {
78+
has_openat2: bool,
79+
}
80+
81+
impl SafeOpenAt {
82+
pub fn new() -> Self {
83+
// Checking for `openat2()` since it first appeared in Linux 5.6.
84+
// SAFETY: all-zero byte-pattern is a valid `libc::open_how`
85+
let how: libc::open_how = unsafe { std::mem::zeroed() };
86+
let cwd = CString::new(".").unwrap();
87+
// SAFETY: `cwd.as_ptr()` points to a valid NUL-terminated string,
88+
// and the `how` pointer is a valid pointer to an `open_how` struct.
89+
let fd = unsafe {
90+
libc::syscall(
91+
libc::SYS_openat2,
92+
libc::AT_FDCWD,
93+
cwd.as_ptr(),
94+
std::ptr::addr_of!(how),
95+
std::mem::size_of::<libc::open_how>(),
96+
)
97+
};
98+
99+
let has_openat2 = fd >= 0;
100+
if has_openat2 {
101+
// SAFETY: `fd` is an open file descriptor
102+
unsafe {
103+
libc::close(fd as libc::c_int);
104+
}
105+
}
106+
107+
Self { has_openat2 }
108+
}
109+
110+
/// An utility function that uses `openat2(2)` to restrict the how the provided pathname
111+
/// is resolved. It uses the following flags:
112+
/// - `RESOLVE_IN_ROOT`: Treat the directory referred to by dirfd as the root directory while
113+
/// resolving pathname. This has the effect as though virtiofsd had used chroot(2) to modify its
114+
/// root directory to dirfd.
115+
/// - `RESOLVE_NO_MAGICLINKS`: Disallow all magic-link (i.e., proc(2) link-like files) resolution
116+
/// during path resolution.
117+
///
118+
/// Additionally, the flags `O_NOFOLLOW` and `O_CLOEXEC` are added.
119+
///
120+
/// # Error
121+
///
122+
/// Will return `Err(errno)` if `openat2(2)` fails, see the man page for details.
123+
///
124+
/// # Safety
125+
///
126+
/// The caller must ensure that dirfd is a valid file descriptor.
127+
pub fn openat(
128+
&self,
129+
dir: &impl AsRawFd,
130+
path: &CStr,
131+
flags: libc::c_int,
132+
mode: u32,
133+
) -> io::Result<File> {
134+
// Fallback to openat
135+
if !self.has_openat2 {
136+
return openat(dir, path, flags, mode);
137+
}
138+
139+
// `openat2(2)` returns an error if `how.mode` contains bits other than those in range 07777,
140+
// let's ignore the extra bits to be compatible with `openat(2)`.
141+
let mode = mode as u64 & 0o7777;
142+
143+
// SAFETY: all-zero byte-pattern represents a valid `libc::open_how`
144+
let mut how: libc::open_how = unsafe { std::mem::zeroed() };
145+
// - RESOLVE_IN_ROOT
146+
// Treat the directory referred to by dirfd as the root directory while resolving pathname.
147+
// Absolute symbolic links are interpreted relative to dirfd. If a prefix component of
148+
// pathname equates to dirfd, then an immediately following .. component likewise equates
149+
// to dirfd (just as /.. is traditionally equivalent to /). If pathname is an absolute
150+
// path, it is also interpreted relative to dirfd.
151+
//
152+
// The effect of this flag is as though the calling process had used chroot(2) to
153+
// (temporarily) modify its root directory (to the directory referred to by dirfd).
154+
// However, unlike chroot(2) (which changes the filesystem root permanently for a process),
155+
// RESOLVE_IN_ROOT allows a program to efficiently restrict path resolution on a per-open
156+
// basis.
157+
//
158+
// Currently, this flag also disables magic-link resolution. However, this may change
159+
// in the future. Therefore, to ensure that magic links are not resolved, the caller should
160+
// explicitly specify RESOLVE_NO_MAGICLINKS.
161+
//
162+
// - RESOLVE_NO_MAGICLINKS
163+
// Disallow all magic-link resolution during path resolution.
164+
//
165+
// Magic links are symbolic link-like objects that are most notably found in proc(5);
166+
// examples include /proc/pid/exe and /proc/pid/fd/*. (See symlink(7) for more details.)
167+
//
168+
// Unknowingly opening magic links can be risky for some applications. Examples of such
169+
// risks include the following:
170+
// • If the process opening a pathname is a controlling process that currently has no
171+
// controlling terminal (see credentials(7)), then opening a magic link inside
172+
// /proc/pid/fd that happens to refer to a terminal would cause the process to acquire
173+
// a controlling terminal.
174+
//
175+
// • In a containerized environment, a magic link inside /proc may refer to an object
176+
// outside the container, and thus may provide a means to escape from the container.
177+
//
178+
// Because of such risks, an application may prefer to disable magic link resolution using
179+
// the RESOLVE_NO_MAGICLINKS flag.
180+
//
181+
// If the trailing component (i.e., basename) of pathname is a magic link, how.resolve
182+
// contains RESOLVE_NO_MAGICLINKS, and how.flags contains both O_PATH and O_NOFOLLOW,
183+
// then an O_PATH file descriptor referencing the magic link will be returned.
184+
how.resolve = libc::RESOLVE_IN_ROOT | libc::RESOLVE_NO_MAGICLINKS;
185+
how.flags = flags as u64;
186+
how.mode = mode;
187+
188+
// SAFETY: `pathname` points to a valid NUL-terminated string, and the `how` pointer is a valid
189+
// pointer to an `open_how` struct. However, the caller must ensure that `dir` can provide a
190+
// valid file descriptor (this can be changed to BorrowedFd).
191+
let ret = unsafe {
192+
libc::syscall(
193+
libc::SYS_openat2,
194+
dir.as_raw_fd(),
195+
path.as_ptr(),
196+
std::ptr::addr_of!(how),
197+
std::mem::size_of::<libc::open_how>(),
198+
)
199+
};
200+
if ret == -1 {
201+
Err(io::Error::last_os_error())
202+
} else {
203+
// Safe because we have just open the RawFd.
204+
let file = unsafe { File::from_raw_fd(ret as RawFd) };
205+
Ok(file)
206+
}
207+
}
208+
}
209+
210+
#[cfg(test)]
211+
mod tests {
212+
use super::*;
213+
214+
#[test]
215+
fn test_openat2() {
216+
let topdir = env!("CARGO_MANIFEST_DIR");
217+
let dir = File::open(topdir).unwrap();
218+
let filename = CString::new("build.rs").unwrap();
219+
220+
let opener = SafeOpenAt::new();
221+
assert!(opener.has_openat2);
222+
opener.openat(&dir, &filename, libc::O_RDONLY, 0).unwrap();
223+
}
224+
}

0 commit comments

Comments
 (0)