Skip to content

Commit bd3a7f1

Browse files
committed
Add support for openat2.
Adds support for openat2 on linux. Includes a new ResolveFlag struct to pass resolve flags, which is passed directly to the new fcntl::openat2 function. libc::open_how isn't exposed here, which may mean this API needs to change if there's an update to openat2 to extend open_how with new fields.
1 parent c6a7d40 commit bd3a7f1

File tree

2 files changed

+129
-1
lines changed

2 files changed

+129
-1
lines changed

src/fcntl.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,80 @@ 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+
pub struct ResolveFlag: libc::c_ulonglong {
249+
/// Do not permit the path resolution to succeed if any component of
250+
/// the resolution is not a descendant of the directory indicated by
251+
/// dirfd. This causes absolute symbolic links (and absolute values of
252+
/// pathname) to be rejected.
253+
RESOLVE_BENEATH;
254+
255+
/// Treat the directory referred to by dirfd as the root directory
256+
/// while resolving pathname.
257+
RESOLVE_IN_ROOT;
258+
259+
/// Disallow all magic-link resolution during path resolution. Magic
260+
/// links are symbolic link-like objects that are most notably found
261+
/// in proc(5); examples include `/proc/[pid]/exe` and `/proc/[pid]/fd/*`.
262+
///
263+
/// See symlink(7) for more details.
264+
RESOLVE_NO_MAGICLINKS;
265+
266+
/// Disallow resolution of symbolic links during path resolution. This
267+
/// option implies RESOLVE_NO_MAGICLINKS.
268+
RESOLVE_NO_SYMLINKS;
269+
270+
/// Disallow traversal of mount points during path resolution (including
271+
/// all bind mounts).
272+
RESOLVE_NO_XDEV;
273+
}
274+
}
275+
276+
/// open or create a file for reading, writing or executing.
277+
///
278+
/// `openat2` is an extension of the [`openat`] function that allows the caller
279+
/// to control how path resolution happens.
280+
///
281+
/// See [the man page](man) for more information.
282+
///
283+
/// [man]: https://man7.org/linux/man-pages/man2/openat2.2.html
284+
#[cfg(target_os = "linux")]
285+
pub fn openat2<P: ?Sized + NixPath>(
286+
dirfd: RawFd,
287+
path: &P,
288+
oflag: OFlag,
289+
mode: Mode,
290+
resolve: ResolveFlag,
291+
) -> Result<RawFd> {
292+
let mut how = open_how(oflag, mode, resolve);
293+
let fd = path.with_nix_path(|cstr| unsafe {
294+
libc::syscall(
295+
libc::SYS_openat2,
296+
dirfd,
297+
cstr.as_ptr(),
298+
&mut how as *mut _,
299+
std::mem::size_of::<libc::open_how>(),
300+
)
301+
})?;
302+
Errno::result(fd as i32)
303+
}
304+
305+
#[cfg(target_os = "linux")]
306+
fn open_how(oflag: OFlag, mode: Mode, resolve: ResolveFlag) -> libc::open_how {
307+
use std::ptr::addr_of_mut;
308+
309+
let mut how = std::mem::MaybeUninit::<libc::open_how>::uninit();
310+
let ptr = how.as_mut_ptr();
311+
unsafe {
312+
addr_of_mut!((*ptr).flags).write(oflag.bits() as libc::c_ulonglong);
313+
addr_of_mut!((*ptr).mode).write(mode.bits() as libc::c_ulonglong);
314+
addr_of_mut!((*ptr).resolve).write(resolve.bits());
315+
how.assume_init()
316+
}
317+
}
318+
245319
/// Change the name of a file.
246320
///
247321
/// The `renameat` function is equivalent to `rename` except in the case where either `old_path`

test/test_fcntl.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use nix::errno::*;
33
#[cfg(not(target_os = "redox"))]
44
use nix::fcntl::{open, readlink, OFlag};
55
#[cfg(not(target_os = "redox"))]
6-
use nix::fcntl::{openat, readlinkat, renameat};
6+
use nix::fcntl::{openat, openat2, readlinkat, renameat, ResolveFlag};
77
#[cfg(all(
88
target_os = "linux",
99
target_env = "gnu",
@@ -57,6 +57,60 @@ fn test_openat() {
5757
close(dirfd).unwrap();
5858
}
5959

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

0 commit comments

Comments
 (0)