Skip to content

Rust Deep Dive: Unsafe Rust, POSIX C-FFI & Linux Low-Level Systems

Created: 2026-11-06 Last updated: 2026-11-07

Executive Summary: Safe Rust provides uncompromising memory safety, but physical computers are inherently unsafe: hardware memory-mapped registers (MMIO), operating system kernel boundaries, DMA ring buffers, and legacy C libraries operate outside the borrow checker's static worldview. Rust solves this through a rigorous dual-world philosophy: Unsafe Rust. The unsafe keyword does not disable the compiler, turn off type checking, or relax borrow rules—it merely grants the systems engineer five specific superpowers to interface directly with bare metal. The golden rule of low-level Rust engineering is to encapsulate unsafe operations behind mathematically safe, ergonomic abstractions so that external consumers can never trigger Undefined Behavior (UB). This chapter forms Part 3.3 (Advanced & Deep Dive): deconstructing the 5 superpowers, mastering raw pointer arithmetic, bridging POSIX C-ABIs via extern "C", converting UTF-8 strings to null-terminated CStr, executing inline CPU assembly via asm!, verifying memory invariants with Miri, and engineering a standalone low-level POSIX telemetry driver.

  • The 5 Superpowers of unsafe


    Delineate the exact boundaries of unsafe: dereferencing raw pointers, calling foreign C functions, implementing unsafe traits, and accessing union fields.

  • Raw Pointers & Memory Alignment


    Manipulate *const T and *mut T, perform raw pointer offset arithmetic, enforce CPU byte alignment invariants, and prevent undefined null dereferences.

  • POSIX C-FFI & ABI Bridging


    Declare C signatures with extern "C", enforce deterministic struct memory layouts via #[repr(C)], and safely bridge strings using CString and CStr.

  • Inline Assembly & Miri Verification


    Execute direct CPU instructions via core::arch::asm!, target #![no_std] bare-metal environments, and verify pointer validity with the Miri interpreter.


1. The Philosophy of unsafe: Safe Encapsulation

In Rust, unsafe is not an escape hatch to write sloppy code; it is an explicit contract. When you write an unsafe block, you are informing the compiler: "I have verified the invariants that your static borrow checker cannot prove."

%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    subgraph EncapsulationModel["Rust Low-Level Architectural Isolation"]
        direction TB
        subgraph SafePublicAPI["Safe Public API Boundary (100% Invariant Checked)"]
            direction TB
            Consumer["High-Level Client Application Code"]
            SafeMethod["pub fn read_kernel_uptime() -> Result<Duration, DriverError>"]
            Consumer --> SafeMethod
        end

        subgraph UnsafeCore["Unsafe Hardware & OS Boundary"]
            direction TB
            UB["unsafe { libc::clock_gettime(CLOCK_MONOTONIC, &mut ts) }"]
            RawPtr["Raw Pointer Arithmetic (*mut timespec)"]
            UB --- RawPtr
        end

        SafeMethod ==>|Guaranteed Safe Invariants| UnsafeCore
    end

The 5 Superpowers of unsafe

Inside an unsafe block or function, you are granted exactly five capabilities that are forbidden in safe Rust:

  1. Dereferencing Raw Pointers (*const T, *mut T).
  2. Calling Unsafe Functions or Methods (including Foreign Function Interface / C functions).
  3. Implementing an Unsafe Trait (such as Send or Sync for raw pointers).
  4. Mutating Mutable Static State (static mut CONFIG).
  5. Accessing Fields of a union (untagged C-style unions).

[!IMPORTANT] The borrow checker is still fully active inside unsafe blocks! Standard references (&T, &mut T) still have their lifetimes and aliasing rules strictly checked.


2. Raw Pointers Deconstructed: *const T & *mut T

Unlike safe references (&T, &mut T), Raw Pointers: - Are allowed to ignore the Aliasing-XOR-Mutability rule (multiple mutable raw pointers can point to the same memory). - Are allowed to be null (std::ptr::null()). - Are not protected by lifetimes (can point to deallocated memory). - Do not implement automatic RAII Drop cleanup.

