Skip to content

Commit 4ab23c3

Browse files
authored
Add support for openat2 (#2339)
Adds an openat2 function with an OpenHow and ResolveFlag options for configuring path resolution. Fixes #1400.
1 parent 5b1b695 commit 4ab23c3

File tree

3 files changed

+176
-0
lines changed

3 files changed

+176
-0
lines changed

changelog/2339.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added support for openat2 on linux.

src/fcntl.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,119 @@ pub fn openat<P: ?Sized + NixPath>(
242242
Errno::result(fd)
243243
}
244244

245+
cfg_if::cfg_if! {
246+
if #[cfg(target_os = "linux")] {
247+
libc_bitflags! {
248+
/// Path resolution flags.
249+
///
250+
/// See [path resolution(7)](https://man7.org/linux/man-pages/man7/path_resolution.7.html)
251+
/// for details of the resolution process.
252+
pub struct ResolveFlag: libc::c_ulonglong {
253+
/// Do not permit the path resolution to succeed if any component of
254+
/// the resolution is not a descendant of the directory indicated by
255+
/// dirfd. This causes absolute symbolic links (and absolute values of
256+
/// pathname) to be rejected.
257+
RESOLVE_BENEATH;
258+
259+
/// Treat the directory referred to by dirfd as the root directory
260+
/// while resolving pathname.
261+
RESOLVE_IN_ROOT;
262+
263+
/// Disallow all magic-link resolution during path resolution. Magic
264+
/// links are symbolic link-like objects that are most notably found
265+
/// in proc(5); examples include `/proc/[pid]/exe` and `/proc/[pid]/fd/*`.
266+
///
267+
/// See symlink(7) for more details.
268+
RESOLVE_NO_MAGICLINKS;
269+
270+
/// Disallow resolution of symbolic links during path resolution. This
271+
/// option implies RESOLVE_NO_MAGICLINKS.
272+
RESOLVE_NO_SYMLINKS;
273+
274+
/// Disallow traversal of mount points during path resolution (including
275+
/// all bind mounts).
276+
RESOLVE_NO_XDEV;
277+
}
278+
}
279+
280+
/// Specifies how [openat2] should open a pathname.
281+
///
282+
/// See <https://man7.org/linux/man-pages/man2/open_how.2type.html>
283+
#[repr(transparent)]
284+
#[derive(Clone, Copy, Debug)]
285+
pub struct OpenHow(libc::open_how);
286+
287+
impl OpenHow {
288+
/// Create a new zero-filled `open_how`.
289+
pub fn new() -> Self {
290+
// safety: according to the man page, open_how MUST be zero-initialized
291+
// on init so that unknown fields are also zeroed.
292+
Self(unsafe {
293+
std::mem::MaybeUninit::zeroed().assume_init()
294+
})
295+
}
296+
297+
/// Set the open flags used to open a file, completely overwriting any
298+
/// existing flags.
299+
pub fn flags(mut self, flags: OFlag) -> Self {
300+
let flags = flags.bits() as libc::c_ulonglong;
301+
self.0.flags = flags;
302+
self
303+
}
304+
305+
/// Set the file mode new files will be created with, overwriting any
306+
/// existing flags.
307+
pub fn mode(mut self, mode: Mode) -> Self {
308+
let mode = mode.bits() as libc::c_ulonglong;
309+
self.0.mode = mode;
310+
self
311+
}
312+
313+
/// Set resolve flags, completely overwriting any existing flags.
314+
///
315+
/// See [ResolveFlag] for more detail.
316+
pub fn resolve(mut self, resolve: ResolveFlag) -> Self {
317+
let resolve = resolve.bits();
318+
self.0.resolve = resolve;
319+
self
320+
}
321+
}
322+
323+
// safety: default isn't derivable because libc::open_how must be zeroed
324+
impl Default for OpenHow {
325+
fn default() -> Self {
326+
Self::new()
327+
}
328+
}
329+
330+
/// Open or create a file for reading, writing or executing.
331+
///
332+
/// `openat2` is an extension of the [`openat`] function that allows the caller
333+
/// to control how path resolution happens.
334+
///
335+
/// # See also
336+
///
337+
/// [openat2](https://man7.org/linux/man-pages/man2/openat2.2.html)
338+
pub fn openat2<P: ?Sized + NixPath>(
339+
dirfd: RawFd,
340+
path: &P,
341+
mut how: OpenHow,
342+
) -> Result<RawFd> {
343+
let fd = path.with_nix_path(|cstr| unsafe {
344+
libc::syscall(
345+
libc::SYS_openat2,
346+
dirfd,
347+
cstr.as_ptr(),
348+
&mut how as *mut OpenHow,
349+
std::mem::size_of::<libc::open_how>(),
350+
)
351+
})?;
352+
353+
Errno::result(fd as RawFd)
354+
}
355+
}
356+
}
357+
245358
/// Change the name of a file.
246359
///
247360
/// The `renameat` function is equivalent to `rename` except in the case where either `old_path`

