Skip to content
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

Allow drivers to implement ioctls. #126

Merged
merged 5 commits into from
Mar 19, 2021
Merged
Changes from all 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
179 changes: 177 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,34 @@ 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);
// SAFETY: This function is called by the kernel, so it must set `fs` appropriately.
let mut cmd = IoctlCommand::new(cmd as _, arg as _);
let ret = T::ioctl(f, &File::from_ptr(file), &mut cmd)?;
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);
// SAFETY: This function is called by the kernel, so it must set `fs` appropriately.
let mut cmd = IoctlCommand::new(cmd as _, arg as _);
let ret = T::compat_ioctl(f, &File::from_ptr(file), &mut cmd)?;
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 +222,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 +254,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 +274,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 +290,8 @@ pub const USE_NONE: ToUse = ToUse {
read: false,
write: false,
seek: false,
ioctl: false,
compat_ioctl: false,
fsync: false,
};

Expand All @@ -249,6 +310,106 @@ 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 {
cmd: u32,
arg: usize,
user_slice: Option<UserSlicePtr>,
}

impl IoctlCommand {
/// Constructs a new [`IoctlCommand`].
///
/// # Safety
///
/// The caller must ensure that `fs` is compatible with `arg` and the original caller's
/// context. For example, if the original caller is from userland (e.g., through the ioctl
/// syscall), then `arg` is untrusted and `fs` should therefore be `USER_DS`.
unsafe fn new(cmd: u32, arg: usize) -> Self {
let user_slice = {
let dir = (cmd >> bindings::_IOC_DIRSHIFT) & bindings::_IOC_DIRMASK;
if dir == bindings::_IOC_NONE {
None
} else {
let size = (cmd >> 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.
UserSlicePtr::new(arg as _, size as _).ok()
}
};

Self {
cmd,
arg,
user_slice,
}
}

/// 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`].
pub fn dispatch<T: IoctlHandler>(&mut self, handler: &T, file: &File) -> KernelResult<i32> {
let dir = (self.cmd >> bindings::_IOC_DIRSHIFT) & bindings::_IOC_DIRMASK;
if dir == bindings::_IOC_NONE {
return T::pure(handler, file, self.cmd, self.arg);
}

let data = self.user_slice.take().ok_or(Error::EFAULT)?;
const READ_WRITE: u32 = bindings::_IOC_READ | bindings::_IOC_WRITE;
match dir {
bindings::_IOC_WRITE => T::write(handler, file, self.cmd, &mut data.reader()),
bindings::_IOC_READ => T::read(handler, file, self.cmd, &mut data.writer()),
READ_WRITE => T::read_write(handler, file, self.cmd, data),
_ => Err(Error::EINVAL),
}
}

/// Returns the raw 32-bit value of the command and the ptr-sized argument.
pub fn raw(&self) -> (u32, usize) {
(self.cmd, self.arg)
}
}

/// 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 +457,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: &mut IoctlCommand) -> 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: &mut IoctlCommand) -> KernelResult<i32> {
Err(Error::EINVAL)
}

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