Skip to content

Commit 4e9d5a0

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 4e9d5a0

File tree

3 files changed

+212
-23
lines changed

3 files changed

+212
-23
lines changed

src/passthrough/file_handle.rs

-2
Original file line numberDiff line numberDiff line change
@@ -319,9 +319,7 @@ impl OpenableFileHandle {
319319
#[cfg(test)]
320320
mod tests {
321321
use super::*;
322-
use nix::unistd::getuid;
323322
use std::ffi::CString;
324-
use std::io::Read;
325323

326324
fn generate_c_file_handle(
327325
handle_bytes: usize,

src/passthrough/mod.rs

+13-21
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),
@@ -459,8 +462,8 @@ impl<S: BitmapSlice + Send + Sync> PassthroughFs<S> {
459462
pub fn import(&self) -> io::Result<()> {
460463
let root = CString::new(self.cfg.root_dir.as_str()).expect("CString::new failed");
461464

462-
let (path_fd, handle_opt, st) = Self::open_file_and_handle(self, &libc::AT_FDCWD, &root)
463-
.map_err(|e| {
465+
let (path_fd, handle_opt, st) =
466+
Self::open_file_and_handle(self, &libc::AT_FDCWD, &root, false).map_err(|e| {
464467
error!("fuse: import: failed to get file or handle: {:?}", e);
465468
e
466469
})?;
@@ -556,30 +559,19 @@ impl<S: BitmapSlice + Send + Sync> PassthroughFs<S> {
556559
openat(dfd, pathname, flags, mode)
557560
}
558561

559-
fn open_file_restricted(
560-
&self,
561-
dir: &impl AsRawFd,
562-
pathname: &CStr,
563-
flags: i32,
564-
mode: u32,
565-
) -> io::Result<File> {
566-
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-
//}
574-
}
575-
576562
/// Create a File or File Handle for `name` under directory `dir_fd` to support `lookup()`.
577563
fn open_file_and_handle(
578564
&self,
579565
dir: &impl AsRawFd,
580566
name: &CStr,
567+
use_openat2: bool,
581568
) -> io::Result<(File, Option<FileHandle>, StatExt)> {
582-
let path_file = self.open_file_restricted(dir, name, libc::O_PATH, 0)?;
569+
let flags = libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_PATH;
570+
let path_file = if use_openat2 {
571+
self.opener.openat(dir, name, flags, 0)?
572+
} else {
573+
openat(dir, name, flags, 0)?
574+
};
583575
let st = statx(&path_file, None)?;
584576
let handle = if self.cfg.inode_file_handles {
585577
FileHandle::from_fd(&path_file)?
@@ -640,7 +632,7 @@ impl<S: BitmapSlice + Send + Sync> PassthroughFs<S> {
640632

641633
let dir = self.inode_map.get(parent)?;
642634
let dir_file = dir.get_file()?;
643-
let (path_fd, handle_opt, st) = Self::open_file_and_handle(self, &dir_file, name)?;
635+
let (path_fd, handle_opt, st) = Self::open_file_and_handle(self, &dir_file, name, true)?;
644636
let id = InodeId::from_stat(&st);
645637

646638
let mut found = None;

src/passthrough/os_compat.rs

+199
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::ffi::{CStr, CString};
7+
use std::fs::File;
8+
use std::io;
9+
use std::os::fd::{AsRawFd, FromRawFd, RawFd};
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,195 @@ 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+
use std::fs::File;
214+
use std::os::unix::fs;
215+
use vmm_sys_util::tempdir::TempDir;
216+
217+
#[test]
218+
fn test_openat2() {
219+
let topdir = env!("CARGO_MANIFEST_DIR");
220+
let dir = File::open(topdir).unwrap();
221+
let filename = CString::new("build.rs").unwrap();
222+
223+
let opener = SafeOpenAt::new();
224+
assert!(opener.has_openat2);
225+
opener.openat(&dir, &filename, libc::O_RDONLY, 0).unwrap();
226+
}
227+
228+
#[test]
229+
// If pathname is an absolute path, it is also interpreted relative to dirfd.
230+
fn test_openat2_absolute() {
231+
let topdir = env!("CARGO_MANIFEST_DIR");
232+
let dir = File::open(topdir).unwrap();
233+
let filename = CString::new("/build.rs").unwrap();
234+
235+
let opener = SafeOpenAt::new();
236+
assert!(opener.has_openat2);
237+
opener.openat(&dir, &filename, libc::O_RDONLY, 0).unwrap();
238+
}
239+
240+
#[test]
241+
// If a prefix component of pathname equates to dirfd, then an immediately following ..
242+
// component likewise equates to dirfd
243+
fn test_openat2_parent() {
244+
let topdir = env!("CARGO_MANIFEST_DIR");
245+
let dir = File::open(topdir).unwrap();
246+
let filename = CString::new("/../../build.rs").unwrap();
247+
248+
let opener = SafeOpenAt::new();
249+
assert!(opener.has_openat2);
250+
opener.openat(&dir, &filename, libc::O_RDONLY, 0).unwrap();
251+
}
252+
253+
#[test]
254+
// Absolute symbolic links are interpreted relative to dirfd.
255+
fn test_openat2_symlink() {
256+
let topdir = env!("CARGO_MANIFEST_DIR");
257+
let dir = File::open(topdir).unwrap();
258+
let tmpdir = TempDir::new().unwrap();
259+
let dest = tmpdir.as_path().join("build.rs");
260+
fs::symlink("/build.rs", dest).unwrap();
261+
let filename = CString::new("build.rs").unwrap();
262+
263+
let opener = SafeOpenAt::new();
264+
assert!(opener.has_openat2);
265+
opener.openat(&dir, &filename, libc::O_RDONLY, 0).unwrap();
266+
}
267+
}

0 commit comments

Comments
 (0)