Creating a raw pointer is 100% safe; only dereferencing it is unsafe:

Creating and Dereferencing Raw Pointers
let mut value: u64 = 42;

// Safe: Creating raw pointers from references
let raw_const: *const u64 = &value as *const u64;
let raw_mut: *mut u64 = &mut value as *mut u64;

// Unsafe: Dereferencing raw pointers
unsafe {
    *raw_mut += 100;
    println!("Mutated raw pointer value: {}", *raw_const);
}

Pointer Arithmetic and Offset Safety

When navigating memory buffers (e.g., custom allocators or network packet headers), Rust provides pointer arithmetic via .offset() and .add():

Raw Pointer Arithmetic
let buffer = [10u8, 20, 30, 40, 50];
let base_ptr: *const u8 = buffer.as_ptr();

unsafe {
    // Advance pointer by 3 bytes
    let third_element = base_ptr.add(3);
    assert_eq!(*third_element, 40);
}

3. Foreign Function Interface (FFI): Interoperating with C

The Linux operating system kernel exposes its system calls and standard libraries through the C Application Binary Interface (C-ABI). Rust interfaces with C via extern "C".

Matching Data Layouts with #[repr(C)]

To prevent rustc from reordering fields to optimize padding, structs shared with C must be annotated with #[repr(C)]:

Declaring C-Compatible Structs
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct TimeSpec {
    pub tv_sec: i64,  // seconds
    pub tv_nsec: i64, // nanoseconds
}

// Direct declaration of external POSIX libc functions
extern "C" {
    pub fn clock_gettime(clock_id: i32, tp: *mut TimeSpec) -> i32;
    pub fn getpid() -> i32;
}

4. Bridging Strings: CString vs. CStr

In Rust, strings (&str, String) are UTF-8 byte sequences with an explicit length, and they do not have a trailing null terminator (\0). In C, strings are null-terminated pointer arrays (char*). Passing a Rust &str directly into a C function will result in buffer over-reads!

%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    subgraph StringBridging["Rust to C String ABI Translation"]
        direction TB
        subgraph RustString["Rust: &str or String"]
            direction TB
            RS["UTF-8 Byte Sequence<br/>Explicit len: 5 bytes<br/>['h', 'e', 'l', 'l', 'o']<br/>NO null terminator!"]
        end

        subgraph Bridge["Conversion via CString"]
            direction TB
            CS["std::ffi::CString::new(s)?<br/>Appends \0, scans for interior nulls"]
        end

        subgraph CStringABI["C ABI: const char*"]
            direction TB
            CP["Null-Terminated Array (*const c_char)<br/>['h', 'e', 'l', 'l', 'o', '\0']"]
        end

        RustString --> Bridge --> CStringABI
    end
Safe String Crossing
use std::ffi::{CStr, CString};

// 1. Rust -> C: Construct owned null-terminated CString
let rust_path = "/etc/os-release";
let c_path = CString::new(rust_path).expect("CString::new failed: Interior null byte detected");
let raw_c_ptr: *const libc::c_char = c_path.as_ptr(); // Safe to pass to POSIX open()

// 2. C -> Rust: Borrow null-terminated pointer as &CStr without allocation
unsafe {
    let c_str: &CStr = CStr::from_ptr(raw_c_ptr);
    let recovered_str: &str = c_str.to_str().expect("C string was not valid UTF-8");
    assert_eq!(recovered_str, "/etc/os-release");
}

5. Inline Assembly (core::arch::asm!)

For kernel development, real-time clock cycles, and hardware drivers, Rust supports direct inline assembly via the stabilized asm! macro:

Reading the Hardware Timestamp Counter (x86_64 RDTSC)
#[inline(always)]
pub fn read_cpu_tsc() -> u64 {
    #[cfg(target_arch = "x86_64")]
    unsafe {
        let low: u32;
        let high: u32;
        core::arch::asm!(
            "rdtsc",
            out("eax") low,
            out("edx") high,
            options(nomem, nostack)
        );
        ((high as u64) << 32) | (low as u64)
    }
    #[cfg(not(target_arch = "x86_64"))]
    {
        0u64 // Fallback stub for non-x86 architectures
    }
}

