Earlier this year, we used the C2Rust framework to translate applications such as Quake 3 to Rust. In this post, we’ll show you that it is also possible to translate privileged software such as modules that are loaded by the Linux kenel. We’ll use a small, 3-file kernel module which is part of the Bareflank Hypervisor SDK developed by Assured Information Security but you can use the same techniques to translate other kernel modules. The basic steps we’ll cover are:

  • translating the C files into Rust,
  • compiling generated Rust, and
  • linking everything into a loadable kernel module.

The Good

We transpiled the kernel module of Bareflank which consists of a small number of C files: common.c, entry.c and platform.c. For the first step, we needed to extract the kernel’s compilation flags and pass them to the transpiler, so we would perfectly replicate the configuration settings and macro expansions from the kernel build. We used the Bear tool to pseudo-build the original kernel module and produce a compile_commands.json file containing the compilation database:

$ cd .../bareflank/bfdriver/src/platform/linux
$ make clean
$ bear make CC=clang

The kernel build system compiles C files differently (enabling or disabling some language features) depending on the compiler used. Since the C2Rust transpiler uses clang as its front-end, we had to use clang for the pseudo-build as well. One complication we ran into were asm gotos, which are a new extension to gcc’s inline assembly syntax that Linux started using in the 5.0 release, but were added to LLVM/clang in version 9.0. When we initially performed this experiment (April 2019), that version of clang hadn’t come out so we were forced to revert to an older 4.x kernel that did not use asm gotos just to get clang to parse the C code. Asm gotos are now supported by clang, but the transpiler doesn’t support them yet. We need to first investigate whether Rust’s inline assembly (either the legacy version or the recently added redesign) even supports this feature right now.

After producing the compilation database, we transpiled the C files into Rust:

$ # Remove compiler flags we can't handle
$ sed -i -e 's/"-Werror.*",//' ./compile_commands.json
$ c2rust transpile ./compile_commands.json --emit-no-std \
  --emit-modules -o bareflank-rs "[email protected]"

After making some fixes and additions to C2Rust and fix the subsequent compilation errors, we compiled the resulting Rust code with:

# cargo build --release --target x86_64-linux-kernel -Zbuild-std=core

We had originally used cargo-xbuild to accomplish this without support from the Rust compiler, but the Rust team added the kernel module target to Cargo at the end of August 2019. Once that happened, we could use cargo build directly to build our kernel module.

We had to make a few minor manual changes to the Rust files:

  • Replace use libc; with use crate::libc; since we had some problems using the libc crate from crates.io inside a kernel module. C2Rust uses the basic C type definitions from this crate, and we had to redefine them manually. This might not be a problem anymore.

  • Manually remove a couple of duplicate current_stack_pointer definitions. This variable is defined in a header in C and the transpiler emits it in every transpiled .rs file as #[no_mangle], which causes Rust linking errors. We had to manually remove all but one definitions of this symbol (the symbol wasn’t even used anywhere, so we could have removed all of them, but didn’t need to).

Once all the fixes were in, we successfully compiled the module but still had to fix a couple of issues. First, Cargo produces a static library called libbareflank_rs.a, but the kernel build system only supports object files and not static libraries. We turned the static library into an object file by calling the system linker from our kernel module Makefile:

$(M)/libbareflank_rs.o: target/x86_64-linux-kernel/release/bareflank_rs.a
        $(LD) -r -o [email protected] --whole-archive $^

The second issue was that we were initially also transpiling the bareflank.mod.c file that the kernel build system auto-generates from the input object files. We were producing a libbareflank_rs.o that already included a transpiled copy of this auto-generated file, then passing that to the kernel build system that would produce a second bareflank.mod.c and link that together with libbareflank_rs.o to produce bareflank.ko. Having two copies of the contents of bareflank.mod.c was causing crashes. Once we removed the transpiled bareflank.mod.rs file from the build, the kernel module loaded and ran successfully.

The Bad

