Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mlockall and munlockall #872

Merged
merged 1 commit into from Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/backend/libc/mm/syscalls.rs
Expand Up @@ -2,6 +2,8 @@

#[cfg(not(target_os = "redox"))]
use super::types::Advice;
#[cfg(any(linux_kernel, freebsdlike, netbsdlike))]
use super::types::MlockAllFlags;
#[cfg(any(target_os = "emscripten", target_os = "linux"))]
use super::types::MremapFlags;
use super::types::{MapFlags, MprotectFlags, MsyncFlags, ProtFlags};
Expand Down Expand Up @@ -220,3 +222,22 @@ pub(crate) unsafe fn userfaultfd(flags: UserfaultfdFlags) -> io::Result<OwnedFd>
}
ret_owned_fd(userfaultfd(bitflags_bits!(flags)))
}

/// Locks all pages mapped into the address space of the calling process.
///
/// This includes the pages of the code, data and stack segment, as well as shared libraries,
/// user space kernel data, shared memory, and memory-mapped files. All mapped pages are
/// guaranteed to be resident in RAM when the call returns successfully;
/// the pages are guaranteed to stay in RAM until later unlocked.
#[inline]
#[cfg(any(linux_kernel, freebsdlike, netbsdlike))]
pub(crate) fn mlockall(flags: MlockAllFlags) -> io::Result<()> {
unsafe { ret(c::mlockall(bitflags_bits!(flags))) }
}

/// Unlocks all pages mapped into the address space of the calling process.
#[inline]
#[cfg(any(linux_kernel, freebsdlike, netbsdlike))]
pub(crate) fn munlockall() -> io::Result<()> {
unsafe { ret(c::munlockall()) }
}
25 changes: 25 additions & 0 deletions src/backend/libc/mm/types.rs
Expand Up @@ -442,3 +442,28 @@ bitflags! {
const _ = !0;
}
}

#[cfg(any(linux_kernel, freebsdlike, netbsdlike))]
bitflags! {
/// `MCL_*` flags for use with [`mlockall`].
///
/// [`mlockall`]: crate::mm::mlockall
#[repr(transparent)]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct MlockAllFlags: u32 {
// libc doesn't define `MCL_ONFAULT` yet.
// const ONFAULT = libc::MCL_ONFAULT;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// Lock all pages which will become mapped into the address
/// space of the process in the future. These could be, for
/// instance, new pages required by a growing heap and stack
/// as well as new memory-mapped files or shared memory
/// regions.
const FUTURE = bitcast!(libc::MCL_FUTURE);
/// Lock all pages which are currently mapped into the address
/// space of the process.
const CURRENT = bitcast!(libc::MCL_CURRENT);

/// <https://docs.rs/bitflags/latest/bitflags/#externally-defined-flags>
const _ = !0;
}
}
9 changes: 9 additions & 0 deletions src/backend/linux_raw/conv.rs
Expand Up @@ -621,6 +621,15 @@ impl<'a, Num: ArgNumber> From<crate::backend::mm::types::MlockFlags> for ArgReg<
}
}

#[cfg(feature = "mm")]
#[cfg(any(linux_kernel, freebsdlike, netbsdlike))]
impl<'a, Num: ArgNumber> From<crate::backend::mm::types::MlockAllFlags> for ArgReg<'a, Num> {
#[inline]
fn from(flags: crate::backend::mm::types::MlockAllFlags) -> Self {
c_uint(flags.bits())
}
}

