Skip to navigation Skip to main content Skip to footer

Rustproofing Linux (Part 1/4 Leaking Addresses)

06 February 2023

By Domen Puncer Kugler

Rust is a programming language guaranteeing memory and thread safety while still being able to access raw memory and hardware. This sounds impossible, and it is, that’s why Rust has an unsafe keyword which allows a programmer to dereference a raw pointer and perform some other dangerous operations. The dangerous code is effectively contained to unsafe blocks, which makes Rust an interesting option for embedded and system programming, where it could potentially be used to replace C, which has a long history of memory safety vulnerabilities.

The Linux Kernel, like many other operating system kernels, has a long history of memory and thread safety vulnerabilities. The Rust for Linux project aims to add Rust language support to the Linux Kernel and try to improve the security situation. Serious efforts to bring Rust into the mainline started in April 2021 and after many patch iterations, minimal Rust support has been merged into Linux 6.1.

With rudimentary Rust support now existing in the Linux Kernel, we expect some developers will try writing new device drivers in Rust, and some will port existing ones (e.g., Google has already developed a prototype of the Binder driver). This blog series explores security aspects of porting a Linux device driver from C to Rust. We have created five vulnerable drivers in C, ported them to Rust, explored a few porting variants that make use of different Rust APIs, and discussed how plausible it is for vulnerabilities to persist across the porting process.

This exploration was done from the perspective of a C programmer who is a beginner in Rust. My Rust code might not be idiomatic Rust, and could probably be described as “path of least resistance” Rust, which might not be too far off from what the other developers will attempt.

The blog series is made of four parts:

Intentionally Leaking Pointer Addresses

Let’s get started with a very simple device driver. It only implements one ioctl command, VULN_PRINT_ADDR, which prints the address of the handling function itself and an address of a stack variable.

While printing a kernel address seems rather innocent – it’s just a number after all – these values are useful sources of information leakage that assists an attacker in bypassing KASLR when developing an exploit for a memory safety vulnerability. The Linux kernel has aimed to reduce such infoleaks for quite some time as per KSPP Kernel pointer leak page. Eventually the "%p" format string in the Linux Kernel was changed to print a hashed pointer value instead of leaking sensitive memory layout information.

The "%p" printing restriction was bypassed in our driver with a printk specific format string "%px" that can be used when you really want to print the address.

When porting this code to Rust, one can see some boilerplate changes. Rust’s module_misc_device! macro neatly merged some miscdevice/module boilerplate, and struct file_operations is now a set of methods that are implemented on the driver struct. The problematic lines themselves have few changes.

The original C version is shown below:

pr_info("%s is at address %pxn", __func__,  __func__);
pr_info("stack is at address %pxn",  stack_dummy);
Original C version

And the Rust version looks quite similar:

pr_info!("RustVuln::ioctl is at address {:p}n", Self::ioctl as *const ());
pr_info!("stack is at address {:p}n",  stack_dummy);
Rust version

In Rust, format strings are different, but that is easy to figure out.

Surprisingly, despite the vast efforts to eliminate pointer infoleaks in the Linux kernel, the Rust frameworks seemingly take a step backwards on this front. In Rust, one can actually just use "{:p}" as an equivalent to "%p" to print pointer values. To be crystal clear: The Rust pr_info! macro does not hash the pointer value like the C pr_info function does. We have reported this to the Rust for Linux maintainers.

It might be surprising to some that the raw addresses are easily accessible. Sure there’s a somewhat odd looking Self::ioctl as *const () (basically a cast to a void pointer), but no unsafe or anything like that.

Developers who feel the need to print pointer values in C, will probably continue to do so after porting their drivers to Rust.

Leaking Stack Contents

First, some backstory: The C standards do not require that memory covered by the struct but not belonging to any of its members is initialised to any value. That is, padding between structure members is not guaranteed to be initialised. This can be a bit surprising, since one does not expect uninitialised data in an initialised data container.

When such structures are copied across kernel trust boundaries (for example, as syscall outputs) small portions of kernel memory may be revealed to user space, constituting an info leak. This problem has apparently happened often enough that the Linux kernel now has an automatic stack initialisation feature. To fix these types of problems, enable CONFIG_INIT_STACK_ALL_PATTERN or CONFIG_INIT_STACK_ALL_ZERO (there’s also the init_on_alloc boot parameter that zero initialises SLAB memory).