We encountered many C features and gcc-specific extensions that the kernel headers use and we had to add to C2Rust:

  • A series of gcc attributes: inline, cold, alias, used, section and more

  • Updates to the C2Rust bitfields crate: support for no_std and booleans

  • A gcc intrinsic used by some kernel memory functions: __builtin_object_size

  • Inline assembly support in C2Rust for memory-only, read-write and early-clobber operands

  • Structures that are both packed and aligned, mainly xregs_state. This particular combination is not currently supported in Rust, so we had to implement a work-around in C2Rust by emitting a pair of nested structures (aligned outer structure, packed inner structure) instead:

#[repr(C, align(64))]
pub struct xregs_state(pub xregs_state_Inner);
#[repr(C, packed)]
pub struct xregs_state_Inner {
  // ...
}

All of these issues have been implemented in C2Rust and should work for all kernel modules, but different modules may expose new unimplemented C features.

The Ugly

C2Rust maps C types to their definitions in the libc crate, but we were unable (at the time) to use this crate in our kernel module. Instead, we redefined some of its types manually:

pub mod libc {
    pub type c_char = i8;
    pub type c_schar = i8;
    pub type c_ulong = u64;
    pub type c_uint = u32;
    // ...all the others
}

(The cty crate also provides the necessary type aliases1.)

We also had to implement a couple of libc functions that the compiler relies on:

pub unsafe fn memset(s: *mut c_void, c: c_int, n: size_t) -> *mut c_void {
    core::ptr::write_bytes(s as *mut u8, c as u8, n as usize);
    s
}

pub unsafe fn memcpy(dest: *mut c_void, src: *const c_void, n: size_t) -> *mut c_void {
    core::ptr::copy_nonoverlapping(src as *const u8, dest as *mut u8, n as usize);
    dest
}

Finally, we added stubs for a few missing kernel functions, and for the Rust panic handler (we decided to implement it properly at a later date):

#[no_mangle]
pub extern "C" fn __bad_size_call_parameter() -> ! {
    unreachable!("__bad_size_call_parameter")
}

#[no_mangle]
pub extern "C" fn __bad_percpu_size() -> ! {
    unreachable!("__bad_percpu_size")
}

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    // FIXME: call into the kernel
    loop {}
}

The main remaining “ugliness” is that the transpiler fully expands C macros, so the transpiled code is not always pleasant to look at:

   match ::core::mem::size_of::<libc::c_int>() as libc::c_ulong {
	1 => {
	    pscr_ret__ = ({
		let mut pfo_ret__: libc::c_int = 0;
		match ::core::mem::size_of::<libc::c_int>() as libc::c_ulong {
		    1 => asm!("movb %gs:$1,$0" :
					       "=q" (pfo_ret__) :
					       "*m" (&cpu_number)
					       : : "volatile"),
		    2 => asm!("movw %gs:$1,$0" :
					       "=r" (pfo_ret__) :
					       "*m" (&cpu_number)
					       : : "volatile"),
		    4 => asm!("movl %gs:$1,$0" :
					       "=r" (pfo_ret__) :
					       "*m" (&cpu_number)
					       : : "volatile"),
		    8 => asm!("movq %gs:$1,$0" :
					       "=r" (pfo_ret__) :
					       "*m" (&cpu_number)
					       : : "volatile"),
		    _ => {
			__bad_percpu_size();
		    }
		}
		pfo_ret__
	    })
	}

C2Rust uses clang as a front-end which currently expands macros very early before parsing, so modifying it to produce an AST with unexpanded macros would be a significant undertaking. We are currently considering other alternatives, but it is unclear whether any of them would be simpler. For the foreseeable future, C2Rust will not be able to transpile non-trivial function-like C macros to Rust macros.

We’d love to hear what you want to see translated next. Drop us a line at [email protected] and let us know! If you have legacy C/C++ code you need modernized, translated or integrated with Rust, we are here to help.


  1. Thanks to reddit user thelights0123 for pointing this out to us. ↩︎