From nobody Wed Nov 27 07:35:02 2024 Received: from dd3514.kasserver.com (dd3514.kasserver.com [85.13.129.232]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id E35C4198E8C; Fri, 11 Oct 2024 18:57:41 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=85.13.129.232 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1728673067; cv=none; b=HZsQlPTeBcO7ePtNVLOkkuTCr34regogW6n+EgXQ4Ak2S84wNzw6w3LaZsx2p0ycD0mkCqfACXCV5KTL0Fqyp7QuET44GZ81OsTLJsUMd1o8yN1Fml98fuVRxnoxn36p4X/Lu3EbZzUcrJWsQtLDks4x93d136YiVbWMD2XOSB4= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1728673067; c=relaxed/simple; bh=TfqRLHSFe95otWFNx8bjzObPDJpgeNSffpBivWLaOpc=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=gpgc+YeO7b9+O+8RyR1nOlWoBMHS332VHgXY4SRZxpy7L7BG/AW+Fv9x0kCMVgeS2EjR7LM/qhTJN8j8cJHKhB35IhY6hZbBIO+0P0YJTFwjRBuPcvD4Q30iNcGGeCL5NNOQ6JawMWPG4uRrpw0b01O6Z3234X8Se5OKz3z3QCQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=walterzollerpiano.com; spf=pass smtp.mailfrom=walterzollerpiano.com; dkim=pass (2048-bit key) header.d=walterzollerpiano.com header.i=@walterzollerpiano.com header.b=CXDfewc8; arc=none smtp.client-ip=85.13.129.232 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=walterzollerpiano.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=walterzollerpiano.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=walterzollerpiano.com header.i=@walterzollerpiano.com header.b="CXDfewc8" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=walterzollerpiano.com; s=kas202409051210; t=1728673043; bh=W9JZxQC+yrDc0pF1vIbATZkMjifkOmjJgbi1RsLWtyE=; h=From:Date:Subject:References:In-Reply-To:To:Cc:From; b=CXDfewc8OQC3HC8TBvY0b05HFH1mZbK0bJCqxKSS4H9EHPitMDU0CIRiW11e/NpJA yMhBTY2UT1dX79htVIXqJIefHw7fYl7IQOXkCnq/R4GatfIeX3cQ9ppySQWb5nCjZW V2rZCMQrQw9L2w+UZWfYIMn4RwEOHJ5SXjLJJdlzZ/nayhxfKEkEHtf36zRKPaeJYB L4Z60bUnpruq2i+zuKPk7W0V1Q4sTnpVm11f4ylj+Nw+JVeuPdzOIwq3CAru5RBkMw r720K0W9JF54fTVpaX0spT8xNgsKfIctgyYGNXEjHQiYT91eILRs+WAAezoiaWnT7V Ua4/JTz63Z7PA== Received: from [127.0.0.1] (31-10-158-205.cgn.dynamic.upc.ch [31.10.158.205]) by dd3514.kasserver.com (Postfix) with ESMTPSA id 2E2F4102314; Fri, 11 Oct 2024 20:57:23 +0200 (CEST) From: Josef Zoller Date: Fri, 11 Oct 2024 20:55:42 +0200 Subject: [PATCH 1/3] rust: char_dev: add character device abstraction Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20241011-rust-char-dev-v1-1-350225ae228b@walterzollerpiano.com> References: <20241011-rust-char-dev-v1-0-350225ae228b@walterzollerpiano.com> In-Reply-To: <20241011-rust-char-dev-v1-0-350225ae228b@walterzollerpiano.com> To: Arnd Bergmann , Greg Kroah-Hartman , Miguel Ojeda , Alex Gaynor Cc: Boqun Feng , Gary Guo , =?utf-8?q?Bj=C3=B6rn_Roy_Baron?= , Benno Lossin , Andreas Hindborg , Alice Ryhl , Trevor Gross , linux-kernel@vger.kernel.org, rust-for-linux@vger.kernel.org, Josef Zoller X-Mailer: b4 0.14.2 X-Developer-Signature: v=1; a=openpgp-sha256; l=49160; i=josef@walterzollerpiano.com; h=from:subject:message-id; bh=TfqRLHSFe95otWFNx8bjzObPDJpgeNSffpBivWLaOpc=; b=owEBbQKS/ZANAwAIAROd718OBo3IAcsmYgBnCXURpJjWm2tt9nYCZw2LcCEtqJi9mj9AOu0z/ 57j6tCPEdiJAjMEAAEIAB0WIQQnwV8chXqnjaC1vcATne9fDgaNyAUCZwl1EQAKCRATne9fDgaN yJ1XD/4kCiEbZjUrqurho0SRGooQPJjqJfCB7fc4qRd2XJQy+rrL1o3Ww1Rs8/oacE22xPycGBf WeuWK80dztvZMDYaC6apwUmSYvVsd2Mk6iwzmM+eqg2OJfDcp1KIUe0yYGGBGXF33aNw+8etTW6 XYWGqst0ZDJtw7qJwKqTwoKdxolThYQxF6dbBYWqKza7VacQ5kFx32Gxka3kaiwACV0SSM1QSne 6J7+OmoMwoqWExw9RIZBrVKaZsNelfUC6Tr1Ph2wEEIxhCFHQ2xmzkQOazHDRis6MYGQYZoUSQQ x5VvgC6xDqCGBzLkyQPB6/mMRLI4ebHXaI6W6Lg+AIZH1zUleG68vrjlz5rdl9anZkFuPdFfpST 9uaz4wzGIfWVIJHCwwXg2EorgpUGMD+S7BTI8uI+6aVbM/sXBIEK2NWVgCoSjmbV1si1d2J5eWM 7AgAzz7S1giPb2OimaqBqV3BVU820kvKUaujuqYIK5P3W6pi3nku7QQ6V4qe8iPSfAN4fHNSBJZ 5KHYaUEvZ8vO5outZR0p9v0BcPRSZG0PsvgTOGhNbi2stRdipY4JVk5caG8e2SNFzIQn1UlgS3J ntB0ZF4BLD4vONppaUTmC0ZqQSBLSyU+KpSdEbC7T6JxUE77jM7HbDRYd75czpISiOtpfITnzMD XucziYjADFFGbfA== X-Developer-Key: i=josef@walterzollerpiano.com; a=openpgp; fpr=27C15F1C857AA78DA0B5BDC0139DEF5F0E068DC8 X-Spamd-Bar: -- Provide the `CharDevice` and `OpenCharDevice` traits that let you specify the file operations for a character device. Furthermore, provide the `IoctlCommand` trait that works together with the character device traits to provide a way to parse ioctl commands into a Rust type. For now, only these file operations can be defined for a char device: open, release, read, write, ioctl, and llseek. The abstractions do not provide a way to choose the major and minor numbers for the devices. Instead, registered devices will always be assigned a unique major number and consecutive minor numbers starting from 0 and running up to the number of devices registered. The division into `CharDevice` and `OpenCharDevice` traits is done to allow greater flexibility when implementing devices that can be opened multiple times, by holding permanent state in the type implementing `CharDevice`, and state that is only valid for a single open in the type implementing `OpenCharDevice`. For simple devices however, that do not need such complex behavior, it is easy to just implement both traits for the same type and just clone the state when opening the device. All file operations except open are optional to implement. If they're not implemented, they're not registered at all with the kernel. Most of the operations return a Result type, allowing positive values to be returned as is, while returning errors using the kernel::error::Error type. Optionally, the device traits allow specifying a custom error type that has to be convertible to kernel::error::Error. The ioctl operation is a bit special, as it first parses the command and argument into a Rust type using the `IoctlCommand` trait, and then calls the ioctl method with the parsed command. The llseek operation gets a mutable reference to the file position as an extra pos argument, which is copied from f_pos before the operation is called. This allows the operation to safely modify the file position, which is then written back to f_pos. Also, the whence argument is converted into the `Whence` enum, before being passed to the operation. This patch does not implement the more advanced file operations that character devices can implement, as the Rust abstractions for the types required by them are largely still missing. As all of the operations are optional anyway, the missing operations can be easily added later. Signed-off-by: Josef Zoller --- rust/bindings/bindings_helper.h | 1 + rust/helpers/fs.c | 16 + rust/kernel/char_dev.rs | 976 ++++++++++++++++++++++++++++++++++++= ++++ rust/kernel/init/macros.rs | 10 +- rust/kernel/ioctl.rs | 46 +- rust/kernel/lib.rs | 1 + 6 files changed, 1046 insertions(+), 4 deletions(-) diff --git a/rust/bindings/bindings_helper.h b/rust/bindings/bindings_helpe= r.h index 4a400a95497915c0fe0da4adfc5dd42a328399e0..fc26db0842212b2ae691c2af748= 134aee6fb657f 100644 --- a/rust/bindings/bindings_helper.h +++ b/rust/bindings/bindings_helper.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include diff --git a/rust/helpers/fs.c b/rust/helpers/fs.c index a75c9676337246ce532ef694e64ba9a7d7ad5842..44904f461dee01293aee9a21b56= 9fc0f50062a62 100644 --- a/rust/helpers/fs.c +++ b/rust/helpers/fs.c @@ -5,8 +5,24 @@ */ =20 #include +#include =20 struct file *rust_helper_get_file(struct file *f) { return get_file(f); } + +unsigned int rust_helper_MAJOR(dev_t dev) +{ + return MAJOR(dev); +} + +unsigned int rust_helper_MINOR(dev_t dev) +{ + return MINOR(dev); +} + +dev_t rust_helper_MKDEV(unsigned int major, unsigned int minor) +{ + return MKDEV(major, minor); +} diff --git a/rust/kernel/char_dev.rs b/rust/kernel/char_dev.rs new file mode 100644 index 0000000000000000000000000000000000000000..b81c0d55ab60f18dc82a9999131= 8a5ae0a26e560 --- /dev/null +++ b/rust/kernel/char_dev.rs @@ -0,0 +1,976 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! Character device support. +//! +//! C headers: [`include/linux/cdev.h`](srctree/include/linux/cdev.h) and +//! [`include/linux/fs.h`](srctree/include/linux/fs.h) + +use crate::{ + bindings, container_of, + error::{to_result, VTABLE_DEFAULT_ERROR}, + fs::{File, LocalFile}, + ioctl::IoctlCommand, + prelude::*, + types::{ForeignOwnable, Opaque}, + uaccess::{UserPtr, UserSlice, UserSliceReader, UserSliceWriter}, +}; +use core::{ffi, mem, ops::Deref}; + +/// Character device ID. +/// +/// This is a wrapper around the kernel's `dev_t` type and identifies a +/// character device by its major and minor numbers. +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct CharDeviceID(bindings::dev_t); // u32 + +impl CharDeviceID { + /// Creates a new device ID from the given major and minor numbers. + /// + /// This corresponds to the kernel's `MKDEV` macro. + pub fn new(major: u32, minor: u32) -> Self { + // SAFETY: The kernel's `MKDEV` macro is safe to call with any val= ues. + Self(unsafe { bindings::MKDEV(major, minor) }) + } + + /// Returns the major number of the device ID. + /// + /// This corresponds to the kernel's `MAJOR` macro. + pub fn major(&self) -> u32 { + // SAFETY: The kernel's `MAJOR` macro is safe to call with any val= ue. + unsafe { bindings::MAJOR(self.0) } + } + + /// Returns the minor number of the device ID. + /// + /// This corresponds to the kernel's `MINOR` macro. + pub fn minor(&self) -> u32 { + // SAFETY: The kernel's `MINOR` macro is safe to call with any val= ue. + unsafe { bindings::MINOR(self.0) } + } +} + +/// Seek mode for the `llseek` method. +/// +/// This enum corresponds to the `SEEK_*` constants in the kernel. +#[repr(u32)] +pub enum Whence { + /// Set the file position to `offset`. + Set =3D bindings::SEEK_SET, + /// Set the file position to the current position plus `offset`. + Cur =3D bindings::SEEK_CUR, + /// Set the file position to the end of the file plus `offset`. + End =3D bindings::SEEK_END, + /// Set the file position to the next location in the file greater tha= n or + /// equal to `offset` containing data. + Data =3D bindings::SEEK_DATA, + /// Set the file position to the next hole in the file greater than or + /// equal to `offset`. + Hole =3D bindings::SEEK_HOLE, +} + +// Make sure at compile time that the `Whence` enum can be safely converted +// from integers up to `SEEK_MAX`. +const _: () =3D assert!(Whence::Hole as u32 =3D=3D bindings::SEEK_MAX); + +/// Trait implemented by a registered character device. +/// +/// A registered character device just handles the `open` operation on the +/// device file and returns an open device type (which implements the +/// [`OpenCharDevice`] trait) that handles the actual I/O operations on the +/// device file. Optionally, the `release` operation can be implemented to +/// handle the final close of the device file, but simple cleanup can also= be +/// done in the `Drop` implementation of the open device type. +/// +/// # Example +/// +/// ``` +/// # use kernel::{ +/// # char_dev::{CharDevice, CharDeviceID, OpenCharDevice}, +/// # fs::{File, LocalFile}, +/// # prelude::*, +/// # uaccess::UserSliceWriter, +/// # }; +/// # +/// struct MyCharDev; +/// struct MyOpenCharDev; +/// +/// #[vtable] +/// impl CharDevice for MyCharDev { +/// type OpenPtr =3D Box; +/// type Err =3D Error; +/// +/// fn new(_dev_id: CharDeviceID) -> Result { +/// Ok(Self) +/// } +/// +/// fn open(&self, _file: &File) -> Result { +/// pr_info!("Opened device!\n"); +/// Box::new(MyOpenCharDev, GFP_KERNEL).map_err(Into::into) +/// } +/// } +/// +/// #[vtable] +/// impl OpenCharDevice for MyOpenCharDev { +/// type IoctlCmd =3D (); +/// type Err =3D Error; +/// +/// fn read( +/// &self, +/// _file: &LocalFile, +/// _buf: UserSliceWriter, +/// _offset: &mut i64, +/// ) -> Result { +/// pr_info!("Read from device!\n"); +/// Ok(0) +/// } +/// } +/// ``` +#[vtable] +pub trait CharDevice: Sized + Send + Sync + 'static { + /// The type of a (smart) pointer to the associated open device type. + /// + /// This type must implement the [`ForeignOwnable`] trait with the + /// associated borrowed type being a reference to the open device type. + /// + /// Most likely, this will be a smart pointer type like [`Box`] or [`A= rc`], + /// just wrapping the open device type. + /// + /// [`Arc`]: crate::sync::Arc + type OpenPtr: for<'a> ForeignOwnable: Deref>; + + /// The error type returned by the device operations. + /// + /// This type must be convertible into the kernel [`Error`] type. + type Err: Into; + + /// Creates a new instance of the character device. + /// + /// This is called when the device is registered with the kernel. The + /// registered device ID is passed as the `dev_id` parameter. + /// + /// # Errors + /// + /// This function can return an error if the device creation fails. + fn new(dev_id: CharDeviceID) -> Result; + + /// Handles the `open` operation on the device file. + /// + /// This is called when the device file is opened. The `file` parameter + /// contains a reference to the file object representing the opened fi= le. + /// + /// The function should return a pointer to an open device type that + /// implements the [`OpenCharDevice`] trait. This type will handle the + /// actual I/O operations on the opened device file. The returned poin= ter + /// will internally be written to the `private_data` field of the file + /// object. If the release operation is implemented, you are guarantee= d to + /// receive this exact pointer as an argument when the final close of = the + /// device file happens. + /// + /// # Errors + /// + /// This function can return an error if the operation fails. In this = case, + /// the error will be propagated back to the user space as an error co= de. + fn open(&self, _file: &File) -> Result; + + /// Handles the `release` operation on the device file. + /// + /// This is called when the device file is closed. The `file` parameter + /// contains a reference to the file object representing the closed fi= le. + /// The `open_dev` parameter contains the pointer to the open device t= ype + /// that was returned by the corresponding `open` operation. + /// + /// This function is optional. If not implemented, the kernel will just + /// drop the open device type pointer and return success to the user s= pace. + /// + /// # Errors + /// + /// This function can return an error if the operation fails. In this = case, + /// the error will be propagated back to the user space as an error co= de. + fn release(&self, _file: &File, _open_dev: Self::OpenPtr) -> Result<()= , Self::Err> { + kernel::build_error(VTABLE_DEFAULT_ERROR) + } +} + +/// Trait implemented by an open character device. +/// +/// An open character device handles the actual I/O operations on the devi= ce +/// file. It is returned by the `open` operation of the associated charact= er +/// device type that implements the [`CharDevice`] trait. +/// +/// # Example +/// +/// ``` +/// # use kernel::{ +/// # char_dev::{CharDevice, CharDeviceID, OpenCharDevice}, +/// # fs::{File, LocalFile}, +/// # prelude::*, +/// # uaccess::UserSliceWriter, +/// # }; +/// # +/// struct MyCharDev; +/// struct MyOpenCharDev; +/// +/// #[vtable] +/// impl CharDevice for MyCharDev { +/// type OpenPtr =3D Box; +/// type Err =3D Error; +/// +/// fn new(_dev_id: CharDeviceID) -> Result { +/// Ok(Self) +/// } +/// +/// fn open(&self, _file: &File) -> Result { +/// pr_info!("Opened device!\n"); +/// Box::new(MyOpenCharDev, GFP_KERNEL).map_err(Into::into) +/// } +/// } +/// +/// #[vtable] +/// impl OpenCharDevice for MyOpenCharDev { +/// type IoctlCmd =3D (); +/// type Err =3D Error; +/// +/// fn read( +/// &self, +/// _file: &LocalFile, +/// _buf: UserSliceWriter, +/// _offset: &mut i64, +/// ) -> Result { +/// pr_info!("Read from device!\n"); +/// Ok(0) +/// } +/// } +/// ``` +#[vtable] +pub trait OpenCharDevice: Send + Sync { + /// The type of the ioctl command that can be passed to the device. + /// + /// This type must implement the [`IoctlCommand`] trait, which is + /// normally done by deriving the trait using the eponymous macro. + /// + /// If the device does not support any ioctl commands, this type can be + /// set to `()`. + type IoctlCmd: IoctlCommand; + + /// The error type returned by the device operations. + /// + /// This type must be convertible into the kernel [`Error`] type. + type Err: Into; + + /// Handles the `read` operation on the device file. + /// + /// This is called when the device file is read from. The `file` param= eter + /// contains a reference to the file object representing the opened fi= le. + /// The `user_writer` parameter contains a writer that can be used to = write + /// the requested data to the user space buffer. + /// The `offset` parameter contains the current offset in the file, and + /// should be updated to the new offset after the read operation. + /// + /// The function should return the number of bytes read from the devic= e. + /// + /// # Errors + /// + /// This function can return an error if the operation fails. In this = case, + /// the error will be propagated back to the user space as an error co= de. + fn read( + &self, + _file: &LocalFile, + _user_writer: UserSliceWriter, + _offset: &mut i64, + ) -> Result { + kernel::build_error(VTABLE_DEFAULT_ERROR) + } + + /// Handles the `write` operation on the device file. + /// + /// This is called when the device file is written to. The `file` para= meter + /// contains a reference to the file object representing the opened fi= le. + /// The `user_reader` parameter contains a reader that can be used to = read + /// the provided data from the user space buffer. + /// The `offset` parameter contains the current offset in the file, and + /// should be updated to the new offset after the write operation. + /// + /// The function should return the number of bytes written to the devi= ce. + /// + /// # Errors + /// + /// This function can return an error if the operation fails. In this = case, + /// the error will be propagated back to the user space as an error co= de. + fn write( + &self, + _file: &LocalFile, + _user_reader: UserSliceReader, + _offset: &mut i64, + ) -> Result { + kernel::build_error(VTABLE_DEFAULT_ERROR) + } + + /// Handles the `ioctl` operation on the device file. + /// + /// This is called when the user space application performs an ioctl + /// operation on the device file. The `file` parameter contains a refe= rence + /// to the file object representing the opened file. + /// The `cmd` parameter contains the ioctl command to execute. + /// The `compat` parameter indicates whether the ioctl operation is + /// performed in compatibility mode, meaning that the user space appli= cation + /// is running in 32-bit mode on a 64-bit kernel. + /// + /// The function should return the result of the ioctl operation, which + /// could include writing data to a user space buffer, depending on the + /// command in question. + /// + /// # Errors + /// + /// This function can return an error if the operation fails. In this = case, + /// the error will be propagated back to the user space as an error co= de. + fn ioctl( + &self, + _file: &File, + _cmd: Self::IoctlCmd, + #[cfg(CONFIG_COMPAT)] _compat: bool, + ) -> Result { + kernel::build_error(VTABLE_DEFAULT_ERROR) + } + + /// Handles the `llseek` operation on the device file. + /// + /// This is called when the device file is seeking. The `file` paramet= er + /// contains a reference to the file object representing the opened fi= le. + /// The `pos` parameter contains the current position in the file, and= should + /// be updated to the new position after the seek operation. The `offs= et` + /// parameter contains the new offset to seek to, and the `whence` par= ameter + /// contains the seek mode. + /// + /// The function should return the new offset after the seek operation. + /// + /// # Errors + /// + /// This function can return an error if the operation fails. In this = case, + /// the error will be propagated back to the user space as an error co= de. + fn llseek( + &self, + _file: &LocalFile, + _pos: &mut i64, + _offset: i64, + _whence: Whence, + ) -> Result { + kernel::build_error(VTABLE_DEFAULT_ERROR) + } +} + +/// This type alias saves a lot of long types in the fops functions. +#[rustfmt::skip] +type OpenDev<'a, T> =3D < + <::OpenPtr as ForeignOwnable>::Borrowed<'a> as Deref +>::Target; + +/// This struct wraps a [`CharDevice`] together with a kernel [`cdev`] obj= ect. +/// +/// This allows us to retrieve the device type from the `inode` object in = the +/// `open` function using the `container_of` macro. +#[pin_data] +struct CharDeviceContainer { + #[pin] + cdev: Opaque, + inner: T, +} + +impl CharDeviceContainer { + /// Creates a new `CharDeviceContainer` from a device type and a base = device ID. + /// + /// This function initializes a new `cdev` object and registers it wit= h the + /// kernel using the device ID calculated from the base device ID and = the + /// index `i`. + fn register( + i: usize, + base_dev_id: CharDeviceID, + fops: *const bindings::file_operations, + ) -> impl PinInit { + let dev_id =3D CharDeviceID(base_dev_id.0 + i as u32); + + try_pin_init!(Self { + cdev <- Opaque::try_ffi_init(|slot: *mut bindings::cdev| { + // SAFETY: The `slot` pointer is valid but uninitialized a= nd + // `cdev_init` only writes to it. Also, the `fops` pointer= is + // never dereferenced without checking for null first. + unsafe { bindings::cdev_init(slot, fops) }; + + // SAFETY: Since `cdev_init` was called, `slot` is valid a= nd + // initialized, which means that `cdev_add` is safe to cal= l. + to_result(unsafe { + bindings::cdev_add(slot, dev_id.0, 1) + }) + }), + inner: T::new(dev_id).map_err(Into::into)?, + }? Error) + } +} + +/// This struct represents a character device registration. +/// +/// It manages the registration of `NUM_MINORS` character devices of type = `T` +/// with consecutive device IDs starting from `base_dev_id`. +/// +/// The devices are automatically unregistered when the registration objec= t is +/// dropped. +/// +/// # Example +/// +/// ``` +/// # use kernel::{ +/// # c_str, +/// # char_dev::{CharDevice, CharDeviceID, DeviceRegistration, OpenCha= rDevice}, +/// # fs::File, +/// # prelude::*, +/// # }; +/// # +/// struct MyCharDev; +/// struct MyOpenCharDev; +/// +/// #[vtable] +/// impl CharDevice for MyCharDev { +/// // --snip-- +/// # type OpenPtr =3D Box; +/// # type Err =3D Error; +/// # +/// # fn new(_dev_id: CharDeviceID) -> Result { +/// # Ok(Self) +/// # } +/// # +/// # fn open(&self, _file: &File) -> Result= { +/// # Box::new(MyOpenCharDev, GFP_KERNEL).map_err(Into::into) +/// # } +/// } +/// +/// #[vtable] +/// impl OpenCharDevice for MyOpenCharDev { +/// // --snip-- +/// # type IoctlCmd =3D (); +/// # type Err =3D Error; +/// } +/// +/// const DEV_NAME: &CStr =3D c_str!("my_char_dev"); +/// const NUM_MINORS: usize =3D 5; +/// +/// struct MyModule { +/// reg: Pin>>, +/// } +/// +/// impl kernel::Module for MyModule { +/// fn init(module: &'static ThisModule) -> Result { +/// let reg =3D Box::pin_init(DeviceRegistration::register(module,= DEV_NAME), GFP_KERNEL)?; +/// +/// let dev_id =3D reg.get_base_dev_id(); +/// pr_info!( +/// "Registered device {DEV_NAME} with major {} and minors {} = through {} \n", +/// dev_id.major(), +/// dev_id.minor(), +/// dev_id.minor() + NUM_MINORS as u32 - 1 +/// ); +/// +/// Ok(Self { reg }) +/// } +/// } +/// ``` +#[pin_data(PinnedDrop)] +pub struct DeviceRegistration { + base_dev_id: CharDeviceID, + #[pin] + fops: Opaque, + #[pin] + devices: [CharDeviceContainer; NUM_MINORS], +} + +impl DeviceRegistration { + /// Registers `NUM_MINORS` character devices of type `T` with the kern= el. + /// + /// The devices are registered with the name `name` and the module `mo= dule`. + /// + /// The function returns a `PinInit` that resolves to a `Registration` + /// object if the registration was successful. + pub fn register(module: &'static ThisModule, name: &'static CStr) -> i= mpl PinInit { + try_pin_init!(&this in Self { + base_dev_id: Self::alloc_region(name)?, + fops <- fops::create_vtable::(module), + devices <- init::pin_init_array_from_fn(|i| { + // SAFETY: `this` is a non-null pointer to a partially + // initialized `Registration` object, where `base_dev_id` + // and `fops` are already initialized, so it is safe to + // dereference it and access these fields. + unsafe { + CharDeviceContainer::register( + i, + (*this.as_ptr()).base_dev_id, + (*this.as_ptr()).fops.get(), + ) + } + }), + }) + } + + /// Returns the base device ID of the registration. + /// + /// The base device ID is the device ID of the first device registered= by + /// this registration. The device IDs of the other devices are consecu= tive + /// numbers starting from this ID. + pub fn get_base_dev_id(&self) -> CharDeviceID { + self.base_dev_id + } + + fn alloc_region(name: &'static CStr) -> Result { + let mut dev_id =3D CharDeviceID::default(); + + // SAFETY: The `dev_id` pointer is valid and thus safe to write to, + // which means that `alloc_chrdev_region` is safe to call. + to_result(unsafe { + bindings::alloc_chrdev_region( + &mut dev_id as *mut CharDeviceID as *mut bindings::dev_t, + 0, + NUM_MINORS as u32, + name.as_char_ptr(), + ) + })?; + + Ok(dev_id) + } +} + +// SAFETY: Registration with and unregistration of character devices can h= appen +// from any thread or CPU, so `Registration` can be `Send`. +unsafe impl Send for DeviceRegistr= ation {} + +// SAFETY: `Registration` doesn't offer any methods or access to fields wh= en +// shared between threads or CPUs, so it is safe to share it. +unsafe impl Sync for DeviceRegistr= ation {} + +#[pinned_drop] +impl PinnedDrop for DeviceRegistra= tion { + /// Unregisters the character devices. + fn drop(self: Pin<&mut Self>) { + // SAFETY: We never move out of `this`. + let this =3D unsafe { self.get_unchecked_mut() }; + + for i in 0..NUM_MINORS { + // SAFETY: All `cdev`s in the device containers have been + // initialized and added to the system, so it is safe to call + // `cdev_del` on them. + unsafe { + bindings::cdev_del(this.devices[i].cdev.get()); + } + } + + // SAFETY: The device region has been allocated and registered, so= it is + // safe to call `unregister_chrdev_region` on it. + unsafe { + bindings::unregister_chrdev_region(this.base_dev_id.0, NUM_MIN= ORS as u32); + } + } +} + +mod fops { + use super::*; + + /// Creates a file operations table for a character device of type `T`. + pub(super) fn create_vtable( + module: &'static ThisModule, + ) -> impl PinInit> { + Opaque::ffi_init(|slot: *mut bindings::file_operations| { + let fops =3D bindings::file_operations { + owner: module.as_ptr(), + open: Some(open::), + release: Some(close::), + read: >::HAS_READ.then_some(read::), + write: >::HAS_WRITE.then_some(write::), + unlocked_ioctl: >::HAS_IOCTL.then_some(unlo= cked_ioctl::), + #[cfg(CONFIG_COMPAT)] + compat_ioctl: >::HAS_IOCTL.then_some(compat= _ioctl::), + llseek: >::HAS_LLSEEK.then_some(llseek::= ), + ..bindings::file_operations::default() + }; + + // SAFETY: `slot` is a valid but uninitialized pointer to a + // `file_operations` object, so it is safe to write to it. + unsafe { + slot.write(fops); + } + }) + } + + /// Handles the `open` operation for a character device. + /// + /// # Safety + /// + /// * The caller must ensure that `inode` points at a valid inode and = that + /// its `i_cdev` field points at a valid `cdev`, contained by a + /// `CharDeviceContainer`. + /// * The caller must ensure that `file` points at a valid file and th= at the + /// file's refcount is positive for the duration of the function cal= l. + /// * The caller must ensure that if there are active `fdget_pos` call= s on + /// this file, then they took the `f_pos_lock` mutex. + unsafe extern "C" fn open( + inode: *mut bindings::inode, + file: *mut bindings::file, + ) -> i32 { + // Handle non O_LARGEFILE open on 32-bit systems + // + // SAFETY: The caller guarantees that `inode` points at a valid in= ode + // and that `file` points at a valid file, so it is safe to call + // `generic_file_open` on them. + let ret =3D unsafe { bindings::generic_file_open(inode, file) }; + if ret !=3D 0 { + return ret; + } + + // SAFETY: The caller guarantees that `inode` points at a valid in= ode + // and that its `i_cdev` field points at a valid `cdev`, contained= by a + // `CharDeviceContainer`, so it is safe to access this containe= r. + let container =3D unsafe { + &*container_of!( + (*inode).__bindgen_anon_4.i_cdev, + CharDeviceContainer, + cdev + ) + }; + + // SAFETY: The caller guarantees that `file` points at a valid fil= e and + // that the file's refcount is positive, as well as that any active + // `fdget_pos` calls on this file took the `f_pos_lock` mutex, so = it is + // safe to call `File::from_raw_file` on it. + let file =3D unsafe { File::from_raw_file(file) }; + + let open_dev =3D match container.inner.open(file).map_err(Into::in= to) { + Ok(open_dev) =3D> open_dev, + Err(e) =3D> { + return e.to_errno(); + } + }; + + // SAFETY: The caller guarantees that `file` points at a valid fil= e, so + // it is safe to access and modify the `private_data` field on it. + unsafe { + (*file.as_ptr()).private_data =3D open_dev.into_foreign().cast= _mut(); + } + + 0 + } + + /// Handles the `close` operation for a character device. + /// + /// # Safety + /// + /// * The caller must ensure that `inode` points at a valid inode and = that + /// its `i_cdev` field points at a valid `cdev`, contained by a + /// `CharDeviceContainer`. + /// * The caller must ensure that `file` points at a valid file and th= at the + /// file's refcount is positive for the duration of the function cal= l. + /// * The caller must ensure that if there are active `fdget_pos` call= s on + /// this file, then they took the `f_pos_lock` mutex. + /// * The caller must ensure that the `private_data` field of `file` i= s a + /// pointer returned by [`ForeignOwnable::into_foreign`] for which a + /// previous matching [`ForeignOwnable::from_foreign`] hasn't been c= alled + /// yet. Additionally, all instances (if any) of values returned by + /// [`ForeignOwnable::borrow`] for this object must have been droppe= d. + unsafe extern "C" fn close( + inode: *mut bindings::inode, + file: *mut bindings::file, + ) -> i32 { + // SAFETY: The caller guarantees that `inode` points at a valid in= ode + // and that its `i_cdev` field points at a valid `cdev`, contained= by a + // `CharDeviceContainer`, so it is safe to access this containe= r. + let container =3D unsafe { + &*container_of!( + (*inode).__bindgen_anon_4.i_cdev, + CharDeviceContainer, + cdev + ) + }; + + // SAFETY: The caller guarantees that `file` points at a valid fil= e, + // that its `private_data` field is a pointer returned by + // `ForeignOwnable::into_foreign` for which a previous matching + // `ForeignOwnable::from_foreign` hasn't been called yet, and that= all + // instances (if any) of values returned by `ForeignOwnable::borro= w` for + // this object have been dropped already, so it is safe to call + // `ForeignOwnable::from_foreign` on it. + let open_dev =3D + unsafe { ::from_foreign((*file).= private_data) }; + + // SAFETY: The caller guarantees that `file` points at a valid fil= e and + // that the file's refcount is positive, as well as that any active + // `fdget_pos` calls on this file took the `f_pos_lock` mutex, so = it is + // safe to call `File::from_raw_file` on it. + let file =3D unsafe { File::from_raw_file(file) }; + + if T::HAS_RELEASE { + if let Err(e) =3D container.inner.release(file, open_dev).map_= err(Into::into) { + return e.to_errno(); + } + } + + 0 + } + + /// Handles the `read` operation for a character device. + /// + /// # Safety + /// + /// * The caller must ensure that `file` points at a valid file and th= at the + /// file's refcount is positive for the duration of the function cal= l. + /// * The caller must ensure that if there is an active `fdget_pos` ca= ll on + /// this file that didn't take the `f_pos_lock` mutex, then that cal= l is + /// on the current thread. + /// * The caller must ensure that the `private_data` field of `file` i= s a + /// pointer returned by [`ForeignOwnable::into_foreign`] for which a + /// previous matching [`ForeignOwnable::from_foreign`] hasn't been c= alled + /// yet. + /// * The caller must ensure that the `offset` pointer is valid and + /// initialized, and that we have exclusive access to it for the dur= ation + /// of the function call. + unsafe extern "C" fn read( + file: *mut bindings::file, + user_buffer: *mut ffi::c_char, + count: usize, + offset: *mut bindings::loff_t, + ) -> isize { + // SAFETY: The caller guarantees that `file` points at a valid fil= e and + // that its `private_data` field is a pointer returned by + // `ForeignOwnable::into_foreign` for which a previous matching + // `ForeignOwnable::from_foreign` hasn't been called yet, so it is= safe + // to call `ForeignOwnable::borrow` on it. + let open_dev =3D unsafe { ::borrow((= *file).private_data) }; + + // SAFETY: The caller guarantees that `file` points at a valid fil= e and + // that the file's refcount is positive, as well as that if there = is an + // active `fdget_pos` call on this file that didn't take the + // `f_pos_lock` mutex, then that call is on the current thread, so= it is + // safe to call `LocalFile::from_raw_file` on it. + let file =3D unsafe { LocalFile::from_raw_file(file) }; + + let user_writer =3D UserSlice::new(user_buffer as UserPtr, count).= writer(); + + // SAFETY: The caller guarantees that `offset` is a valid and + // initialized pointer, and that we have exclusive access to it fo= r the + // duration of the function call, so it is safe to dereference and= form + // a mutable reference to it. + let offset =3D unsafe { &mut *offset }; + + match open_dev.read(file, user_writer, offset).map_err(Into::into)= { + Ok(n) =3D> n as isize, + Err(e) =3D> e.to_errno() as isize, + } + } + + /// Handles the `write` operation for a character device. + /// + /// # Safety + /// + /// * The caller must ensure that `file` points at a valid file and th= at the + /// file's refcount is positive for the duration of the function cal= l. + /// * The caller must ensure that if there is an active `fdget_pos` ca= ll on + /// this file that didn't take the `f_pos_lock` mutex, then that cal= l is + /// on the current thread. + /// * The caller must ensure that the `private_data` field of `file` i= s a + /// pointer returned by [`ForeignOwnable::into_foreign`] for which a + /// previous matching [`ForeignOwnable::from_foreign`] hasn't been c= alled + /// yet. + /// * The caller must ensure that the `offset` pointer is valid and + /// initialized, and that we have exclusive access to it for the dur= ation + /// of the function call. + unsafe extern "C" fn write( + file: *mut bindings::file, + user_buffer: *const ffi::c_char, + count: usize, + offset: *mut bindings::loff_t, + ) -> isize { + // SAFETY: The caller guarantees that `file` points at a valid fil= e and + // that its `private_data` field is a pointer returned by + // `ForeignOwnable::into_foreign` for which a previous matching + // `ForeignOwnable::from_foreign` hasn't been called yet, so it is= safe + // to call `ForeignOwnable::borrow` on it. + let open_dev =3D unsafe { ::borrow((= *file).private_data) }; + + // SAFETY: The caller guarantees that `file` points at a valid fil= e and + // that the file's refcount is positive, as well as that if there = is an + // active `fdget_pos` call on this file that didn't take the + // `f_pos_lock` mutex, then that call is on the current thread, so= it is + // safe to call `LocalFile::from_raw_file` on it. + let file =3D unsafe { LocalFile::from_raw_file(file) }; + + let user_reader =3D UserSlice::new(user_buffer as UserPtr, count).= reader(); + + // SAFETY: The caller guarantees that `offset` is a valid and + // initialized pointer, and that we have exclusive access to it fo= r the + // duration of the function call, so it is safe to dereference and= form + // a mutable reference to it. + let offset =3D unsafe { &mut *offset }; + + match open_dev + .write(file, user_reader, offset) + .map_err(Into::into) + { + Ok(n) =3D> n as isize, + Err(e) =3D> e.to_errno() as isize, + } + } + + /// Handles the `unlocked_ioctl` operation for a character device. + /// + /// # Safety + /// + /// * The caller must ensure that `file` points at a valid file and th= at the + /// file's refcount is positive for the duration of the function cal= l. + /// * The caller must ensure that if there are active `fdget_pos` call= s on + /// this file, then they took the `f_pos_lock` mutex. + /// * The caller must ensure that the `private_data` field of `file` i= s a + /// pointer returned by [`ForeignOwnable::into_foreign`] for which a + /// previous matching [`ForeignOwnable::from_foreign`] hasn't been c= alled + /// yet. + unsafe extern "C" fn unlocked_ioctl( + file: *mut bindings::file, + cmd: ffi::c_uint, + arg: ffi::c_ulong, + ) -> ffi::c_long { + type Cmd<'a, T> =3D as OpenCharDevice>::IoctlCmd; + + // SAFETY: The caller guarantees that `file` points at a valid fil= e and + // that its `private_data` field is a pointer returned by + // `ForeignOwnable::into_foreign` for which a previous matching + // `ForeignOwnable::from_foreign` hasn't been called yet, so it is= safe + // to call `ForeignOwnable::borrow` on it. + let open_dev =3D unsafe { ::borrow((= *file).private_data) }; + + // SAFETY: The caller guarantees that `file` points at a valid fil= e and + // that the file's refcount is positive, as well as that any active + // `fdget_pos` calls on this file took the `f_pos_lock` mutex, so = it is + // safe to call `File::from_raw_file` on it. + let file =3D unsafe { File::from_raw_file(file) }; + + let cmd =3D match as IoctlCommand>::parse(cmd, arg).ma= p_err(Into::into) { + Ok(cmd) =3D> cmd, + Err(e) =3D> return e.to_errno() as ffi::c_long, + }; + + match open_dev + .ioctl( + file, + cmd, + #[cfg(CONFIG_COMPAT)] + false, + ) + .map_err(Into::into) + { + Ok(ret) =3D> ret as ffi::c_long, + Err(e) =3D> e.to_errno() as ffi::c_long, + } + } + + /// Handles the `compat_ioctl` operation for a character device. + /// + /// # Safety + /// + /// * The caller must ensure that `file` points at a valid file and th= at the + /// file's refcount is positive for the duration of the function cal= l. + /// * The caller must ensure that if there are active `fdget_pos` call= s on + /// this file, then they took the `f_pos_lock` mutex. + /// * The caller must ensure that the `private_data` field of `file` i= s a + /// pointer returned by [`ForeignOwnable::into_foreign`] for which a + /// previous matching [`ForeignOwnable::from_foreign`] hasn't been c= alled + /// yet. + #[cfg(CONFIG_COMPAT)] + unsafe extern "C" fn compat_ioctl( + file: *mut bindings::file, + cmd: ffi::c_uint, + arg: ffi::c_ulong, + ) -> ffi::c_long { + type Cmd<'a, T> =3D as OpenCharDevice>::IoctlCmd; + + // SAFETY: The caller guarantees that `file` points at a valid fil= e and + // that its `private_data` field is a pointer returned by + // `ForeignOwnable::into_foreign` for which a previous matching + // `ForeignOwnable::from_foreign` hasn't been called yet, so it is= safe + // to call `ForeignOwnable::borrow` on it. + let open_dev =3D unsafe { ::borrow((= *file).private_data) }; + + // SAFETY: The caller guarantees that `file` points at a valid fil= e and + // that the file's refcount is positive, as well as that any active + // `fdget_pos` calls on this file took the `f_pos_lock` mutex, so = it is + // safe to call `File::from_raw_file` on it. + let file =3D unsafe { File::from_raw_file(file) }; + + let parse_fn =3D if as IoctlCommand>::HAS_COMPAT_PARSE= { + as IoctlCommand>::compat_parse + } else { + as IoctlCommand>::parse + }; + + let cmd =3D match parse_fn(cmd, arg).map_err(Into::into) { + Ok(cmd) =3D> cmd, + Err(e) =3D> return e.to_errno() as ffi::c_long, + }; + + match open_dev.ioctl(file, cmd, true).map_err(Into::into) { + Ok(ret) =3D> ret as ffi::c_long, + Err(e) =3D> e.to_errno() as ffi::c_long, + } + } + + /// Handles the `llseek` operation for a character device. + /// + /// # Safety + /// + /// * The caller must ensure that `file` points at a valid file and th= at the + /// file's refcount is positive for the duration of the function cal= l. + /// * The caller must ensure that if there is an active `fdget_pos` ca= ll on + /// this file that didn't take the `f_pos_lock` mutex, then that cal= l is + /// on the current thread. + /// * The caller must ensure that the `private_data` field of `file` i= s a + /// pointer returned by [`ForeignOwnable::into_foreign`] for which a + /// previous matching [`ForeignOwnable::from_foreign`] hasn't been c= alled + /// yet. + /// * The caller must ensure that `whence` is less than or equal to `S= EEK_MAX`. + unsafe extern "C" fn llseek( + file: *mut bindings::file, + offset: bindings::loff_t, + whence: ffi::c_int, + ) -> bindings::loff_t { + // SAFETY: The caller guarantees that `file` points at a valid fil= e and + // that its `private_data` field is a pointer returned by + // `ForeignOwnable::into_foreign` for which a previous matching + // `ForeignOwnable::from_foreign` hasn't been called yet, so it is= safe + // to call `ForeignOwnable::borrow` on it. + let open_dev =3D unsafe { ::borrow((= *file).private_data) }; + + // SAFETY: The caller guarantees that `file` points at a valid fil= e, so + // it is safe to access its `f_pos` field. + let mut pos =3D unsafe { (*file).f_pos }; + + // SAFETY: The caller guarantees that `file` points at a valid fil= e and + // that the file's refcount is positive, as well as that if there = is an + // active `fdget_pos` call on this file that didn't take the + // `f_pos_lock` mutex, then that call is on the current thread, so= it is + // safe to call `LocalFile::from_raw_file` on it. + let file =3D unsafe { LocalFile::from_raw_file(file) }; + + // SAFETY: The caller guarantees that `whence` is less than or equ= al to + // `SEEK_MAX`, so it is safe to transmute it to a `Whence` by the + // constant assertion above. + let whence =3D unsafe { mem::transmute::(whence as _)= }; + + let res =3D match open_dev + .llseek(file, &mut pos, offset, whence) + .map_err(Into::into) + { + Ok(pos) =3D> pos as bindings::loff_t, + Err(e) =3D> e.to_errno() as bindings::loff_t, + }; + + // SAFETY: The caller guarantees that `file` points at a valid fil= e, so + // it is safe to access its `f_pos` field. + unsafe { + (*file.as_ptr()).f_pos =3D pos; + } + + res + } +} diff --git a/rust/kernel/init/macros.rs b/rust/kernel/init/macros.rs index 1fd146a832416514a2bdcb269615509d75e3a559..d75376bb5ba8e85d19a106917b5= d5ce3febc7533 100644 --- a/rust/kernel/init/macros.rs +++ b/rust/kernel/init/macros.rs @@ -1154,9 +1154,13 @@ fn assert_zeroable(_: *mu= t T) {} unsafe { ::core::ptr::write_bytes(slot, 0, 1) }; $init_zeroed // This will be `()` if set. })? - // Create the `this` so it can be referenced by the us= er inside of the - // expressions creating the individual fields. - $(let $this =3D unsafe { ::core::ptr::NonNull::new_unc= hecked(slot) };)? + $( + // Create the `this` so it can be referenced by th= e user inside of the + // expressions creating the individual fields. + // + // SAFETY: `slot` is valid, because we are inside = of an initializer closure. + let $this =3D unsafe { ::core::ptr::NonNull::new_u= nchecked(slot) }; + )? // Initialize every field. $crate::__init_internal!(init_slot($($use_data)?): @data(data), diff --git a/rust/kernel/ioctl.rs b/rust/kernel/ioctl.rs index 2fc7662339e54b20153a46da06cc5c2e450024da..03359ab28495b94d98d53db2115= bbbcc520c18a3 100644 --- a/rust/kernel/ioctl.rs +++ b/rust/kernel/ioctl.rs @@ -6,7 +6,8 @@ =20 #![expect(non_snake_case)] =20 -use crate::build_assert; +use crate::{build_assert, error::VTABLE_DEFAULT_ERROR, prelude::*}; +use core::ffi; =20 /// Build an ioctl number, analogous to the C macro of the same name. #[inline(always)] @@ -70,3 +71,46 @@ pub const fn _IOC_NR(nr: u32) -> u32 { pub const fn _IOC_SIZE(nr: u32) -> usize { ((nr >> uapi::_IOC_SIZESHIFT) & uapi::_IOC_SIZEMASK) as usize } + +/// Types implementing this trait can be used to parse ioctl commands. +#[vtable] +pub trait IoctlCommand: Sized + Send + Sync + 'static { + /// The error type returned by the parse functions. + /// + /// This type must be convertible into the kernel [`Error`] type. + type Err: Into; + + /// Parse an ioctl command. + /// + /// This function parses the `cmd` argument as an ioctl command number + /// and returns a command that interprets the `arg` argument as needed. + /// + /// # Errors + /// + /// This function may return an error if the command is invalid. + fn parse(cmd: ffi::c_uint, arg: ffi::c_ulong) -> Result; + + /// Parse an ioctl command for compatibility mode. + /// + /// If the compatibility mode is enabled, this function parses the `cm= d` + /// argument as an ioctl command number and returns a command that + /// interprets the `arg` argument as needed. The values come from a 32= -bit + /// user-space application and may need to be parsed differently. + /// + /// # Errors + /// + /// This function may return an error if the command is invalid. + #[cfg(CONFIG_COMPAT)] + fn compat_parse(_cmd: ffi::c_uint, _arg: ffi::c_ulong) -> Result { + kernel::build_error(VTABLE_DEFAULT_ERROR) + } +} + +#[vtable] +impl IoctlCommand for () { + type Err =3D Error; + + fn parse(_cmd: ffi::c_uint, _arg: ffi::c_ulong) -> Result { + Err(ENOTTY) + } +} diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs index 6bcbe9bbb46294e29c8d78cb4cae3cbe13062104..86969b1ac599cf00683eef1182a= fb39811ba88b7 100644 --- a/rust/kernel/lib.rs +++ b/rust/kernel/lib.rs @@ -31,6 +31,7 @@ #[cfg(CONFIG_BLOCK)] pub mod block; mod build_assert; +pub mod char_dev; pub mod device; pub mod error; #[cfg(CONFIG_RUST_FW_LOADER_ABSTRACTIONS)] --=20 2.47.0 From nobody Wed Nov 27 07:35:02 2024 Received: from dd3514.kasserver.com (dd3514.kasserver.com [85.13.129.232]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id EA3401CEE86; Fri, 11 Oct 2024 18:57:58 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=85.13.129.232 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1728673083; cv=none; b=DH3h7kF3YAD0FuOZwRtowihGecdv1O4nESks4m/PBAmwPM+XSdUSCO9jtKZq88fuofgthriIZn7tpb2gJfX2eKSHUtT9Cy+SwF2vHgh0jPVBAjX47p6psApoS0D70Yiq5X0GyNqZhRJITxor0CYKS1QFsoRpNotmIc8Qz8h+pWU= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1728673083; c=relaxed/simple; bh=0q0YdXypHQoS2L7yNMI6p3pr+nIDzO+5+R38LNiFp9I=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=DxVkIc+3OG4QoWW+ueL8MGaXV8rPquXadL2xkw08hk9pekSLWzsMY7K+93TAoVlFPCI9OCbAvir3aSyM7bBqmiCLvB7UkoJLUyIbsdSkxvC06P7v/LCwg6sR1evDEttB4slYzPCpcOK8Vie/q8dbhi3xxNDUBnjyw7PX8Hvcjh8= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=walterzollerpiano.com; spf=pass smtp.mailfrom=walterzollerpiano.com; dkim=pass (2048-bit key) header.d=walterzollerpiano.com header.i=@walterzollerpiano.com header.b=jB77iuDO; arc=none smtp.client-ip=85.13.129.232 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=walterzollerpiano.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=walterzollerpiano.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=walterzollerpiano.com header.i=@walterzollerpiano.com header.b="jB77iuDO" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=walterzollerpiano.com; s=kas202409051210; t=1728673044; bh=WrqJka8i5qDvGJ+Y0RrHzdEvY1FeuQPvghnutR0w8bE=; h=From:Date:Subject:References:In-Reply-To:To:Cc:From; b=jB77iuDOLObA7y9Apm6haPksllgaOC4FoOGXsaV3JtF0HyQu81s+RVDkxZ5ab6jYu RRIYTWDcH8wNuUKyUADBgFmw/qOz0dNQcV4tvrha5PEbT0eIoyc/IxwPzZVDggQ5of 94RCYwohBlrZnqOI6pgpFo6b3PAu819uF++8sQC5TlYwSGbfjEhafY3Sk3iGk7xd6l PCCyFiHoga2U2tAmd1ZiA+w4A7KJRbmf3sBLCi7uGNHYsPCVxxpXQjVni80HFV9tCU 6sq0yHJF2G5hXaevOgRbsthYPgj1aJuTUb6k9tQ6SYlxzBEaUWQeQ+N0E1v0nAWRXB +lDQwyrxM2alA== Received: from [127.0.0.1] (31-10-158-205.cgn.dynamic.upc.ch [31.10.158.205]) by dd3514.kasserver.com (Postfix) with ESMTPSA id EF40A103AE0; Fri, 11 Oct 2024 20:57:23 +0200 (CEST) From: Josef Zoller Date: Fri, 11 Oct 2024 20:55:43 +0200 Subject: [PATCH 2/3] rust: macros: add IoctlCommand derive macro Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20241011-rust-char-dev-v1-2-350225ae228b@walterzollerpiano.com> References: <20241011-rust-char-dev-v1-0-350225ae228b@walterzollerpiano.com> In-Reply-To: <20241011-rust-char-dev-v1-0-350225ae228b@walterzollerpiano.com> To: Arnd Bergmann , Greg Kroah-Hartman , Miguel Ojeda , Alex Gaynor Cc: Boqun Feng , Gary Guo , =?utf-8?q?Bj=C3=B6rn_Roy_Baron?= , Benno Lossin , Andreas Hindborg , Alice Ryhl , Trevor Gross , linux-kernel@vger.kernel.org, rust-for-linux@vger.kernel.org, Josef Zoller X-Mailer: b4 0.14.2 X-Developer-Signature: v=1; a=openpgp-sha256; l=15919; i=josef@walterzollerpiano.com; h=from:subject:message-id; bh=0q0YdXypHQoS2L7yNMI6p3pr+nIDzO+5+R38LNiFp9I=; b=owEBbQKS/ZANAwAIAROd718OBo3IAcsmYgBnCXUSWhbnapsd0DsRQAG42iRnQlj4GIA13ovNl CjjQpUIx+iJAjMEAAEIAB0WIQQnwV8chXqnjaC1vcATne9fDgaNyAUCZwl1EgAKCRATne9fDgaN yBmuD/0WZF294zFm8cl3uzo3YMELuCJioLsL21Ex5jfgADB5TOSc5LbxDOwKPPFc6STVtIPdtFJ 1IJI90YpTrHTXEGw8EUrml2+rriy3D2kgncXcOs0nhpRvTlU+S8Peq3pNh2vjxO4/tdFM43+/Jg UR7IxoPcvOcbL0/BjwWhDWQIe28ZPpjDG7KcQwY+5Ndo2Pq9rJAGOalpMw3kobcoLZtC394JtL+ cYg6Yih30qQucvizCOu2haTmDFSq2F+1/2rOMmDu5UurAfZBt0ZJy9mXp86t2sGMJQeFdabYaeQ CjP2DY8F9CVzn3kxfpDM9n5OGc7BL57yihnlgOCbe8bJS5YN3/okcBhrQFTdmccuWl+yadCA/5+ xNQ2+TjMGGq8OJUcRU66NS7JkrxsG8ExpRDYg6yZWB4ss/ZoeyVBCQ/0dN499u1wy6qyRjjyLkL dFCymHSwe3TBD5ykmSKDdThPsWGpNrzBE6ZKWBQ6YkptAo/XoSnBuQTqtBweRIm2C+PuK9EA1m+ 6Wl+/tHKCLmp6trcEmD7QSxvprg5COyd1ILOUDAPZbmNCBlZlkntD6YUSDZesKUmxuknp0RYwej W8oJp+B1vkveIHdl4Tk5AaZ2xlybXhB+XUieebvOr/IueZfsDEnL2u66AalOChzZQorRSl5X1bo HYgcKlJpajrP/Gw== X-Developer-Key: i=josef@walterzollerpiano.com; a=openpgp; fpr=27C15F1C857AA78DA0B5BDC0139DEF5F0E068DC8 X-Spamd-Bar: ---- Provide a macro that derives the `IoctlCommand` trait for simple enums by converting every variant into a unique command. The macro can be instructed to use a specific letter or integer as the code. Each variant is then assigned a consecutive number starting from 0 or a given value. The type of the command, i.e. if it is a read or write command, is inferred from the variant's associated data: if it has no data or only an integer, it is neither read nor write, if it has a UserSliceReader it is a write command, if it has a UserSliceWriter it is a read command, and if it just has a UserSlice it is a read-write command. The code and the variant's number and type are then combined to parse the command from the user-provided cmd and arg values. Signed-off-by: Josef Zoller --- rust/kernel/ioctl.rs | 190 ++++++++++++++++++++++++++++++++++++++++++++ rust/kernel/prelude.rs | 2 +- rust/macros/ioctl_cmd.rs | 202 +++++++++++++++++++++++++++++++++++++++++++= ++++ rust/macros/lib.rs | 21 +++++ 4 files changed, 414 insertions(+), 1 deletion(-) diff --git a/rust/kernel/ioctl.rs b/rust/kernel/ioctl.rs index 03359ab28495b94d98d53db2115bbbcc520c18a3..f6af9c10c0b244b8d8183cf70b4= ef5ce9233c935 100644 --- a/rust/kernel/ioctl.rs +++ b/rust/kernel/ioctl.rs @@ -73,6 +73,22 @@ pub const fn _IOC_SIZE(nr: u32) -> usize { } =20 /// Types implementing this trait can be used to parse ioctl commands. +/// +/// Normally, this trait is derived for a command enum. +/// +/// # Example +/// +/// ``` +/// #[derive(IoctlCommand)] +/// #[ioctl(code =3D 0x18, start_num =3D 0)] +/// enum Command { +/// NoReadWrite, // No read or write access. +/// NoReadWriteButTakesArg(u64), // No read or write access, but takes= an argument. +/// ReadOnly(UserSliceWriter), // We write data for the user to read. +/// WriteOnly(UserSliceReader), // We read data that the user wrote. +/// WriteAndRead(UserSlice), // We read data from the user and the= n write data to the user. +/// } +/// ``` #[vtable] pub trait IoctlCommand: Sized + Send + Sync + 'static { /// The error type returned by the parse functions. @@ -114,3 +130,177 @@ fn parse(_cmd: ffi::c_uint, _arg: ffi::c_ulong) -> Re= sult { Err(ENOTTY) } } + +/// Support macro for deriving the `IoctlCommand` trait. +#[doc(hidden)] +#[macro_export] +macro_rules! __derive_ioctl_cmd { + (parse_input: + @enum_name($enum_name:ident), + @code($code:literal), + @variants( + $( + @variant($i:literal, $variant:ident, $arg_type:tt), + )* + ) + ) =3D> { + #[automatically_derived] + impl $crate::ioctl::IoctlCommand for $enum_name { + type Err =3D $crate::error::Error; + + const USE_VTABLE_ATTR: () =3D (); + + const HAS_PARSE: bool =3D true; + + fn parse( + cmd: ::core::ffi::c_uint, + arg: ::core::ffi::c_ulong, + ) -> ::core::result::Result { + let ty =3D $crate::ioctl::_IOC_TYPE(cmd) as u8; + + if ty !=3D $code { + return Err($crate::error::code::ENOTTY); + } + + let nr =3D $crate::ioctl::_IOC_NR(cmd) as u8; + let dir =3D $crate::ioctl::_IOC_DIR(cmd); + let size =3D $crate::ioctl::_IOC_SIZE(cmd); + + // Make sure we don't get unused parameter warnings + let _ =3D arg; + + match (nr, dir, size) { + $( + ::kernel::__derive_ioctl_cmd!( + match_pattern: + @variant($i, $arg_type) + ) =3D> ::kernel::__derive_ioctl_cmd!( + match_body: + @dir(dir), + @size(size), + @arg(arg), + @variant($variant, $arg_type) + ), + )* + _ =3D> Err($crate::error::code::ENOTTY), + } + } + } + }; + (match_pattern: + @variant($i:literal, None) + ) =3D> { + ($i, $crate::uapi::_IOC_NONE, 0) + }; + (match_body: + @dir($dir:ident), + @size($size:ident), + @arg($arg:ident), + @variant($variant:ident, None) + ) =3D> { + Ok(Self::$variant) + }; + (match_pattern: + @variant($i:literal, u64) + ) =3D> { + ($i, $crate::uapi::_IOC_NONE, 0) + }; + (match_body: + @dir($dir:ident), + @size($size:ident), + @arg($arg:ident), + @variant($variant:ident, u64) + ) =3D> { + Ok(Self::$variant($arg)) + }; + (match_pattern: + @variant($i:literal, UserSliceWriter) + ) =3D> { + ($i, $crate::uapi::_IOC_READ, _) + }; + (match_body: + @dir($dir:ident), + @size($size:ident), + @arg($arg:ident), + @variant($variant:ident, UserSliceWriter) + ) =3D> { + { + let user_writer =3D $crate::uaccess::UserSlice::new( + $arg as $crate::uaccess::UserPtr, + $size + ) + .writer(); + + Ok(Self::$variant(user_writer)) + } + }; + (match_pattern: + @variant($i:literal, UserSliceReader) + ) =3D> { + ($i, $crate::uapi::_IOC_WRITE, _) + }; + (match_body: + @dir($dir:ident), + @size($size:ident), + @arg($arg:ident), + @variant($variant:ident, UserSliceReader) + ) =3D> { + { + let user_reader =3D $crate::uaccess::UserSlice::new( + $arg as $crate::uaccess::UserPtr, + $size + ) + .reader(); + + Ok(Self::$variant(user_reader)) + } + }; + (match_pattern: + @variant($i:literal, UserSlice) + ) =3D> { + ($i, _, _) + }; + (match_body: + @dir($dir:ident), + @size($size:ident), + @arg($arg:ident), + @variant($variant:ident, UserSlice) + ) =3D> { + // Unfortunately, we cannot just do a match guard + if $dir !=3D $crate::uapi::_IOC_READ | $crate::uapi::_IOC_WRITE { + Err($crate::error::code::ENOTTY) + } else { + let user_slice =3D $crate::uaccess::UserSlice::new( + $arg as $crate::uaccess::UserPtr, + $size + ); + + Ok(Self::$variant(user_slice)) + } + }; + (match_pattern: + @variant($i:literal, $arg_type:tt) + ) =3D> { + ($i, _, _) + }; + (match_body: + @dir($dir:ident), + @size($size:ident), + @arg($arg:ident), + @variant($variant:ident, $arg_type:tt) + ) =3D> { + { + // We have an unsupported argument type + const _: () =3D ::core::assert!( + false, + ::core::concat!( + "Invalid argument type for ioctl command ", + stringify!($variant), + ": ", + stringify!($arg_type), + ) + ); + ::core::unreachable!() + } + }; +} diff --git a/rust/kernel/prelude.rs b/rust/kernel/prelude.rs index 4571daec0961bb34fb6956a4e9eda8445954b719..1277d1ec5a476d3e115f6b2ba43= 2b0fbe28941a2 100644 --- a/rust/kernel/prelude.rs +++ b/rust/kernel/prelude.rs @@ -20,7 +20,7 @@ pub use alloc::{boxed::Box, vec::Vec}; =20 #[doc(no_inline)] -pub use macros::{module, pin_data, pinned_drop, vtable, Zeroable}; +pub use macros::{module, pin_data, pinned_drop, vtable, IoctlCommand, Zero= able}; =20 pub use super::build_assert; =20 diff --git a/rust/macros/ioctl_cmd.rs b/rust/macros/ioctl_cmd.rs new file mode 100644 index 0000000000000000000000000000000000000000..366a9b1f7ba70ba764b0d78cb32= d82125bc7b854 --- /dev/null +++ b/rust/macros/ioctl_cmd.rs @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: GPL-2.0 + +use proc_macro::{token_stream, Delimiter, Literal, TokenStream, TokenTree}; + +fn expect_punct(input: &mut impl Iterator, expected: c= har, reason: &str) { + let Some(TokenTree::Punct(punct)) =3D input.next() else { + panic!("expected '{expected}' {reason}"); + }; + + if punct.as_char() !=3D expected { + panic!("expected '{expected}' {reason}"); + } +} + +fn expect_ident(input: &mut impl Iterator, expected: &= str, reason: &str) { + let Some(TokenTree::Ident(ident)) =3D input.next() else { + panic!("expected '{expected}' {reason}"); + }; + + if ident.to_string() !=3D expected { + panic!("expected '{expected}' {reason}"); + } +} + +fn expect_group( + input: &mut impl Iterator, + expected: Delimiter, + reason: &str, +) -> token_stream::IntoIter { + let Some(TokenTree::Group(group)) =3D input.next() else { + panic!("expected group {reason}"); + }; + + if group.delimiter() !=3D expected { + panic!("expected group {reason}"); + } + + group.stream().into_iter() +} + +fn parse_attribute(input: &mut impl Iterator) -> (u8, = u8) { + expect_punct(input, '#', "to start attribute"); + + let mut stream =3D expect_group(input, Delimiter::Bracket, "as attribu= te body"); + + expect_ident(&mut stream, "ioctl", "as attribute name"); + + let mut inner_stream =3D expect_group( + &mut stream, + Delimiter::Parenthesis, + "as attribute arguments", + ); + + expect_ident(&mut inner_stream, "code", "as ioctl attribute field"); + expect_punct(&mut inner_stream, '=3D', "in ioctl attribute field"); + + let Some(TokenTree::Literal(lit)) =3D inner_stream.next() else { + panic!("expected ioctl attribute code value"); + }; + + let lit_str =3D lit.to_string(); + let code =3D if lit_str.starts_with("b'") { + lit_str + .chars() + .nth(2) + .expect("expected ioctl attribute code value") as u8 + } else if let Some(hex) =3D lit_str.strip_prefix("0x") { + u8::from_str_radix(hex, 16).expect("expected ioctl attribute code = value") + } else { + lit_str + .parse() + .expect("expected ioctl attribute code value") + }; + + let start_num =3D if let Some(tree) =3D inner_stream.next() { + if !matches!(tree, TokenTree::Punct(punct) if punct.as_char() =3D= =3D ',') { + panic!("expected ioctl attribute comma"); + } + + expect_ident(&mut inner_stream, "start_num", "as ioctl attribute f= ield"); + expect_punct(&mut inner_stream, '=3D', "in ioctl attribute field"); + + let Some(TokenTree::Literal(lit)) =3D inner_stream.next() else { + panic!("expected ioctl attribute start number value"); + }; + + lit.to_string() + .parse() + .expect("expected ioctl attribute start number value") + } else { + 0 + }; + + assert!( + inner_stream.next().is_none(), + "unexpected token in ioctl attribute" + ); + assert!( + stream.next().is_none(), + "unexpected token in ioctl attribute" + ); + + (code, start_num) +} + +fn parse_enum_def(input: &mut impl Iterator) -> TokenT= ree { + expect_ident(input, "enum", "to start enum definition"); + + let Some(ident @ TokenTree::Ident(_)) =3D input.next() else { + panic!("expected enum name"); + }; + + ident +} + +fn parse_enum_body( + input: &mut impl Iterator, +) -> Vec<(TokenTree, Option)> { + let mut stream =3D expect_group(input, Delimiter::Brace, "as enum body= ").peekable(); + + let mut variants =3D Vec::new(); + + while let Some(variant) =3D stream.next_if(|t| matches!(t, TokenTree::= Ident(_))) { + let arg_type =3D if let Some(TokenTree::Group(group)) =3D + stream.next_if(|t| matches!(t, TokenTree::Group(_))) + { + if group.delimiter() !=3D Delimiter::Parenthesis { + panic!("expected group"); + } + + let mut inner_stream =3D group.stream().into_iter(); + + let arg_type =3D if let Some(ident @ TokenTree::Ident(_)) =3D = inner_stream.next() { + ident + } else { + panic!("expected argument type") + }; + + assert!( + inner_stream.next().is_none(), + "unexpected token in enum variant" + ); + + Some(arg_type) + } else { + None + }; + + variants.push((variant, arg_type)); + + if stream + .next_if(|t| matches!(t, TokenTree::Punct(punct) if punct.as_c= har() =3D=3D ',')) + .is_none() + { + break; + } + } + + assert!(stream.next().is_none(), "unexpected token in enum body"); + + variants +} + +pub(crate) fn derive(input: TokenStream) -> TokenStream { + let mut input =3D input.into_iter(); + + let (code, start_num) =3D parse_attribute(&mut input); + let enum_name =3D parse_enum_def(&mut input); + let variants =3D parse_enum_body(&mut input); + + assert!(input.next().is_none(), "unexpected token in ioctl_cmd"); + + let code =3D TokenTree::from(Literal::u8_suffixed(code)); + + let variants =3D variants + .into_iter() + .enumerate() + .map(|(i, (variant, arg_type))| { + let i =3D i as u8 + start_num; + + let i =3D TokenTree::from(Literal::u8_suffixed(i)); + + if let Some(arg_type) =3D arg_type { + quote! { + @variant(#i, #variant, #arg_type), + } + } else { + quote! { + @variant(#i, #variant, None), + } + } + }); + + quote! { + ::kernel::__derive_ioctl_cmd!( + parse_input: + @enum_name(#enum_name), + @code(#code), + @variants(#(#variants)*) + ); + } +} diff --git a/rust/macros/lib.rs b/rust/macros/lib.rs index a626b1145e5c4ff00692e9d4e11fdb93500db1a8..5a33ed69b5b0b64f6720fb54e18= 056af9b2f7a00 100644 --- a/rust/macros/lib.rs +++ b/rust/macros/lib.rs @@ -10,6 +10,7 @@ mod quote; mod concat_idents; mod helpers; +mod ioctl_cmd; mod module; mod paste; mod pin_data; @@ -412,6 +413,26 @@ pub fn paste(input: TokenStream) -> TokenStream { tokens.into_iter().collect() } =20 +/// Derives the [`IoctlCommand`] trait for the given enum. +/// +/// # Example +/// +/// ``` +/// #[derive(IoctlCommand)] +/// #[ioctl(code =3D 0x18, start_num =3D 0)] +/// enum Command { +/// NoReadWrite, // No read or write access. +/// NoReadWriteButTakesArg(u64), // No read or write access, but takes= an argument. +/// ReadOnly(UserSliceWriter), // We write data for the user to read. +/// WriteOnly(UserSliceReader), // We read data that the user wrote. +/// WriteAndRead(UserSlice), // We read data from the user and the= n write data to the user. +/// } +/// ``` +#[proc_macro_derive(IoctlCommand, attributes(ioctl))] +pub fn derive_ioctl_cmd(input: TokenStream) -> TokenStream { + ioctl_cmd::derive(input) +} + /// Derives the [`Zeroable`] trait for the given struct. /// /// This can only be used for structs where every field implements the [`Z= eroable`] trait. --=20 2.47.0 From nobody Wed Nov 27 07:35:02 2024 Received: from dd3514.kasserver.com (dd3514.kasserver.com [85.13.129.232]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id F3BC71CEE86; Fri, 11 Oct 2024 18:58:14 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=85.13.129.232 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1728673097; cv=none; b=o/NO211uL9FV4m7+Q0yJohsP/cr/Wopx6Dij6TQbryV2hnXEwRpTIhRJAZypOktdFnkOkhW4pX9CrVs6uRO2Q2aTGNYNpwABagbDd9q1qvKiKc3vPbNRZuV7Q4qIzmNKv5sG55R05CAXGRbEFX5tqA02khajRMzW6Lv40Uql5GY= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1728673097; c=relaxed/simple; bh=8ijGK545wuqmu/N6m4EmYX5SB8wiE9GzZuk86berweg=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=RXubkaWfqt4CzOcFVcWRfEA/5BYtCsBKIpJafSjLYvUx4GeschTop101SoGpWGn1gkb+DdgUSROv+NdgqoLlwOYbsWKKvpEEwnkDTJMj0aRT8hGxVEhokXNg68uchQoCRgBYrZl5gBHqIT0EK3Rb9traUSzrGWWopjKAWIXSWZM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=walterzollerpiano.com; spf=pass smtp.mailfrom=walterzollerpiano.com; dkim=pass (2048-bit key) header.d=walterzollerpiano.com header.i=@walterzollerpiano.com header.b=MJpK9i+9; arc=none smtp.client-ip=85.13.129.232 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=walterzollerpiano.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=walterzollerpiano.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=walterzollerpiano.com header.i=@walterzollerpiano.com header.b="MJpK9i+9" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=walterzollerpiano.com; s=kas202409051210; t=1728673045; bh=Ei9FKHJhHqwmO4iRm6y9B6f2oefMSCnpI34mYCLehig=; h=From:Date:Subject:References:In-Reply-To:To:Cc:From; b=MJpK9i+9gOC/yzsqW0VoS+TR/bGaisii+oeF8v1FH0CGn3r8b1lkxHgqzjWbYY8rg d83+jI04fO0IZSryQC0Mhpkn7UIbgoCQryugRYLjjtnPgMeDm4fYpDzvbQqSiOfT31 kiSu4WXvgZaadLzi8Pp38LdzAsr/bm1oAvjCRXvWlnFTUvP9+Lr06uIKZORm4M5L22 AuIW+RrfHByQK9bv3ARC479vSXp5bOqf9DwF5oSiZyYCfs0+VsIIuXnlHe2/KMdDo7 Y4uWDzpKY3eUUiiF/FzzAs5FksY3l6E27Gp/BiP/wMaMW8+pmDy0eztVwl8VXdpX3K yhbnzOyDb2qnw== Received: from [127.0.0.1] (31-10-158-205.cgn.dynamic.upc.ch [31.10.158.205]) by dd3514.kasserver.com (Postfix) with ESMTPSA id AFF72103AE1; Fri, 11 Oct 2024 20:57:24 +0200 (CEST) From: Josef Zoller Date: Fri, 11 Oct 2024 20:55:44 +0200 Subject: [PATCH 3/3] samples: rust: add character device sample Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20241011-rust-char-dev-v1-3-350225ae228b@walterzollerpiano.com> References: <20241011-rust-char-dev-v1-0-350225ae228b@walterzollerpiano.com> In-Reply-To: <20241011-rust-char-dev-v1-0-350225ae228b@walterzollerpiano.com> To: Arnd Bergmann , Greg Kroah-Hartman , Miguel Ojeda , Alex Gaynor Cc: Boqun Feng , Gary Guo , =?utf-8?q?Bj=C3=B6rn_Roy_Baron?= , Benno Lossin , Andreas Hindborg , Alice Ryhl , Trevor Gross , linux-kernel@vger.kernel.org, rust-for-linux@vger.kernel.org, Josef Zoller X-Mailer: b4 0.14.2 X-Developer-Signature: v=1; a=openpgp-sha256; l=19610; i=josef@walterzollerpiano.com; h=from:subject:message-id; bh=8ijGK545wuqmu/N6m4EmYX5SB8wiE9GzZuk86berweg=; b=owEBbQKS/ZANAwAIAROd718OBo3IAcsmYgBnCXUSQl1niGmIuyiF7d+TTqzLbzpKqjX4qZhfO JBAAUKKhi6JAjMEAAEIAB0WIQQnwV8chXqnjaC1vcATne9fDgaNyAUCZwl1EgAKCRATne9fDgaN yPKSEACfMR4jVAW5prfKySxV++jGrKiXYiFagJsN0zGYP83QtADij4I4xREJqRYsl5OmCPOn5TP fd5qpk9y6UQI9n2PcL8uzGnUiVVvY6xWYrrt0H06dS6AOvxWKHobkdcCcaHHiE4Kiy3LXChGeZi mLxW0XjVY3Hggkc48w5J4di0cmH4+aTthafVNtsbXb1dchTkoKmRnsIe7VJk4QaX3kJPNp5IVNQ HOhAJSs5Uzuhwy2330Pf70gZz1QItNUxFlUhv8nN0m0BhQerPoGAyRW1nDqWMUwWYAZdTFvAHXz oLUL2uZ/FHlDpZcjpTFhIE6C9N2BGC4OrRwTaxSmIf3p0VR2qszd0MQWYAiN4mJLRrGLPf6MpbR 9ugxzy1udE2UDKUs9h0NExiQX2F2L464UzRkpNBOAJR5GaULJUHJUVOmdoD1tnEVEF02zNEfa73 GU1yz0rYjMfEFxmGyFMX+5kNgPtm7GbFn5R8yTn6U409VK13z6susI9YGnVhcUe4Y/R21fMz3k0 JwWftXtIE5dmAExIyR1LWseqiSCITuofPxfCrkF/4K4/4bR03tMb6zIxZbudZey3iJ02oNG0LDa wC3dqKEd2QkIVNYLdxkv3A9UTOhyhzyscX8Sm/LEvTfxoLbiA6AY8HTxjAa9lqJWy6grxPX6Ol7 DlzCGyqZ5MCCDdA== X-Developer-Key: i=josef@walterzollerpiano.com; a=openpgp; fpr=27C15F1C857AA78DA0B5BDC0139DEF5F0E068DC8 X-Spamd-Bar: +++ Provide a sample implementation of a character device in Rust, following the scull device from the well-known book "Linux Device Drivers". This sample uses the abstractions from the previous patches in this series to implement the scull device in mostly safe Rust. All of the unsafe code comes from the fact that the data structure used in the C implementation is inherently unsafe and cannot easily be represented in safe Rust without a lot of memory overhead. This sample should be a good starting point for people who want to start writing kernel code in Rust, as a character device is relatively simple and does not require a lot of kernel knowledge to understand. Signed-off-by: Josef Zoller --- samples/rust/Kconfig | 10 + samples/rust/Makefile | 1 + samples/rust/rust_char_dev.rs | 506 ++++++++++++++++++++++++++++++++++++++= ++++ 3 files changed, 517 insertions(+) diff --git a/samples/rust/Kconfig b/samples/rust/Kconfig index b0f74a81c8f9ad24c9dc1ca057f83531156084aa..a85e0c2e84c07ee9c0b965eb83c= 07a90011e7d0d 100644 --- a/samples/rust/Kconfig +++ b/samples/rust/Kconfig @@ -30,6 +30,16 @@ config SAMPLE_RUST_PRINT =20 If unsure, say N. =20 +config SAMPLE_RUST_CHAR_DEV + tristate "Character device" + help + This option builds the Rust character device module sample. + + To compile this as a module, choose M here: + the module will be called rust_char_dev. + + If unsure, say N. + config SAMPLE_RUST_HOSTPROGS bool "Host programs" help diff --git a/samples/rust/Makefile b/samples/rust/Makefile index 03086dabbea44f4aa87f4a67ac24b8ea4e5a8f2a..cea054d71a7121a2ad991bd51b0= d72caafe1d86e 100644 --- a/samples/rust/Makefile +++ b/samples/rust/Makefile @@ -2,5 +2,6 @@ =20 obj-$(CONFIG_SAMPLE_RUST_MINIMAL) +=3D rust_minimal.o obj-$(CONFIG_SAMPLE_RUST_PRINT) +=3D rust_print.o +obj-$(CONFIG_SAMPLE_RUST_CHAR_DEV) +=3D rust_char_dev.o =20 subdir-$(CONFIG_SAMPLE_RUST_HOSTPROGS) +=3D hostprogs diff --git a/samples/rust/rust_char_dev.rs b/samples/rust/rust_char_dev.rs new file mode 100644 index 0000000000000000000000000000000000000000..8df83fb31bced0870b43416d9a8= dbd1d4cd118d1 --- /dev/null +++ b/samples/rust/rust_char_dev.rs @@ -0,0 +1,506 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! Rust character device sample. +//! +//! This sample demonstrates how to create a simple character device in Ru= st, +//! by reimplementing the `scull` device from the Linux Device Drivers boo= k. + +use core::{mem, ptr::NonNull}; + +use kernel::{ + c_str, + char_dev::{CharDevice, CharDeviceID, DeviceRegistration, OpenCharDevic= e, Whence}, + fs::{file::flags, File, LocalFile}, + new_mutex, + prelude::*, + sync::{Arc, Mutex}, + uaccess::{UserSlice, UserSliceReader, UserSliceWriter}, +}; + +module! { + type: RustCharDevModule, + name: "rust_char_dev", + author: "Rust for Linux Contributors", + description: "Rust character device sample", + license: "GPL", +} + +const DEVICE_NAME: &CStr =3D c_str!("rust_scull"); +const DEFAULT_QSET_SIZE: usize =3D 1000; +const DEFAULT_QUANTUM_SIZE: usize =3D 4000; +const NUM_DEVS: usize =3D 4; + +// This is probably too specific a function to be in the Rust standard lib= rary... +trait OptionExt { + fn get_or_try_insert_with(&mut self, f: F) -> Result<&mut T, E> + where + F: FnOnce() -> Result; +} + +impl OptionExt for Option { + fn get_or_try_insert_with(&mut self, f: F) -> Result<&mut T, E> + where + F: FnOnce() -> Result, + { + if self.is_none() { + *self =3D Some(f()?); + } + + Ok(self.as_mut().unwrap()) + } +} + +#[derive(IoctlCommand)] +#[ioctl(code =3D b'k')] +enum Command { + Reset, // 0 + SetQuantum(UserSliceReader), // 1 + SetQset(UserSliceReader), // 2 + TellQuantum(u64), // 3 + TellQset(u64), // 4 + GetQuantum(UserSliceWriter), // 5 + GetQset(UserSliceWriter), // 6 + QueryQuantum, // 7 + QueryQset, // 8 + ExchangeQuantum(UserSlice), // 9 + ExchangeQset(UserSlice), // 10 + ShiftQuantum(u64), // 11 + ShiftQset(u64), // 12 +} + +// We implement `ScullQset` as close as possible to the `struct scull_qset= ` implementation from +// the book. This means that we have to use raw pointers and some unsafe c= ode for the data. +// Otherwise, we'd have massive memory overhead by storing sizes/capacitie= s unnecessarily. +// E.g. every `ScullQset` would be 8 * qset_size bytes larger if we used `= Box<[_]>` and +// 16 * qset_size bytes larger if we used `Vec<_>`. +// However, the knowledge that all data arrays are of the same size isn't = possible to express +// in safe Rust without this memory overhead. +struct ScullQset { + data: Option>>>, + quantum_size: usize, + qset_size: usize, + next: Option>, +} + +impl ScullQset { + fn new(quantum_size: usize, qset_size: usize) -> Self { + Self { + data: None, + quantum_size, + qset_size, + next: None, + } + } + + /// Returns a reference to the quantum at index `i` if it exists. + fn get_quantum(&self, i: usize) -> Option<&[u8]> { + let data_slice =3D NonNull::slice_from_raw_parts(self.data?, self.= qset_size); + // SAFETY: `data_slice` points to a valid slice of `Option>`. + let quantum =3D unsafe { data_slice.as_ref()[i] }; + let quantum_slice =3D NonNull::slice_from_raw_parts(quantum?, self= .quantum_size); + // SAFETY: `quantum_slice` points to a valid slice of `u8`. + Some(unsafe { quantum_slice.as_ref() }) + } + + /// Returns a mutable reference to the quantum at index `i`, allocatin= g it first if necessary. + fn get_quantum_mut(&mut self, i: usize) -> Option<&mut [u8]> { + let data =3D self + .data + .get_or_try_insert_with(|| { + let mut data =3D + mem::ManuallyDrop::new(Vec::with_capacity(self.qset_si= ze, GFP_KERNEL)?); + for _ in 0..self.qset_size { + data.push(None, GFP_KERNEL)?; + } + + assert!(data.len() =3D=3D data.capacity()); + + // SAFETY: `data.as_mut_ptr()` is non-null. + Ok::<_, Error>(unsafe { NonNull::new_unchecked(data.as_mut= _ptr()) }) + }) + .ok()?; + + let mut data_slice =3D NonNull::slice_from_raw_parts(*data, self.q= set_size); + + // SAFETY: `data_slice` points to a valid slice of `Option>`. + let maybe_quantum =3D unsafe { &mut data_slice.as_mut()[i] }; + let quantum =3D maybe_quantum + .get_or_try_insert_with(|| { + let mut quantum =3D + mem::ManuallyDrop::new(Vec::with_capacity(self.quantum= _size, GFP_KERNEL)?); + for _ in 0..self.quantum_size { + quantum.push(0, GFP_KERNEL)?; + } + + assert!(quantum.len() =3D=3D quantum.capacity()); + + // SAFETY: `quantum.as_mut_ptr()` is non-null. + Ok::<_, Error>(unsafe { NonNull::new_unchecked(quantum.as_= mut_ptr()) }) + }) + .ok()?; + + let mut quantum_slice =3D NonNull::slice_from_raw_parts(*quantum, = self.quantum_size); + // SAFETY: `quantum_slice` points to a valid slice of `u8`. + Some(unsafe { quantum_slice.as_mut() }) + } +} + +impl Drop for ScullQset { + fn drop(&mut self) { + if let Some(data) =3D self.data.take() { + // SAFETY: `data` was created by `Vec::with_capacity` with a c= apacity of `qset_size`. + let data_vec =3D + unsafe { Vec::from_raw_parts(data.as_ptr(), self.qset_size= , self.qset_size) }; + + for quantum in data_vec { + let Some(quantum) =3D quantum else { continue }; + + // SAFETY: `quantum` was created by `Vec::with_capacity` w= ith a capacity of + // `quantum_size`. + let _ =3D unsafe { + Vec::from_raw_parts(quantum.as_ptr(), self.quantum_siz= e, self.quantum_size) + }; + } + } + } +} + +// SAFETY: The raw pointers are uniquely owned by `ScullQset` and not shar= ed, so it's safe to send +// it to another thread. +unsafe impl Send for ScullQset {} + +struct ScullDevInner { + data: Option>, + quantum_size: usize, + qset_size: usize, + size: usize, +} + +impl Default for ScullDevInner { + fn default() -> Self { + Self { + data: None, + quantum_size: DEFAULT_QUANTUM_SIZE, + qset_size: DEFAULT_QSET_SIZE, + size: 0, + } + } +} + +impl ScullDevInner { + fn trim(&mut self) { + mem::take(&mut self.data); + self.size =3D 0; + } + + fn follow(&mut self, n: usize) -> Option<&mut ScullQset> { + let mut qs =3D self + .data + .get_or_try_insert_with(|| { + Box::new( + ScullQset::new(self.quantum_size, self.qset_size), + GFP_KERNEL, + ) + }) + .ok()?; + + for _ in 0..n { + qs =3D qs + .next + .get_or_try_insert_with(|| { + // We use `qs.quantum_size` and `qs.qset_size` here to= avoid subtly + // different behavior from the original C implementati= on. + // If we used the sizes from `self`, we could end up w= ith differently + // sized qsets in the linked list (which would not be = a safety problem). + // Like this, we only use an updated size after `trim`= has been called, + // which is the same behavior as in the book. + Box::new(ScullQset::new(qs.quantum_size, qs.qset_size)= , GFP_KERNEL) + }) + .ok()?; + } + + Some(qs) + } +} + +#[derive(Clone)] +struct ScullDev { + inner: Arc>, +} + +#[vtable] +impl CharDevice for ScullDev { + type OpenPtr =3D Box; + type Err =3D Error; + + fn new(_dev_id: CharDeviceID) -> Result { + Ok(Self { + inner: Arc::pin_init(new_mutex!(ScullDevInner::default()), GFP= _KERNEL)?, + }) + } + + fn open(&self, file: &File) -> Result { + if file.flags() & flags::O_ACCMODE =3D=3D flags::O_WRONLY { + // TODO: this should be lock_interruptible, but that's not in = the Rust API yet + self.inner.lock().trim(); + } + + Ok(Box::new(self.clone(), GFP_KERNEL)?) + } +} + +#[vtable] +impl OpenCharDevice for ScullDev { + type IoctlCmd =3D Command; + type Err =3D Error; + + fn read(&self, _file: &LocalFile, mut buf: UserSliceWriter, offset: &m= ut i64) -> Result { + let pos =3D usize::try_from(*offset).map_err(|_| EINVAL)?; + + // TODO: this should be lock_interruptible, but that's not in the = Rust API yet + let mut inner =3D self.inner.lock(); + + // To keep the behavior of the original C implementation, namely t= hat the quantum and qset + // sizes are only updated after a trim, we use the sizes from the = inner data if it exists. + let (quantum_size, qset_size) =3D inner + .data + .as_ref() + .map_or((inner.quantum_size, inner.qset_size), |qs| { + (qs.quantum_size, qs.qset_size) + }); + let item_size =3D quantum_size * qset_size; + + if pos >=3D inner.size { + return Ok(0); + } + + let mut count =3D buf.len().min(inner.size - pos); + let item =3D pos / item_size; + let rest =3D pos % item_size; + let s_pos =3D rest / quantum_size; + let q_pos =3D rest % quantum_size; + + let Some(q) =3D inner.follow(item).and_then(|qs| qs.get_quantum(s_= pos)) else { + return Ok(0); + }; + + count =3D count.min(quantum_size - q_pos); + + buf.write_slice(&q[q_pos..q_pos + count])?; + + *offset +=3D count as i64; + + Ok(count) + } + + fn write( + &self, + _file: &LocalFile, + mut buf: UserSliceReader, + offset: &mut i64, + ) -> Result { + let pos =3D usize::try_from(*offset).map_err(|_| EINVAL)?; + + // TODO: this should be lock_interruptible, but that's not in the = Rust API yet + let mut inner =3D self.inner.lock(); + + // To keep the behavior of the original C implementation, namely t= hat the quantum and qset + // sizes are only updated after a trim, we use the sizes from the = inner data if it exists. + let (quantum_size, qset_size) =3D inner + .data + .as_ref() + .map_or((inner.quantum_size, inner.qset_size), |qs| { + (qs.quantum_size, qs.qset_size) + }); + let item_size =3D quantum_size * qset_size; + + let item =3D pos / item_size; + let rest =3D pos % item_size; + let s_pos =3D rest / quantum_size; + let q_pos =3D rest % quantum_size; + + let Some(q) =3D inner.follow(item).and_then(|qs| qs.get_quantum_mu= t(s_pos)) else { + return Err(ENOMEM); + }; + + let count =3D buf.len().min(quantum_size - q_pos); + + buf.read_slice(&mut q[q_pos..q_pos + count])?; + + let new_pos =3D pos + count; + *offset =3D new_pos as i64; + + if new_pos > inner.size { + inner.size =3D new_pos; + } + + Ok(count) + } + + fn ioctl( + &self, + _file: &File, + cmd: Self::IoctlCmd, + #[cfg(CONFIG_COMPAT)] _compat: bool, + ) -> Result { + // The original implementation from the book actually doesn't cons= ider the lock here at all, + // but Rust forces us to do so :) + let mut inner =3D self.inner.lock(); + + // We should definitely check if the user is trying to set a size = to 0, or we'll + // end up with panics in the read/write functions due to division = by zero. + // However, the original implementation doesn't account for this, = so we won't either. + match cmd { + Command::Reset =3D> { + inner.quantum_size =3D DEFAULT_QUANTUM_SIZE; + inner.qset_size =3D DEFAULT_QSET_SIZE; + } + Command::SetQuantum(mut reader) =3D> { + // TODO: guard this command (and all others where a size c= an be set by the user) + // with `capability(CAP_SYS_ADMIN)`, which is not yet poss= ible in the Rust API. + let quantum_size =3D reader.read()?; + + if !reader.is_empty() { + return Err(EINVAL); + } + + inner.quantum_size =3D quantum_size; + } + Command::TellQuantum(quantum) =3D> { + inner.quantum_size =3D quantum as usize; + } + Command::GetQuantum(mut writer) =3D> { + writer.write(&inner.quantum_size)?; + + if !writer.is_empty() { + return Err(EINVAL); + } + } + Command::QueryQuantum =3D> { + return Ok(inner.quantum_size as u64); + } + Command::ExchangeQuantum(slice) =3D> { + let (mut reader, mut writer) =3D slice.reader_writer(); + let quantum_size =3D reader.read()?; + + if !reader.is_empty() { + return Err(EINVAL); + } + + writer.write(&inner.quantum_size)?; + + inner.quantum_size =3D quantum_size; + } + Command::ShiftQuantum(quantum) =3D> { + let old_quantum =3D inner.quantum_size; + inner.quantum_size =3D quantum as usize; + return Ok(old_quantum as u64); + } + Command::SetQset(mut reader) =3D> { + let qset_size =3D reader.read()?; + + if !reader.is_empty() { + return Err(EINVAL); + } + + inner.qset_size =3D qset_size; + } + Command::TellQset(qset) =3D> { + inner.qset_size =3D qset as usize; + } + Command::GetQset(mut writer) =3D> { + writer.write(&inner.qset_size)?; + + if !writer.is_empty() { + return Err(EINVAL); + } + } + Command::QueryQset =3D> { + return Ok(inner.qset_size as u64); + } + Command::ExchangeQset(slice) =3D> { + let (mut reader, mut writer) =3D slice.reader_writer(); + let qset_size =3D reader.read()?; + + if !reader.is_empty() { + return Err(EINVAL); + } + + writer.write(&inner.qset_size)?; + + inner.qset_size =3D qset_size; + } + Command::ShiftQset(qset) =3D> { + let old_qset =3D inner.qset_size; + inner.qset_size =3D qset as usize; + return Ok(old_qset as u64); + } + } + + Ok(0) + } + + fn llseek( + &self, + _file: &LocalFile, + pos: &mut i64, + offset: i64, + whence: Whence, + ) -> Result { + let size =3D self.inner.lock().size as i64; + + let new_offset =3D match whence { + Whence::Set =3D> offset, + Whence::Cur =3D> *pos + offset, + Whence::End =3D> size + offset, + _ =3D> return Err(EINVAL), + }; + + if new_offset < 0 { + return Err(EINVAL); + } + + *pos =3D new_offset; + + Ok(new_offset as u64) + } +} + +struct RustCharDevModule { + reg: Pin>>, +} + +impl kernel::Module for RustCharDevModule { + fn init(module: &'static ThisModule) -> Result { + pr_info!("Rust character device sample (init)\n"); + + let reg =3D Box::pin_init( + DeviceRegistration::register(module, DEVICE_NAME), + GFP_KERNEL, + )?; + + let base_dev_id =3D reg.get_base_dev_id(); + pr_info!( + "Registered device {DEVICE_NAME} with major {} and minors {} t= hrough {}\n", + base_dev_id.major(), + base_dev_id.minor(), + base_dev_id.minor() + NUM_DEVS as u32 - 1 + ); + + Ok(RustCharDevModule { reg }) + } +} + +impl Drop for RustCharDevModule { + fn drop(&mut self) { + let dev_id =3D self.reg.get_base_dev_id(); + pr_info!( + "Device {DEVICE_NAME} had major {} and minors {} through {}\n", + dev_id.major(), + dev_id.minor(), + dev_id.minor() + NUM_DEVS as u32 - 1 + ); + + pr_info!("Rust character device sample (exit)\n"); + } +} --=20 2.47.0