|
3 | 3 | // found in the LICENSE-BSD-3-Clause file.
|
4 | 4 | // SPDX-License-Identifier: Apache-2.0
|
5 | 5 |
|
| 6 | +use std::ffi::{CStr, CString}; |
| 7 | +use std::fs::File; |
| 8 | +use std::io; |
| 9 | +use std::os::fd::{AsRawFd, FromRawFd, RawFd}; |
| 10 | + |
6 | 11 | use vm_memory::ByteValued;
|
7 | 12 |
|
| 13 | +use super::util::openat; |
| 14 | + |
8 | 15 | #[repr(C, packed)]
|
9 | 16 | #[derive(Clone, Copy, Debug, Default)]
|
10 | 17 | pub struct LinuxDirent64 {
|
@@ -66,3 +73,195 @@ pub const STATX_BASIC_STATS: libc::c_uint = 0x07ff;
|
66 | 73 |
|
67 | 74 | #[cfg(not(target_env = "gnu"))]
|
68 | 75 | 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