diff --git a/rust/kernel/file_operations.rs b/rust/kernel/file_operations.rs
index 712a42fca4e5ef..094cdd0fa12728 100644
--- a/rust/kernel/file_operations.rs
+++ b/rust/kernel/file_operations.rs
@@ -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`].
@@ -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,
@@ -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,
@@ -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,
     };
 }
@@ -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,
 }
@@ -231,6 +290,8 @@ pub const USE_NONE: ToUse = ToUse {
     read: false,
     write: false,
     seek: false,
+    ioctl: false,
+    compat_ioctl: false,
     fsync: false,
 };
 
@@ -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`.
@@ -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`.