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
Hello,
We found the following soundness issue:
Safe Rust file-descriptor APIs assume that
current->filesexists, but that execution-context invariant is not represented or enforced by the Rust API, even though safe callbacks such asMiscDevice::release()can run aftercurrent->fileshas 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 afterexit_files()has already setcurrent->filestoNULL.The relevant ordering is:
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 callsbindings::fget(fd):https://github.com/torvalds/linux/blob/5dd1818b15d98d4a20806cd00b1b40320b06004f/rust/kernel/fs/file.rs#L262-L271
The C
fget()path eventually usescurrent->fileswithout accounting for it beingNULL.When called from
MiscDevice::release()during this teardown path, KASAN reports a crash in__fget_files():FileDescriptorReservation::get_unused_fd_flags()is also a public safe Rustkernelcrate function and callsbindings::get_unused_fd_flags(flags):https://github.com/torvalds/linux/blob/5dd1818b15d98d4a20806cd00b1b40320b06004f/rust/kernel/fs/file.rs#L402-L414
This reaches
alloc_fd(), which obtainscurrent->filesand then accessesfiles->file_lock. When invoked from the sameMiscDevice::release()context, the file table is alreadyNULL.KASAN reports:
Notably,
FileDescriptorReservationalready prevents the value from being moved to another task in order to ensure thatcurrentdoes not change betweenget_unused_fd_flags(),fd_install(), andput_unused_fd().In this case, however,
currentdoes not change. The same task continues executing, butcurrent->fileshas been set toNULL.In both cases, the reproducer contains no
unsafeRust code.Reproduction
We modified the upstream
samples/rust/rust_misc_device.rsexample only to create a minimal reproducer that exercises the affected safe APIs fromMiscDevice::release().The following reproducer contains two independent triggering paths:
The
LocalFile::fget()path is:The second path is:
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:
KASAN logs:
LocalFile::fget(): https://github.com/user-attachments/files/32364471/fget-upstream-decoded.logFileDescriptorReservation::get_unused_fd_flags(): https://github.com/user-attachments/files/32364472/get-unused-fd-upstream-decoded.logWe have not reported this to the mailing list yet because we are unsure where the preferred fix should live.
Cc @ojeda @Darksonn @BennoLossin @brauner