#[cfg(feature = "mm")]
impl<'a, Num: ArgNumber> From<crate::backend::mm::types::MapFlags> for ArgReg<'a, Num> {
#[inline]
Expand Down
28 changes: 28 additions & 0 deletions src/backend/linux_raw/mm/syscalls.rs
Expand Up @@ -6,6 +6,8 @@
#![allow(unsafe_code)]
#![allow(clippy::undocumented_unsafe_blocks)]

#[cfg(any(linux_kernel, freebsdlike, netbsdlike))]
use super::types::MlockAllFlags;
use super::types::{
Advice, MapFlags, MlockFlags, MprotectFlags, MremapFlags, MsyncFlags, ProtFlags,
UserfaultfdFlags,
Expand Down Expand Up @@ -210,3 +212,29 @@ pub(crate) unsafe fn munlock(addr: *mut c::c_void, length: usize) -> io::Result<
pub(crate) unsafe fn userfaultfd(flags: UserfaultfdFlags) -> io::Result<OwnedFd> {
ret_owned_fd(syscall_readonly!(__NR_userfaultfd, flags))
}

/// Locks all pages mapped into the address space of the calling process.
///
/// This includes the pages of the code, data and stack segment, as well as shared libraries,
/// user space kernel data, shared memory, and memory-mapped files. All mapped pages are
/// guaranteed to be resident in RAM when the call returns successfully;
/// the pages are guaranteed to stay in RAM until later unlocked.
#[inline]
#[cfg(any(linux_kernel, freebsdlike, netbsdlike))]
pub(crate) fn mlockall(flags: MlockAllFlags) -> io::Result<()> {
// When `mlockall` is used with `MCL_ONFAULT | MCL_FUTURE`, the ordering
// of `mlockall` with respect to arbitrary loads may be significant,
// because if a load happens and evokes a fault before the `mlockall`,
// the memory doesn't get locked, but if the load and therefore
// the fault happens after, then the memory does get locked.
// So to be conservative in this regard, we use `syscall` instead
// of `syscall_readonly`
unsafe { ret(syscall!(__NR_mlockall, flags)) }
}

/// Unlocks all pages mapped into the address space of the calling process.
#[inline]
#[cfg(any(linux_kernel, freebsdlike, netbsdlike))]
pub(crate) fn munlockall() -> io::Result<()> {
unsafe { ret(syscall_readonly!(__NR_munlockall)) }
}
34 changes: 34 additions & 0 deletions src/backend/linux_raw/mm/types.rs
Expand Up @@ -262,3 +262,37 @@ bitflags! {
const _ = !0;
}
}

#[cfg(any(linux_kernel, freebsdlike, netbsdlike))]
bitflags! {
/// `MCL_*` flags for use with [`mlockall`].
///
/// [`mlockall`]: crate::mm::mlockall
#[repr(transparent)]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct MlockAllFlags: u32 {
/// Used together with `MCL_CURRENT`, `MCL_FUTURE`, or both. Mark
/// all current (with `MCL_CURRENT`) or future (with `MCL_FUTURE`)
/// mappings to lock pages when they are faulted in. When
/// used with `MCL_CURRENT`, all present pages are locked, but
/// `mlockall()` will not fault in non-present pages. When used
/// with `MCL_FUTURE`, all future mappings will be marked to
/// lock pages when they are faulted in, but they will not be
/// populated by the lock when the mapping is created.
/// `MCL_ONFAULT` must be used with either `MCL_CURRENT` or
/// `MCL_FUTURE` or both.
const ONFAULT = linux_raw_sys::general::MCL_ONFAULT;
/// Lock all pages which will become mapped into the address
/// space of the process in the future. These could be, for
/// instance, new pages required by a growing heap and stack
/// as well as new memory-mapped files or shared memory
/// regions.
const FUTURE = linux_raw_sys::general::MCL_FUTURE;
/// Lock all pages which are currently mapped into the address
/// space of the process.
const CURRENT = linux_raw_sys::general::MCL_CURRENT;

/// <https://docs.rs/bitflags/latest/bitflags/#externally-defined-flags>
const _ = !0;
}
}
65 changes: 65 additions & 0 deletions src/mm/mmap.rs
Expand Up @@ -10,6 +10,8 @@ use crate::{backend, io};
use backend::fd::AsFd;
use core::ffi::c_void;

#[cfg(any(linux_kernel, freebsdlike, netbsdlike))]
pub use backend::mm::types::MlockAllFlags;
#[cfg(linux_kernel)]
pub use backend::mm::types::MlockFlags;
#[cfg(any(target_os = "emscripten", target_os = "linux"))]
Expand Down Expand Up @@ -340,3 +342,66 @@ pub unsafe fn mlock_with(ptr: *mut c_void, len: usize, flags: MlockFlags) -> io:
pub unsafe fn munlock(ptr: *mut c_void, len: usize) -> io::Result<()> {
backend::mm::syscalls::munlock(ptr, len)
}