6. Undefined Behavior & Formal Verification with Miri

Writing unsafe code carries the severe risk of introducing Undefined Behavior (UB): - Dereferencing dangling, unaligned, or null pointers. - Violating the Stacked Borrows / Tree Borrows aliasing model (e.g., mutating through a pointer derived from a shared reference). - Reading uninitialized memory. - Producing an invalid primitive value (e.g., a bool with byte value 2).

The Rust project provides Miri: an official interpreter that executes Rust intermediate representation (MIR) and verifies that unsafe code adheres strictly to the formal Rust memory model.

Executing Formal Verification with Miri
# Install and execute Miri on your test suite
cargo +nightly miri test

7. Hands-On Systems Lab: posix_raw_driver.rs

We will construct a production-ready, bare-metal Linux POSIX driver. It directly interfaces with Linux libc without external dependencies: queries kernel telemetry with uname, reads nanosecond clocks via clock_gettime, inspects raw memory bytes, and wraps all unsafe operations in a 100% safe, leak-free Rust struct.

posix_raw_driver.rs (Standalone Executable Systems Lab)
use std::ffi::CStr;
use std::fmt;
use std::mem::MaybeUninit;

/// POSIX Clock ID: CLOCK_MONOTONIC (System-wide monotonic clock)
const CLOCK_MONOTONIC: i32 = 1;

/// C-Compatible Timespec Structure
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct TimeSpec {
    pub tv_sec: i64,  // seconds
    pub tv_nsec: i64, // nanoseconds
}

/// Linux utsname structure (POSIX uname syscall)
/// On Linux, each field is a fixed null-terminated char array of 65 bytes
#[repr(C)]
pub struct UtsName {
    pub sysname: [u8; 65],
    pub nodename: [u8; 65],
    pub release: [u8; 65],
    pub version: [u8; 65],
    pub machine: [u8; 65],
    pub domainname: [u8; 65],
}

// Direct FFI Declarations against Linux C standard library (libc)
extern "C" {
    fn getpid() -> i32;
    fn uname(buf: *mut UtsName) -> i32;
    fn clock_gettime(clk_id: i32, tp: *mut TimeSpec) -> i32;
}

/// Safe Rust representation of Kernel Telemetry
#[derive(Debug, Clone)]
pub struct LinuxKernelInfo {
    pub os_name: String,
    pub hostname: String,
    pub kernel_release: String,
    pub architecture: String,
    pub pid: i32,
}

impl fmt::Display for LinuxKernelInfo {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "OS: {} | Host: {} | Kernel: {} | Arch: {} (PID: {})",
            self.os_name, self.hostname, self.kernel_release, self.architecture, self.pid
        )
    }
}

/// Safe Encapsulating Driver
pub struct PosixRawDriver;

impl PosixRawDriver {
    /// Queries Linux kernel telemetry via raw uname FFI call
    pub fn query_kernel_info() -> Result<LinuxKernelInfo, &'static str> {
        let pid = unsafe { getpid() };

        // Use MaybeUninit to safely allocate uninitialized stack buffer for C to populate
        let mut raw_utsname = MaybeUninit::<UtsName>::uninit();

        let result = unsafe { uname(raw_utsname.as_mut_ptr()) };
        if result != 0 {
            return Err("Failed to execute uname syscall");
        }

        // Now initialized by kernel
        let uts = unsafe { raw_utsname.assume_init() };

        // Helper closure to convert null-terminated C byte array to safe Rust String
        let parse_c_string = |arr: &[u8]| -> String {
            CStr::from_bytes_until_nul(arr)
                .map(|cstr| cstr.to_string_lossy().into_owned())
                .unwrap_or_else(|_| "UNKNOWN".to_string())
        };

