Skip to content

Latest commit

 

History

History
162 lines (127 loc) · 6.93 KB

File metadata and controls

162 lines (127 loc) · 6.93 KB

Phase 6 — A serious Rust driver + a C→Rust rewrite

Two more Rust components added to the cloned tree, going beyond the rust_safe_counter misc device of Phase 3:

  1. rust_pci_probe — a real PCI driver (touches hardware over MMIO).
  2. rust_crc16 — a C→Rust rewrite of a kernel routine, verified bit-identical against the original C.

Both are wired into samples/rust/{Kconfig,Makefile} and built built-in (=y) so they run/probe during boot and announce results in dmesg.


1. rust_pci_probe — a Rust PCI driver

Written against QEMU's pci-testdev interface (the same device the upstream Rust PCI sample targets — that's the supported, available test device, and pci::Vendor::REDHAT is the only way to name it from outside the kernel crate, since Vendor::from_raw is crate-private, so QEMU's edu at vendor 0x1234 isn't directly addressable).

What it does, and why each piece is safer than the C equivalent:

Step Rust API used Safety win over C
Match the device pci_device_table! + pci::DeviceId::from_id table is type-checked; no hand-rolled struct pci_device_id arrays
Map BAR0 pdev.iomap_region_sized::<SIZE>(0, name) returning Devres<Bar> the mapping is released automatically on unbind/drop — no leaked iounmap, no use-after-unmap
MMIO registers the register! DSL + bar.read()/write_reg() every access is bounds-checked against SIZE; field widths are encoded in the type
Runtime-offset write bar.try_write8(data, offset)? offset that can't be proven in-range at compile time is checked at runtime and returns Err, not a wild write
Config space pdev.config_space().read(REG) typed reads of vendor/device/revision/BARs

The driver enables the device, maps BAR0 via a lifetime-managed Devres, dumps vendor/device/revision and BAR0 base from config space, then runs the testdev register protocol and logs the resulting data-match count. Boot QEMU with -device pci-testdev to make it probe.

The headline guarantee: the I/O mapping's lifetime is tied to the driver binding by the type system. In C, forgetting pci_iounmap/pci_release_region on an error path is a classic leak/UAF; here it is structurally impossible.

Result — boot with -device pci-testdev (QEMU rc=0)

rust_pci_probe 0000:00:03.0: probing pci-testdev (PCI ID REDHAT, 0x5)
rust_pci_probe 0000:00:03.0: config space  vendor=0x1b36 device=0x0005 rev=0x00
rust_pci_probe 0000:00:03.0: BAR0 base = 0xfebd5000
rust_pci_probe 0000:00:03.0: MMIO ok, testdev data-match count = 1 -> driver bound

The Rust driver matched the device, enabled it, mapped BAR0 (a Devres mapping released automatically on unbind), read vendor/device/revision/BAR from PCI config space, and performed real MMIO — every access memory-safe and bounds-checked. Log: artifacts/rust/boot-pci-crc16.log.


2. rust_crc16 — rewriting C in Rust, then proving it equivalent

This is the most direct answer to "take C code and rewrite it in Rust." The kernel's lib/crc/crc16.c is:

u16 crc16(u16 crc, const u8 *p, size_t len)
{
    while (len--)
        crc = (crc >> 8) ^ crc16_table[(crc & 0xff) ^ *p++];
    return crc;
}

The bug-prone shape here is the const u8 *p + separate size_t len: every caller must keep them in sync, and a wrong len reads off the end of the buffer — a textbook out-of-bounds read.

The Rust port takes a slice instead, so the length travels with the data and the iterator physically cannot run past the end:

fn crc16_rust(seed: u16, data: &[u8]) -> u16 {
    let mut crc = seed;
    for &byte in data {                 // no pointer, no length arg, no OOB
        crc ^= byte as u16;
        for _ in 0..8 {
            crc = if crc & 1 != 0 { (crc >> 1) ^ 0xA001 } else { crc >> 1 };
        }
    }
    crc
}

Not just "trust me" — it's checked against the original

The module declares the kernel's exported C crc16 via FFI and, at init, runs both implementations over several test vectors (empty, "A", "123456789", a sentence, raw bytes) and compares:

extern "C" { fn crc16(crc: u16, p: *const u8, len: usize) -> u16; }
...
let rust = crc16_rust(0, v);
let c = unsafe { crc16(0, v.as_ptr(), v.len()) };   // the real kernel C routine
assert_eq_logged(rust, c);

So the boot log is a live, in-kernel proof that the safe Rust rewrite is bit-identical to the C it replaces — the methodology you'd use to port any self-contained C routine to Rust with confidence.

Result — built-in, runs at boot (QEMU rc=0)

rust_crc16: verifying the Rust port against the C crc16()
rust_crc16: len= 0  rust=0x0000  C=0x0000  match
rust_crc16: len= 1  rust=0x30c0  C=0x30c0  match
rust_crc16: len= 9  rust=0xbb3d  C=0xbb3d  match   <-- 0xBB3D is THE canonical
rust_crc16: len=43  rust=0xfcdf  C=0xfcdf  match       CRC-16/ARC check value
rust_crc16: len= 9  rust=0x3343  C=0x3343  match       for "123456789"
rust_crc16: overall PASS — the safe Rust port is bit-identical to the C original

Every vector matches the kernel's own exported C crc16(), and the "123456789" vector produces 0xBB3D — the textbook CRC-16/ARC check constant — confirming the port is not just self-consistent but a correct implementation of the real algorithm. Log: artifacts/rust/boot-pci-crc16.log.


3. rust_mathport — the same scheme, a second time (int_sqrt + gcd)

To show the C→Rust methodology generalizes beyond checksums, rust_mathport ports two routines from lib/math/ — the bit-by-bit integer square root int_sqrt() and the gcd() — into safe Rust, and verifies each against the kernel's exported C function via FFI at init:

extern "C" { fn int_sqrt(x: usize) -> usize; fn gcd(a: usize, b: usize) -> usize; }

Result — built-in, runs at boot (QEMU rc=0)

rust_mathport: int_sqrt(4) rust=2 C=2 match
rust_mathport: int_sqrt(1000000) rust=1000 C=1000 match
rust_mathport: int_sqrt(3735928559) rust=61122 C=61122 match
rust_mathport: gcd(1071, 462) rust=21 C=21 match
rust_mathport: gcd(123456, 789012) rust=12 C=12 match
rust_mathport: overall PASS — both safe Rust ports are bit-identical to C

All ten int_sqrt and six gcd cases match the kernel's own C. Same recipe — pure function + C oracle + in-kernel equality check — different domain (integer math). Log: artifacts/rust/boot-mathport.log.

Why this is the right shape for "replace C with Rust"

Both components are leaf-level and self-contained: a driver for one device, a pure function with a reference oracle. That is exactly where rewriting in Rust pays off — the safety guarantees are real, the blast radius of any mistake is tiny, and correctness is verifiable (a probe that binds, a CRC that matches). Scale this pattern up — one leaf at a time, each verified — and you get the upstream Rust-for-Linux strategy, not a risky big-bang rewrite. See 04-rust-in-kernel.md for the broader argument.