test/test_fcntl.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ use nix::errno::*;
44
use nix::fcntl::{open, readlink, OFlag};
55
#[cfg(not(target_os = "redox"))]
66
use nix::fcntl::{openat, readlinkat, renameat};
7+
8+
#[cfg(target_os = "linux")]
9+
use nix::fcntl::{openat2, OpenHow, ResolveFlag};
10+
711
#[cfg(all(
812
target_os = "linux",
913
target_env = "gnu",
@@ -57,6 +61,64 @@ fn test_openat() {
5761
close(dirfd).unwrap();
5862
}
5963

64+
#[test]
65+
#[cfg(target_os = "linux")]
66+
// QEMU does not handle openat well enough to satisfy this test
67+
// https://gitlab.com/qemu-project/qemu/-/issues/829
68+
#[cfg_attr(qemu, ignore)]
69+
fn test_openat2() {
70+
const CONTENTS: &[u8] = b"abcd";
71+
let mut tmp = NamedTempFile::new().unwrap();
72+
tmp.write_all(CONTENTS).unwrap();
73+
74+
let dirfd =
75+
open(tmp.path().parent().unwrap(), OFlag::empty(), Mode::empty())
76+
.unwrap();
77+
78+
let fd = openat2(
79+
dirfd,
80+
tmp.path().file_name().unwrap(),
81+
OpenHow::new()
82+
.flags(OFlag::O_RDONLY)
83+
.mode(Mode::empty())
84+
.resolve(ResolveFlag::RESOLVE_BENEATH),
85+
)
86+
.unwrap();
87+
88+
let mut buf = [0u8; 1024];
89+
assert_eq!(4, read(fd, &mut buf).unwrap());
90+
assert_eq!(CONTENTS, &buf[0..4]);
91+
92+
close(fd).unwrap();
93+
close(dirfd).unwrap();
94+
}
95+
96+
#[test]
97+
#[cfg(target_os = "linux")]
98+
// QEMU does not handle openat well enough to satisfy this test
99+
// https://gitlab.com/qemu-project/qemu/-/issues/829
100+
#[cfg_attr(qemu, ignore)]
101+
fn test_openat2_forbidden() {
102+
let mut tmp = NamedTempFile::new().unwrap();
103+
tmp.write_all(b"let me out").unwrap();
104+
105+
let dirfd =
106+
open(tmp.path().parent().unwrap(), OFlag::empty(), Mode::empty())
107+
.unwrap();
108+
109+
let escape_attempt =
110+
tmp.path().parent().unwrap().join("../../../hello.txt");
111+
112+
let res = openat2(
113+
dirfd,
114+
&escape_attempt,
115+
OpenHow::new()
116+
.flags(OFlag::O_RDONLY)
117+
.resolve(ResolveFlag::RESOLVE_BENEATH),
118+
);
119+
assert_eq!(Err(Errno::EXDEV), res);
120+
}
121+
60122
#[test]
61123
#[cfg(not(target_os = "redox"))]
62124
fn test_renameat() {

0 commit comments

Comments
 (0)