        Ok(LinuxKernelInfo {
            os_name: parse_c_string(&uts.sysname),
            hostname: parse_c_string(&uts.nodename),
            kernel_release: parse_c_string(&uts.release),
            architecture: parse_c_string(&uts.machine),
            pid,
        })
    }

    /// High-resolution nanosecond hardware clock query via POSIX clock_gettime
    pub fn read_monotonic_nanos() -> Result<u64, &'static str> {
        let mut ts = TimeSpec { tv_sec: 0, tv_nsec: 0 };

        let status = unsafe { clock_gettime(CLOCK_MONOTONIC, &mut ts as *mut TimeSpec) };
        if status != 0 {
            return Err("clock_gettime returned non-zero error code");
        }

        let nanos = (ts.tv_sec as u64) * 1_000_000_000 + (ts.tv_nsec as u64);
        Ok(nanos)
    }

    /// Demonstrates safe raw pointer memory inspection and alignment
    pub fn inspect_memory_slice(data: &[u64]) {
        println!("Inspecting raw pointer memory addresses and alignment:");
        let base_ptr = data.as_ptr();

        for i in 0..data.len() {
            unsafe {
                let elem_ptr = base_ptr.add(i);
                let address = elem_ptr as usize;
                let is_aligned = address % std::mem::align_of::<u64>() == 0;
                println!(
                    "  Index [{}]: Address = 0x{:x} | Value = {:>4} | Aligned (8B): {}",
                    i, address, *elem_ptr, is_aligned
                );
            }
        }
    }
}

fn main() {
    println!("=== LINUX RUST LOW-LEVEL POSIX RAW DRIVER LAB ===\n");

    // 1. Query Kernel via FFI
    match PosixRawDriver::query_kernel_info() {
        Ok(info) => {
            println!("Kernel Telemetry Retrieved via POSIX FFI:");
            println!("  {}\n", info);
        }
        Err(e) => eprintln!("Error querying kernel: {e}"),
    }

    // 2. High-Resolution Nanosecond Latency Benchmark
    println!("High-Resolution Monotonic Clock Measurement:");
    match (PosixRawDriver::read_monotonic_nanos(), PosixRawDriver::read_monotonic_nanos()) {
        (Ok(t1), Ok(t2)) => {
            let delta = t2.saturating_sub(t1);
            println!("  Timestamp T1: {} ns", t1);
            println!("  Timestamp T2: {} ns", t2);
            println!("  Time Delta:   {} ns (Syscall Latency)\n", delta);
        }
        _ => eprintln!("Failed to read nanosecond clock"),
    }

    // 3. Raw Pointer Arithmetic & Alignment Inspection
    let test_array = [100u64, 200, 300, 400];
    PosixRawDriver::inspect_memory_slice(&test_array);

    println!("\nDriver Execution Completed: 100% Memory Safety Encapsulated.");
}

Building & Running the Lab

Compile and execute the standalone low-level POSIX driver directly with rustc:

Compiling and Running the POSIX Raw Driver
rustc -O posix_raw_driver.rs
./posix_raw_driver

Expected Terminal Output:

=== LINUX RUST LOW-LEVEL POSIX RAW DRIVER LAB ===

Kernel Telemetry Retrieved via POSIX FFI:
  OS: Linux | Host: srv001 | Kernel: 6.1.0-28-amd64 | Arch: x86_64 (PID: 418290)

High-Resolution Monotonic Clock Measurement:
  Timestamp T1: 1748291048123901 ns
  Timestamp T2: 1748291048124143 ns
  Time Delta:   242 ns (Syscall Latency)

Inspecting raw pointer memory addresses and alignment:
  Index [0]: Address = 0x7ffe94821a80 | Value =  100 | Aligned (8B): true
  Index [1]: Address = 0x7ffe94821a88 | Value =  200 | Aligned (8B): true
  Index [2]: Address = 0x7ffe94821a90 | Value =  300 | Aligned (8B): true
  Index [3]: Address = 0x7ffe94821a98 | Value =  400 | Aligned (8B): true

Driver Execution Completed: 100% Memory Safety Encapsulated.


Rust Masterclass: Complete Curriculum Overview

Congratulations! You have completed the comprehensive Rust Systems Engineering curriculum across all 3 progressive tiers: