Repository navigation
Safe file-descriptor Rust APIs can lead to NULL pointer dereference #1256
Description
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:
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):
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):
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:
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.log
We have not reported this to the mailing list yet because we are unsure where the preferred fix should live.
Activity
nbdd0121 commentedon Sep 18, 2026
I think we can fix this either on Rust/C side by checking
current->filesfor NULL. It cannot be concurrently modified so there is no TOCTOU problems there.Error code wise --
LocalFile::fgetshould just get EBADF, and the reservation should just returnEMFILE(imaging a exiting process to have no descriptor open and have an FD rlimit of 0).Note that the mailing list can be used for bug report too, you do not need a fix to post there.
GeorgeAndrou commentedon Sep 20, 2026
@nbdd0121 thanks for the feedback. We are preparing a defensive patch for the two originally reported paths, and while working on it we found two additional ways the same underlying issue could manifest. We plan to send the patch to the mailing list soon.
A
FileDescriptorReservationmay be created whilecurrent->filesis valid and remain alive on the same task until afterexit_files().Using task work queued on
current, following the sameKBox/callback_headownership pattern as Binder'sDeferredFdCloser, we reproduced:exit_files(), which reachesput_unused_fd()withcurrent->files == NULLand crashes.FileDescriptorReservation::fd_install()afterexit_files(), which reachesfd_install()withcurrent->files == NULLand crashes.The reservation remains on the same task throughout, so the existing
NotThreadSaferestriction is respected. However, remaining on the same task does not guarantee that task-local state such ascurrent->filesis still available when the reservation is later used or dropped.A reproducer for the
fd_install()case is below.Rust driver
repro.c
KASAN crash