We have created an example driver to demonstrate how stack contents can be leaked accidentally. Note: for this demonstration to work, the kernel needs to be configured with CONFIG_INIT_STACK_NONE=y, which disables automatic stack variable initialisation. The interesting part follows:

struct vuln_info {
        u8 version;
        u64 id;
        u8 _reserved;
};

static long vuln_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
        struct vuln_info info;

        switch (cmd) {
        case VULN_GET_INFO:
                info = (struct vuln_info) {
                        .version = 1,
                        .id = 0x1122334455667788,
                };
                if (copy_to_user((void __user *)arg,  info, sizeof(info)) != 0)
ioctl leaking stack memory contents

As described, the entire struct is not initialised. While the _reserved member is implicitly initialised by the compiler, the padding bytes are not.

In memory, this data structure will be represented as:

Offset Byte 7 6 5 4 3 2 1 0
0 Padding 01
8 11 22 33 44 55 66 77 88
16 Padding 00
Memory layout of struct vuln_info

We can test the transferred padding values with a simple PoC, whose output is shown below. We see padding bytes at offset 0 and 16 are leaked, and the 0xffff... looks very much like an address from the kernel address space.

root@(none):/# /xxx/rustproofing-linux/poc/test.sh vuln_stack_leak
[   24.367962] vuln_stack_leak: loading out-of-tree module taints kernel.
value at offset 0 differs: 0xbe4b0a10dd2f9a01 vs 0x1
value at offset 16 differs: 0xffffffffb2fcd300 vs 0
PoC demonstrating data has been leaked

Porting to Rust

Accessing Userspace Memory

The Linux kernel uses copy_from_user to copy data from userspace, and copy_to_user to copy kernel buffers to userspace. However, in Rust for Linux, these same copy operations can be achieved with UserSlicePtrReader and UserSlicePtrWriter. In this section, we will only describe the “writer”, since both the reader and writer follow the same design patterns, only the direction is reversed.

UserSlicePtrWriter consists of a pointer and a length, and any write operation will increment the pointer while decrementing the length until the whole buffer is consumed. Note how this is a nice way of eliminating TOCTOU style bugs – that is, the writer only allows the data to be written once. It has an unsafe write_raw operation which is a copy_to_user wrapper with the mentioned pointer and length change. Here, write_raw is unsafe because the programmer needs to guarantee the destination kernel buffer pointer is safe to be dereferenced and “length” number of bytes can be read from it.

It also “inherits” the following convenient and safe IoBufferWriter methods:

  • write_slice – Write slice into a userspace buffer.
  • write – Write a WritableFromBytes type into a userspace buffer. It’s worth pointing out that WritableFromBytes is defined for the integer types, but one could define it for any type, and then be able to use write on that type, like we do below.

The UserSlicePtrReader has all the above semantics (just reverse read and write) with the added method:

  • read_all – read the whole userspace buffer into a newly allocated Vec

Rust IOCTL handling uses an UserSlicePtrReader and/or UserSlicePtrWriter, depending on IOCTL direction.

Initial Rust Version

The Rust version has a bit more involved IOCTL handling compared to the above C version, but it should still be easy to follow. The relevant bits from above translate to:

#[repr(C)] // same struct layout as in C, since we are sending it to userspace
struct VulnInfo {
    version: u8,
    id: u64,
    _reserved: u8,
}

        match cmd {
            VULN_GET_INFO => {
                let info = VulnInfo {
                    version: 1,
                    id: 0x1122334455667788,
                    _reserved: 0, // compiler requires an initialiser
                };

                // pointer weakening coercion + cast
                let info_ptr =  info as *const _ as *const u8;
                // SAFETY: "info" is declared above and is local
                unsafe { writer.write_raw(info_ptr, size_of_val( info))? };
Rust implementation that leaks stack memory contents

The main differences between C and Rust are:

  • #[repr(C)] is required, because the Rust compiler is otherwise free to reorder struct members.
  • The _reserved member needs to be explicitly initialised, whereas in C unspecified members are implicitly initialised.
  • There’s an unsafe block required for write_raw, which is just a fancy copy_to_user wrapper.

When we try the PoC, we observe that the kernel stack contents were leaked once again:

root@(none):/# /xxx/rustproofing-linux/poc/test.sh rust_vuln_stack_leak
value at offset 0 differs: 0xffffffffb2fcd501 vs 0x1
value at offset 16 differs: 0xffff888003d77f00 vs 0
PoC demonstrating data has been leaked

In a way, this is somewhat surprising, but maybe it shouldn’t be. After all, the Rust version looks a lot like the C version, and there’s even an unsafe block which should make the developer or auditor think twice that there might be something unsafe about this code.

WritableToBytes Variant

The unsafe write_raw that was used above takes a raw pointer, but there’s actually a nicer API we can leverage here. A safe write could be used, if we implement the unsafe trait WritableToBytes. The new Rust variant then becomes:

#[repr(C)] // same struct layout as in C, since we are sending it to userspace
struct VulnInfo {
    version: u8,
    id: u64,
    _reserved: u8,
}
unsafe impl WritableToBytes for VulnInfo { }
[...]
                let info = VulnInfo {
                    version: 1,
                    id: 0x1122334455667788,
                    _reserved: 0, // compiler requires an initialiser
                };

                writer.write( info)?;
Nicer Rust variant with WritableToBytes

There is the following informational comment above WritableToBytes, which explains exactly what is wrong with our code. We feel somewhat uneasy about this comment. The correct usage of this API requires that developers notice the comment:

/// A type must not include padding bytes and must be fully initialised to safely implement
/// [`WritableToBytes`] (i.e., it doesn't contain [`MaybeUninit`] fields). A composition of
/// writable types in a structure is not necessarily writable because it may result in padding
/// bytes.
Very relevant warning

Bonus Point for no unsafe

While playing around with this we have discovered we can actually do this without any unsafe in our code:

// pr_info! can be used to hide unsafe
pr_info!("writing data to userspace {:?}n", writer.write_raw(info_ptr, size_of_val( info))?);
Trick to hide unsafe

And the PoC works:

root@(none):/# /xxx/rustproofing-linux/poc/test.sh rust_vuln_stack_leak_v3
[  178.302849] vuln_stack_leak: writing data to userspace ()
value at offset 0 differs: 0xffff88800420ff01 vs 0x1
value at offset 16 differs: 0xffffff00 vs 0
PoC demonstrating data has been leaked

But what is happening here? pr_info! has an internal unsafe in its implementation, so the arguments are interpreted as unsafe. This was rather surprising to us, so we reported it to the Rust for Linux maintainers. We have later discovered they track this issue already, and our report served as a reminder. A fix is due to be merged with the next release.

Fixing Struct Padding Leaks

The idiomatic way in a C-based Linux kernel is to explicitly zero the structure’s memory before initialising it:

memset( info, 0, sizeof(info)); // explicitly clear all "info" memory
info = (struct vuln_info) {
        .version = 1,
        .id = 0x1122334455667788,
};
Idiomatic fix is with a memset

In Rust this can be accomplished with MaybeUninit porting variation, as shown below:

let mut info = MaybeUninit::<VulnInfo>::zeroed();
let info = info.write(VulnInfo {
    version: 1,
    id: 0x1122334455667788,
    _reserved: 0, // compiler requires an initialiser
});
Fixing Rust version with MaybeUninit

Note that while MaybeUninit::::zeroed() will fix the issue, there’s still that unsafe required for WritableToBytes. To eliminate this undesirable unsafe block, one can explicitly write all individual struct members as shown in this example.

Mitigation

This is a common enough problem that automatic stack initialisation feature was merged recently. When compiling the driver, if the CONFIG_INIT_STACK_ALL_ZERO config option is enabled, the issue can no longer be reproduced in the C-based driver:

root@(none):/# /xxx/rustproofing-linux/poc/test.sh vuln_stack_leak
[   36.900538] vuln_stack_leak: loading out-of-tree module taints kernel.
root@(none):/#
PoC shows no data leaked when mitigation applied

Oddly, the problem can still be reproduced in the Rust port of the vulnerable code:

root@(none):/# /xxx/rustproofing-linux/poc/test.sh rust_vuln_stack_leak
value at offset 0 differs: 0xffffffffaa7d8b01 vs 0x1
value at offset 16 differs: 0xffff8880047d7f00 vs 0
root@(none):/#
PoC shows data leaked in Rust port even with mitigation

In other words, the CONFIG_INIT_STACK_ALL_ZERO build option does nothing for Rust code! Developers must be cautious to avoid shooting themselves in the foot when porting a driver from C to Rust, especially if they previously relied on this config option to mitigate this class of vulnerability. It seems that kernel info leaks and KASLR bypasses might be here to stay, at least, for a little while longer.

This surprising behaviour has been reported to the maintainers of the Rust for Linux project.

There’s More…

Part 2 of this blog series talks about race conditions and the caveats one needs to pay attention to.