Skip to content

Commit c68cd0b

Browse files
committed
Add support for openat2
Adds an openat2 function with an OpenHow and ResolveFlag options for configuring path resolution.
1 parent c6a7d40 commit c68cd0b

File tree

2 files changed

+171
-0
lines changed

2 files changed

+171
-0
lines changed

src/fcntl.rs

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

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

test/test_fcntl.rs

Lines changed: 59 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,61 @@ 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+
fn test_openat2_forbidden() {
99+
let mut tmp = NamedTempFile::new().unwrap();
100+
tmp.write_all(b"let me out").unwrap();
101+
102+
let dirfd =
103+
open(tmp.path().parent().unwrap(), OFlag::empty(), Mode::empty())
104+
.unwrap();
105+
106+
let escape_attempt =
107+
tmp.path().parent().unwrap().join("../../../hello.txt");
108+
109+
let res = openat2(
110+
dirfd,
111+
&escape_attempt,
112+
OpenHow::new()
113+
.flags(OFlag::O_RDONLY)
114+
.resolve(ResolveFlag::RESOLVE_BENEATH),
115+
);
116+
assert_eq!(Err(Errno::EXDEV), res);
117+
}
118+
60119
#[test]
61120
#[cfg(not(target_os = "redox"))]
62121
fn test_renameat() {

0 commit comments

Comments
 (0)