Skip to content

Latest commit

 

History

82 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mprotect-rs

A Rust library for hardware-enforced memory protection using Intel PKU (Memory Protection Keys). This library provides a type-safe, RAII-based interface to manage memory access permissions at the thread level with extremely low overhead.

Overview

Traditional memory protection using mprotect requires system calls and TLB flushes, which are expensive. Intel PKU allows userspace applications to change access permissions of memory pages within a few CPU cycles by modifying the PKRU register.

pkeyguard-rs leverages Rust's ownership model and borrow checker to ensure that:

Memory is only accessible when the appropriate guard is in scope.

Read/Write permissions are enforced both at compile-time (via Rust references) and runtime (via Hardware CPU exceptions).

Features

Zero-cost Abstractions: Direct mapping of PKU instructions to Rust's type system.

RAII Guards: Access rights are automatically revoked when the guard goes out of scope.

Defense in Depth: Even if an attacker bypasses the borrow checker with unsafe code, the hardware (Intel PKU) will trigger a Segmentation Fault if the permissions are not set correctly.

Thread-Local Isolation: Permissions are managed per-thread, as the PKRU register is part of the thread context.

Usage

Example: Secure Memory Access

use pkeyguard::{RegionGuard, PkeyGuard, AccessPermissions, PkeyPermissions, allocator};

fn main() -> Result<(), RuntimeError> {
    // 1. Allocate a memory region via mmap/mprotect
    // Default is ReadWrite, but we will control access via PKU later.
    let mut region = RegionGuard::<allocator::Mmap, u32>::new(0, AccessPermissions::ReadWrite)
        .map_err(RuntimeError::MprotectError)?;

    // 2. Create a new Protection Key (Pkey)
    // Initially set to NoAccess for maximum security.
    let pkey = PkeyGuard::new(PkeyPermissions::NoAccess)
        .map_err(RuntimeError::MprotectError)?;

    // 3. Associate the region with the Protection Key
    // From here, the hardware enforces access based on the pkey's state.
    let mut associated_region = pkey
        .associate::<PkeyPermissions::NoAccess>(&mut region)
        .map_err(RuntimeError::MprotectError)?;

    // 4. Write access block
    {
        // Temporarily elevate permissions to ReadWrite
        let write_guard = associated_region
            .set_access_rights::<PkeyPermissions::ReadWrite>()
            .map_err(RuntimeError::MprotectError)?;
        
        let mut mut_ref_guard = write_guard.mut_ref_guard()
            .map_err(|e| RuntimeError::PkeyGuardError(e))?;

        *mut_ref_guard = 123; // Safe write
        println!("Value written: {}", *mut_ref_guard);
    } // write_guard drops here, pkey reverts to NoAccess automatically

    // 5. Read-only access block
    {
        let read_guard = associated_region
            .set_access_rights::<PkeyPermissions::ReadOnly>()
            .map_err(RuntimeError::MprotectError)?;
        
        let ref_guard = read_guard.ref_guard()
            .map_err(|e| RuntimeError::PkeyGuardError(e))?;

        println!("Value read: {}", *ref_guard);

        // Hardware Enforcement Example:
        // Even if we use unsafe code to bypass the borrow checker, 
        // the CPU will trigger a Segmentation Fault because the PKRU register
        // is set to ReadOnly.
        /*
        unsafe {
            let mut_ref = ref_guard.ptr() as *mut u32;
            *mut_ref = 789; // CRASH: Segmentation Fault (Hardware-enforced)
        }
        */
    }

    Ok(())
}

How it Works

  1. RegionGuard: Wraps a memory region allocated via mmap.
  2. PkeyGuard: Manages the lifecycle of an Intel PKU key (0-15).
  3. Association: Uses pkey_mprotect to tag specific memory pages with a protection key.
  4. set_access_rights: Executes the WRPKRU instruction to modify the current thread's access rights. This is much faster than a system call.

Allocators

Mmap is the recommended allocator for protected regions because mmap returns page-aligned memory and accepts page-level protection flags directly.

Jmalloc is also available for jemalloc-backed regions. It allocates a page-aligned block, rounds the protected length up to the system page size, and applies mprotect to that page-sized region. This page alignment is required because mprotect and pkey_mprotect operate on page boundaries.

Safety Guarantees

The library uses the Typestate Pattern. You cannot obtain a mutable reference to the protected data unless you have explicitly transitioned the associated_region into a ReadWrite state. This prevents logical errors where memory might be left in an unprotected state.

Testing

Run the integration tests with:

cargo test --tests

The integration tests always exercise the mprotect, allocator, and RegionGuard paths, including a regression test that verifies Jmalloc regions are page-aligned, page-sized, and accepted by mprotect. MPK-specific tests first check whether the current environment supports MPK by verifying x86 PKU/OSPKE CPU support and trying pkey_alloc; they return early when MPK is unavailable.

Run the sample program without MPK workloads:

cargo run

Run the sample program with MPK workloads enabled when the environment supports them:

cargo run -- --mpk

In the QEMU environment started by ~/qemu/boot.sh, use cargo run -- --mpk to run the MPK sample workloads. If MPK is unavailable, the program prints a skip message instead of executing MPK instructions.

Requirements

  • CPU: Intel Skylake Scalable or newer (supporting the PKU feature).
  • OS: Linux kernel 4.9 or later with CONFIG_X86_INTEL_MEMORY_PROTECTION_KEYS enabled.

License

This project is licensed under the MIT License.

About

An implementation of mprotect() and pkey_mprotect() for Rust. This enables Rust to set access rights to each pages, using PTE flags or Intel MPK (Memory Protection Keys).

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages