Skip to content

Safe file-descriptor Rust APIs can lead to NULL pointer dereference #1256

Description

@GeorgeAndrou

Hello,

We found the following soundness issue:

Safe Rust file-descriptor APIs assume that current->files exists, but that execution-context invariant is not represented or enforced by the Rust API, even though safe callbacks such as MiscDevice::release() can run after current->files has been cleared.

Confirmed on current upstream master: 5dd1818b15d98d4a20806cd00b1b40320b06004f (v7.3-rc3-313-g5dd1818b15d9)

The attached KASAN logs were collected on the current upstream commit above.

The Problem

MiscDevice::release() is exposed as a safe Rust callback:

https://github.com/torvalds/linux/blob/5dd1818b15d98d4a20806cd00b1b40320b06004f/rust/kernel/miscdevice.rs#L135-L138

During process teardown, however, MiscDevice::release() may run after exit_files() has already set current->files to NULL.

The relevant ordering is:

do_exit()
    -> exit_files()
         -> current->files = NULL
    -> exit_task_work()
         -> task_work_run()
              -> __fput()
                   -> file->f_op->release()
                        -> MiscDevice::release()

This becomes a problem for safe Rust APIs that internally operate on the current task's file-descriptor table.

LocalFile::fget() is a public safe Rust function and calls bindings::fget(fd):

https://github.com/torvalds/linux/blob/5dd1818b15d98d4a20806cd00b1b40320b06004f/rust/kernel/fs/file.rs#L262-L271

The C fget() path eventually uses current->files without accounting for it being NULL.

When called from MiscDevice::release() during this teardown path, KASAN reports a crash in __fget_files():

RIP: 0010:__fget_files (fs/file.c:1022 fs/file.c:1099)

Call Trace:
 <TASK>
 <kernel::miscdevice::MiscdeviceVTable<rust_misc_device::ReproDevice>>::release
     (samples/rust/rust_misc_device.rs:56 rust/kernel/miscdevice.rs:265)
 __fput (fs/file_table.c:512)
 task_work_run (kernel/task_work.c:233)
 do_exit (kernel/exit.c:1011)
 do_group_exit (kernel/exit.c:1154)
 __x64_sys_exit_group (kernel/exit.c:1165)

FileDescriptorReservation::get_unused_fd_flags() is also a public safe Rust kernel crate function and calls bindings::get_unused_fd_flags(flags):

https://github.com/torvalds/linux/blob/5dd1818b15d98d4a20806cd00b1b40320b06004f/rust/kernel/fs/file.rs#L402-L414

This reaches alloc_fd(), which obtains current->files and then accesses files->file_lock. When invoked from the same MiscDevice::release() context, the file table is already NULL.

KASAN reports:

BUG: KASAN: null-ptr-deref in _raw_spin_lock
Write of size 4 at addr 0000000000000080
...
alloc_fd (include/linux/spinlock.h:347 fs/file.c:576)
<kernel::miscdevice::MiscdeviceVTable<rust_misc_device::ReproDevice>>::release
    (samples/rust/rust_misc_device.rs:56 rust/kernel/miscdevice.rs:265)
__fput (fs/file_table.c:512)
task_work_run (kernel/task_work.c:233)
do_exit (kernel/exit.c:1011)

Notably, FileDescriptorReservation already prevents the value from being moved to another task in order to ensure that current does not change between get_unused_fd_flags(), fd_install(), and put_unused_fd().

In this case, however, current does not change. The same task continues executing, but current->files has been set to NULL.

In both cases, the reproducer contains no unsafe Rust code.

Reproduction

We modified the upstream samples/rust/rust_misc_device.rs example only to create a minimal reproducer that exercises the affected safe APIs from MiscDevice::release().

The following reproducer contains two independent triggering paths:

// SPDX-License-Identifier: GPL-2.0

//! Minimal Rust misc-device reproducer.

use kernel::{
    fs::{
        file::{FileDescriptorReservation, LocalFile},
        File,
    },
    miscdevice::{
        MiscDevice,
        MiscDeviceOptions,
        MiscDeviceRegistration,
    },
    prelude::*,
};

module! {
    type: ReproModule,
    name: "rust_misc_device",
    authors: ["User0"],
    description: "Rust file descriptor API reproducer",
    license: "GPL",
}

#[pin_data]
struct ReproModule {
    #[pin]
    _miscdev: MiscDeviceRegistration<ReproDevice>,
}

impl kernel::InPlaceModule for ReproModule {
    fn init(_module: &'static ThisModule) -> impl PinInit<Self, Error> {
        try_pin_init!(Self {
            _miscdev <- MiscDeviceRegistration::register(
                MiscDeviceOptions {
                    name: c"rust-fd-repro",
                },
            ),
        })
    }
}

struct ReproDevice;

#[vtable]
impl MiscDevice for ReproDevice {
    type Ptr = ();

    fn open(
        _file: &File,
        _misc: &MiscDeviceRegistration<Self>,
    ) -> Result<Self::Ptr> {
        Ok(())
    }

    fn release(_device: Self::Ptr, _file: &File) {
        let _ = LocalFile::fget(0); // 1st path
        // let _ = FileDescriptorReservation::get_unused_fd_flags(0); // 2nd path
    }
}

The LocalFile::fget() path is:

let _ = LocalFile::fget(0);

The second path is:

let _ = FileDescriptorReservation::get_unused_fd_flags(0);

Only one path needs to be enabled at a time.

The same userspace program triggers both by opening the misc device and exiting without explicitly closing it:

#include <fcntl.h>
#include <stdlib.h>

int main(void)
{
    open("/dev/rust-fd-repro", O_RDONLY);
    exit(0);
}

KASAN logs:

We have not reported this to the mailing list yet because we are unsure where the preferred fix should live.

Cc @ojeda @Darksonn @BennoLossin @brauner

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions