Skip to content

Allow drivers to implement ioctls. #126

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 19, 2021
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 155 additions & 2 deletions rust/kernel/file_operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,36 @@ use crate::error::{Error, KernelResult};
use crate::user_ptr::{UserSlicePtr, UserSlicePtrReader, UserSlicePtrWriter};

/// Wraps the kernel's `struct file`.
///
/// # Invariants
///
/// The pointer [`File::ptr`] is non-null and valid.
pub struct File {
ptr: *const bindings::file,
}

impl File {
/// Constructs a new [`struct file`] wrapper.
///
/// # Safety
///
/// The pointer `ptr` must be non-null and valid for the lifetime of the object.
unsafe fn from_ptr(ptr: *const bindings::file) -> File {
// INVARIANTS: the safety contract ensures the type invariant will hold.
File { ptr }
}

/// Returns the current seek/cursor/pointer position (`struct file::f_pos`).
pub fn pos(&self) -> u64 {
// SAFETY: `File::ptr` is guaranteed to be valid by the type invariants.
unsafe { (*self.ptr).f_pos as u64 }
}

/// Returns whether the file is in blocking mode.
pub fn is_blocking(&self) -> bool {
// SAFETY: `File::ptr` is guaranteed to be valid by the type invariants.
unsafe { (*self.ptr).f_flags & bindings::O_NONBLOCK == 0 }
}
}

/// Equivalent to [`std::io::SeekFrom`].
Expand Down Expand Up @@ -138,6 +155,30 @@ unsafe extern "C" fn llseek_callback<T: FileOperations>(
}
}

unsafe extern "C" fn unlocked_ioctl_callback<T: FileOperations>(
file: *mut bindings::file,
cmd: c_types::c_uint,
arg: c_types::c_ulong,
) -> c_types::c_long {
from_kernel_result! {
let f = &*((*file).private_data as *const T);
let ret = T::ioctl(f, &File::from_ptr(file), IoctlCommand(cmd as _), arg as _)?;
Ok(ret as _)
}
}

unsafe extern "C" fn compat_ioctl_callback<T: FileOperations>(
file: *mut bindings::file,
cmd: c_types::c_uint,
arg: c_types::c_ulong,
) -> c_types::c_long {
from_kernel_result! {
let f = &*((*file).private_data as *const T);
let ret = T::compat_ioctl(f, &File::from_ptr(file), IoctlCommand(cmd as _), arg as _)?;
Ok(ret as _)
}
}

unsafe extern "C" fn fsync_callback<T: FileOperations>(
file: *mut bindings::file,
start: bindings::loff_t,
Expand Down Expand Up @@ -177,7 +218,11 @@ impl<T: FileOperations> FileOperationsVtable<T> {
},

check_flags: None,
compat_ioctl: None,
compat_ioctl: if T::TO_USE.compat_ioctl {
Some(compat_ioctl_callback::<T>)
} else {
None
},
copy_file_range: None,
fallocate: None,
fadvise: None,
Expand Down Expand Up @@ -205,7 +250,11 @@ impl<T: FileOperations> FileOperationsVtable<T> {
show_fdinfo: None,
splice_read: None,
splice_write: None,
unlocked_ioctl: None,
unlocked_ioctl: if T::TO_USE.ioctl {
Some(unlocked_ioctl_callback::<T>)
} else {
None
},
write_iter: None,
};
}
Expand All @@ -221,6 +270,12 @@ pub struct ToUse {
/// The `llseek` field of [`struct file_operations`].
pub seek: bool,

/// The `unlocked_ioctl` field of [`struct file_operations`].
pub ioctl: bool,

/// The `compat_ioctl` field of [`struct file_operations`].
pub compat_ioctl: bool,

/// The `fsync` field of [`struct file_operations`].
pub fsync: bool,
}
Expand All @@ -231,6 +286,8 @@ pub const USE_NONE: ToUse = ToUse {
read: false,
write: false,
seek: false,
ioctl: false,
compat_ioctl: false,
fsync: false,
};

Expand All @@ -249,6 +306,88 @@ macro_rules! declare_file_operations {
};
}

