memory_pages

memory_pages provides a cross-platform memory management API which allows for acquiring memory pages from the kernel and setting permissions on them

1 unstable release

0.1.0 Mar 24, 2023

#258 in Memory management

MIT license

73KB
1.5K SLoC

memory_pages: High level API for low level memory management

While using low-level memory management in a project can provide substantial benefits, it is very often cumbersome. The API-s differ significantly between OS-s and have many pitfalls one can easily fall into. But what if, all the unsafety, all the platform-specific differences could be simply abstracted away? What if you could do very fine-grain memory adjustments without ever seeing a pointer? This crate provides such zero-cost abstractions.

For who it is for?

This crate is mostly meant for performance critical projects, especially ones dealing with huge amounts of data. The APIs and types provided by this crate are not universal solutions to all problems. Since this crate is designed with those applications in mind, it can achieve great improvements(2x faster allocations) over using standard memory management. Another example of strengths of this crate is PagedVec-s clear_decommit function that not only clears the vector - it also informs the OS about the vec being cleared. This information allows the physical memory pages storing the data to be decommited, making the PagedVec occupy no space in the RAM until it is written into, while still having the required space reserved.

What does memory_pages provide?

Safety by strict typing

One of the common pitfalls of using low-level APIs is the ease of making mistakes regarding page permissions. Mixing them up can at best lead to a segfault, and at worst introduce serious security vulnerabilities. This crate leverages rusts type system(zero sized marker types) to make certain kinds of errors simply impossible. Trying to acquire a mutable reference, and writing into data that was marked as read-only would normally lead to a segfault and crash at runtime. A collection of memory pages, called, unsurprisingly, Pages, must have a AllowWrite marker type, in order to implement functions and traits that allow for it to be written into. This turns all sorts of horrible runtime errors into compile-time ones, making it impossible to miss them.

Holds your hand, but does not hold you back.

This crates core philosophy is to always guide, never restrict. Almost everything that can be done with this crate can be done without ever seeing the word unsafe. While references and the special FnRef type are automatically invalidated on permission changes, those safety restrictions can be easily subverted by using unsafe functions and pointers. There are some very unsafe APIs, which are locked behind feature gates.

Highly performant

Getting memory pages directly and cutting the middle man can be 2x faster!

Memory acquisition route Speed of allocating space for 50 MB structure
Standard Allocator 6.5299 µs
Allocating pages manually 3.5670 µs

Provide useful hints to the kernel

Use functions such as adivse_use_soon, advise_use_seq, adivse_use_rng to provide memory usage hints.

With great power comes great responsibility

Just as this crate can greatly improve performance and reduce memory usage, it can also decrease performance and increase memory usage. While achieving results substantially worse than using default allocators is pretty hard and requires being very very sloppy, it is harder to squeeze everything out of this crate. Gain from usage depends on the competence of the user. As a good example, in some tests, a 2x allocation time reduction was achieved. But those examples were extensively tweaked, to find out the maximal potential performance gain.

Kernel memory usage hints

Is the memory use going to be sequential or random? Provide optional hints to the kernel which may improve performance in some cases.

Examples

Dealing with pages directly

Data storage

use memory_pages::*;
let mut memory = Pages<AllowRead,AllowWrite,DenyExec> = Pages::new(0x40000);
read_data(&mut memory).unwrap();
validate_data(&mut memory);

Prevent writes

use memory_pages::*;
let mut memory = Pages<AllowRead,AllowWrite,DenyExec> = Pages::new(0x40000);
read_data(&mut memory).unwrap();
let mut memory.deny_write();
// `memory` is now read-only and a write attempt would case a segfault
// Because of that, borrowing it as `&mut [u8]` is now not avalible, so this would not compile if used
// write_data(&mut memory);

x86_64 function assembled at run-time

This example does not work on windows, due to differences in the calling conventions

use memory_pages::*;
let mut memory:Pages<AllowRead,AllowWrite,DenyExec> = Pages::new(0x4000);
// hex-encoded X86_64 assembly for adding 2 numbers
// lea 	rax, [rdi+rsi]
memory[0] = 0x48;
memory[1] = 0x8d;
memory[2] = 0x04;
memory[3] = 0x37;
// ret
memory[4] = 0xC3;
// Sets execution to allow and write to denny to prevent exploits
let memory = memory.set_protected_exec();
//TODO: this should check for lifetimes!
let add:extern "C" fn(u64,u64)->u64 = unsafe{memory.get_fn(0)};
assert_eq!(add(43,34),77);

PagedVec

Create new vec

let mut vec = PagedVec::new(0x10000);
for _ in 1_000_000{
    vec.push(102.32);
}

Clear and deccomit

let mut vec = PagedVec::new(0x10000);
for _ in 1_000_000{
    vec.push(102.32);
}
// Clears vec, keeping the capacity but freeing the phisical memory,
// which is automaticaly reclaimed as it is filled back up.
vec.clear_decommit();

Dependencies

~175KB