/// Locks all pages mapped into the address space of the calling process.
///
/// This includes the pages of the code, data and stack segment, as well as shared libraries,
/// user space kernel data, shared memory, and memory-mapped files. All mapped pages are
/// guaranteed to be resident in RAM when the call returns successfully;
/// the pages are guaranteed to stay in RAM until later unlocked.
///
/// # References
/// - [POSIX]
/// - [Linux]
/// - [FreeBSD]
/// - [NetBSD]
/// - [OpenBSD]
/// - [DragonFly BSD]
/// - [illumos]
/// - [glibc]
///
/// [POSIX]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/mlockall.html
/// [Linux]: https://man7.org/linux/man-pages/man2/mlockall.2.html
/// [FreeBSD]: https://man.freebsd.org/cgi/man.cgi?query=mlockall&sektion=2
/// [NetBSD]: https://man.netbsd.org/mlockall.2
/// [OpenBSD]: https://man.openbsd.org/mlockall.2
/// [DragonFly BSD]: https://man.dragonflybsd.org/?command=mlockall&section=2
/// [illumos]: https://illumos.org/man/3C/mlockall
/// [glibc]: https://www.gnu.org/software/libc/manual/html_node/Page-Lock-Functions.html#index-mlockall
#[cfg(any(linux_kernel, freebsdlike, netbsdlike))]
#[inline]
pub fn mlockall(flags: MlockAllFlags) -> io::Result<()> {
backend::mm::syscalls::mlockall(flags)
}

/// Unlocks all pages mapped into the address space of the calling process.
///
/// # Warnings
///
/// This function is aware of all the memory pages in the process, as if it were a debugger.
/// It unlocks all the pages, which could potentially compromise security assumptions made by
/// code about memory it has encapsulated.
///
/// # References
/// - [POSIX]
/// - [Linux]
/// - [FreeBSD]
/// - [NetBSD]
/// - [OpenBSD]
/// - [DragonFly BSD]
/// - [illumos]
/// - [glibc]
///
/// [POSIX]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/munlockall.html
/// [Linux]: https://man7.org/linux/man-pages/man2/munlockall.2.html
/// [FreeBSD]: https://man.freebsd.org/cgi/man.cgi?query=munlockall&sektion=2
/// [NetBSD]: https://man.netbsd.org/munlockall.2
/// [OpenBSD]: https://man.openbsd.org/munlockall.2
/// [DragonFly BSD]: https://man.dragonflybsd.org/?command=munlockall&section=2
/// [illumos]: https://illumos.org/man/3C/munlockall
/// [glibc]: https://www.gnu.org/software/libc/manual/html_node/Page-Lock-Functions.html#index-munlockall
#[cfg(any(linux_kernel, freebsdlike, netbsdlike))]
#[inline]
pub fn munlockall() -> io::Result<()> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, munlock is unsafe, because it takes a raw pointer and can operate on memory the caller doesn't own or borrow. It doesn't segfault or access data, but it can potentially compromise the security assumptions of the code that does own the memory.

Should munlockall be unsafe too? On one hand, it does a superset of what munlock does, which seems to suggest that it should be unsafe.

But on the other, since munlockall doesn't take a pointer, it somehow has to know where all the allocated pages in the process are, which is something that can really only be done by debuggers or similar. And in Rust, debuggers do not make things unsafe. open isn't unsafe just because you can open /proc/self/mem; that's considered a debugging interface. So in principle, munlockall is like attaching a debugger to the process and doing magic. You can do it, but what it does is outside the scope of Rust semantics.

I'm open to other opinions here, but right now I think I buy this argument that munlockall is like a debugger, and therefore can be a safe function. But that said, we should document this.

The comment should say something along the lines of "warning, this function is aware of all the memory pages in the process, as if it were a debugger. It unlocks all the pages, which could potentially compromise security assumptions made by code about memory it has encapsulated".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add a warning comment, but I don't think that mlockall and munlockall should be marked unsafe. In my understanding, unsafe should be used only for code which could violate Rust memory safety and swapping is far outside of the Rust memory model. I vaguely remember discussions where overwhelming opinion was that using unsafe for code which may cause security issues is a misuse of unsafe.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I ultimately also concluded that they should be safe too, though for slightly different reasons.

I'm aware of the discussions. I believe the strong opinions about this are motivated by the perpetual need to push back against the endless stream of miscellaneous invariants that might get added to unsafe otherwise, and I broadly agree.

My interpretation of memory safety is that it's about anything that bypasses the Rust memory abstraction level, and instead operates at the level of "a pointer is just an integer, memory is just a global array of bytes". This is a subtle and debatable distinction, but my main point here is just that I don't see this as disagreeing with the strong opinions, it's just a specific interpretation of them.

Separately, "/proc/self/mem", teaches us that memory safety is not the only question. We also need to ask something like "is it a debugger?". That's also a subtle question, but my point here is just to say that in this moment, in this context, I think munlockall qualifies as a debugger, which makes it safe (but still deserves documentation comments).

backend::mm::syscalls::munlockall()
}