/// Allows the handling of ioctls defined with the `_IO`, `_IOR`, `_IOW`, and `_IOWR` macros.
///
/// For each macro, there is a handler function that takes the appropriate types as arguments.
pub trait IoctlHandler: Sync {
/// Handles ioctls defined with the `_IO` macro, that is, with no buffer as argument.
fn pure(&self, _file: &File, _cmd: u32, _arg: usize) -> KernelResult<i32> {
Err(Error::EINVAL)
}

/// Handles ioctls defined with the `_IOR` macro, that is, with an output buffer provided as
/// argument.
fn read(&self, _file: &File, _cmd: u32, _writer: &mut UserSlicePtrWriter) -> KernelResult<i32> {
Err(Error::EINVAL)
}

/// Handles ioctls defined with the `_IOW` macro, that is, with an input buffer provided as
/// argument.
fn write(
&self,
_file: &File,
_cmd: u32,
_reader: &mut UserSlicePtrReader,
) -> KernelResult<i32> {
Err(Error::EINVAL)
}

/// Handles ioctls defined with the `_IOWR` macro, that is, with a buffer for both input and
/// output provided as argument.
fn read_write(&self, _file: &File, _cmd: u32, _data: UserSlicePtr) -> KernelResult<i32> {
Err(Error::EINVAL)
}
}

/// Represents an ioctl command.
///
/// It can use the components of an ioctl command to dispatch ioctls using
/// [`IoctlCommand::dispatch`].
pub struct IoctlCommand(u32);

impl IoctlCommand {
/// Dispatches the given ioctl to the appropriate handler based on the value of the command. It
/// also creates a [`UserSlicePtr`], [`UserSlicePtrReader`], or [`UserSlicePtrWriter`]
/// depending on the direction of the buffer of the command.
///
/// It is meant to be used in implementations of [`FileOperations::ioctl`] and
/// [`FileOperations::compat_ioctl`].
///
/// # Safety
///
/// The caller must ensure that `fs` is set to a value compatible with the pointer in `arg`.
/// This is because this function uses [`UserSlicePtr`], which calls `access_ok`.
pub unsafe fn dispatch<T: IoctlHandler>(
&self,
handler: &T,
file: &File,
arg: usize,
) -> KernelResult<i32> {
let dir = (self.0 >> bindings::_IOC_DIRSHIFT) & bindings::_IOC_DIRMASK;
if dir == bindings::_IOC_NONE {
return T::pure(handler, file, self.0, arg);
}

let size = (self.0 >> bindings::_IOC_SIZESHIFT) & bindings::_IOC_SIZEMASK;

// SAFETY: We only create one instance of the user slice, so TOCTOU issues are not possible.
// The `set_fs` requirements are imposed on the caller.
let data = UserSlicePtr::new(arg as _, size as _)?;
const READ_WRITE: u32 = bindings::_IOC_READ | bindings::_IOC_WRITE;
match dir {
bindings::_IOC_WRITE => T::write(handler, file, self.0, &mut data.reader()),
bindings::_IOC_READ => T::read(handler, file, self.0, &mut data.writer()),
READ_WRITE => T::read_write(handler, file, self.0, data),
_ => Err(Error::EINVAL),
}
}

/// Returns the raw 32-bit value of the command.
pub fn raw(&self) -> u32 {
self.0
}
}

/// Corresponds to the kernel's `struct file_operations`.
///
/// You implement this trait whenever you would create a `struct file_operations`.
Expand Down Expand Up @@ -296,6 +435,20 @@ pub trait FileOperations: Sync + Sized {
Err(Error::EINVAL)
}

/// Performs IO control operations that are specific to the file.
///
/// Corresponds to the `unlocked_ioctl` function pointer in `struct file_operations`.
fn ioctl(&self, _file: &File, _cmd: IoctlCommand, _arg: usize) -> KernelResult<i32> {
Err(Error::EINVAL)
}

/// Performs 32-bit IO control operations on that are specific to the file on 64-bit kernels.
///
/// Corresponds to the `compat_ioctl` function pointer in `struct file_operations`.
fn compat_ioctl(&self, _file: &File, _cmd: IoctlCommand, _arg: usize) -> KernelResult<i32> {
Err(Error::EINVAL)
}

/// Syncs pending changes to this file.
///
/// Corresponds to the `fsync` function pointer in `struct file_operations`.
Expand Down