From 99fefb7adf1bfeda7e24991fdff0a0ae0843d387 Mon Sep 17 00:00:00 2001 From: Ruihan Li Date: Thu, 4 Dec 2025 11:50:06 +0800 Subject: [PATCH] Implement new ioctl infrastructure --- kernel/src/fs/utils/ioctl.rs | 75 ----- kernel/src/fs/utils/mod.rs | 2 - kernel/src/util/ioctl/mod.rs | 473 ++++++++++++++++++++++++++++++++ kernel/src/util/ioctl/sealed.rs | 69 +++++ kernel/src/util/mod.rs | 1 + rustfmt.toml | 2 +- 6 files changed, 544 insertions(+), 78 deletions(-) delete mode 100644 kernel/src/fs/utils/ioctl.rs create mode 100644 kernel/src/util/ioctl/mod.rs create mode 100644 kernel/src/util/ioctl/sealed.rs diff --git a/kernel/src/fs/utils/ioctl.rs b/kernel/src/fs/utils/ioctl.rs deleted file mode 100644 index bdfaa6681..000000000 --- a/kernel/src/fs/utils/ioctl.rs +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -use crate::prelude::*; - -#[expect(clippy::upper_case_acronyms)] -#[repr(u32)] -#[derive(Debug, Clone, Copy, TryFromInt)] -pub enum IoctlCmd { - /// Get terminal attributes - TCGETS = 0x5401, - /// Set terminal attributes - TCSETS = 0x5402, - /// Drain the output buffer and set attributes - TCSETSW = 0x5403, - /// Drain the output buffer, and discard pending input, and set attributes - TCSETSF = 0x5404, - /// Make the given terminal the controlling terminal of the calling process. - TIOCSCTTY = 0x540e, - /// Get the process group ID of the foreground process group on this terminal - TIOCGPGRP = 0x540f, - /// Set the foreground process group ID of this terminal. - TIOCSPGRP = 0x5410, - /// Get the number of bytes in the input buffer. - FIONREAD = 0x541B, - /// Get window size - TIOCGWINSZ = 0x5413, - /// Set window size - TIOCSWINSZ = 0x5414, - /// Enable or disable non-blocking I/O mode. - FIONBIO = 0x5421, - /// The calling process gives up this controlling terminal - TIOCNOTTY = 0x5422, - /// Return the session ID of FD - TIOCGSID = 0x5429, - /// Clear the close on exec flag on a file descriptor - FIONCLEX = 0x5450, - /// Set the close on exec flag on a file descriptor - FIOCLEX = 0x5451, - /// Enable or disable asynchronous I/O mode - FIOASYNC = 0x5452, - /// Get pty index - TIOCGPTN = 0x80045430, - /// Lock/unlock pty - TIOCSPTLCK = 0x40045431, - /// Get pty lock state - TIOCGPTLCK = 0x80045439, - /// Safely open the slave - TIOCGPTPEER = 0x5441, - /// font operations - KDFONTOP = 0x4B72, - /// Get console mode - KDGETMODE = 0x4B3B, - /// Set console mode - KDSETMODE = 0x4B3A, - /// Get keyboard mode - KDGKBMODE = 0x4B44, - /// Set keyboard mode - KDSKBMODE = 0x4B45, - /// Get tdx report using TDCALL - TDXGETREPORT = 0xc4405401, - /// Get variable screen information (resolution, color depth, etc.) - GETVSCREENINFO = 0x4600, - /// Set variable screen information - PUTVSCREENINFO = 0x4601, - /// Get fixed screen information (memory layout, line length, etc.) - GETFSCREENINFO = 0x4602, - /// Get framebuffer color map - GETCMAP = 0x4604, - /// Set framebuffer color map - PUTCMAP = 0x4605, - /// Pan display to show different part of virtual screen - PANDISPLAY = 0x4606, - /// Blank or unblank the framebuffer display - FBIOBLANK = 0x4611, -} diff --git a/kernel/src/fs/utils/mod.rs b/kernel/src/fs/utils/mod.rs index 691ff9eb1..03ba8bc0d 100644 --- a/kernel/src/fs/utils/mod.rs +++ b/kernel/src/fs/utils/mod.rs @@ -17,7 +17,6 @@ pub use inode::{ }; pub use inode_mode::InodeMode; pub(crate) use inode_mode::{chmod, mkmod, perms_to_mask, who_and_perms_to_mask, who_to_mask}; -pub use ioctl::IoctlCmd; pub use open_args::OpenArgs; pub use page_cache::{CachePage, PageCache, PageCacheBackend}; pub use random_test::{generate_random_operation, new_fs_in_memory}; @@ -40,7 +39,6 @@ mod fs; mod id_bitmap; mod inode; mod inode_mode; -mod ioctl; mod open_args; mod page_cache; mod random_test; diff --git a/kernel/src/util/ioctl/mod.rs b/kernel/src/util/ioctl/mod.rs new file mode 100644 index 000000000..4f1be3d59 --- /dev/null +++ b/kernel/src/util/ioctl/mod.rs @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! Decoding and dispatching ioctl commands. +//! +//! When the system call layer handles an ioctl system call, it creates a [`RawIoctl`], which is +//! basically the numeric ioctl command and argument. +//! +//! The component that handles the specific ioctl logic should first convert the [`RawIoctl`] to a +//! strongly typed [`Ioctl`] instance, which decodes the ioctl command and includes type +//! information about the ioctl argument. +//! +//! To achieve this, define type aliases for [`Ioctl`] using the [`ioc`] macro, preferably in a +//! separate module for clarity. At the function that dispatches the ioctl commands, we can +//! glob-import the defined [`Ioctl`] type aliases and use the [`dispatch_ioctl`] macro for the +//! ioctl dispatching. +//! +//! Here is an complete example that demonstrates the basic usage: +//! ``` +//! mod ioctl_defs { +//! use crate::util::ioctl::{ioc, InData, OutData, PassByVal}; +//! +//! // Here we give the code in Linux to provide an intuitive guide on how to use the `ioc` +//! // macro and how it corresponds to the Linux definitions. This is for demonstration +//! // purposes only. For regular code, keeping the reference link below is sufficient. +//! // +//! // Also, note that the line containing the `ioc` macro will not be automatically formatted. +//! // This is a deliberate design so that whitespaces can be manually inserted to vertically +//! // align the `ioc` macro's parameters and improve code readability. +//! +//! // Reference: > +//! +//! // ```c +//! // #define TIOCSCTTY 0x540E +//! // ``` +//! pub(super) type SetControlTty = ioc!(TIOCSCTTY, 0x540E, InData); +//! +//! // ```c +//! // #define TIOCSPTLCK _IOW('T', 0x31, int) /* Lock/unlock Pty */ +//! // ``` +//! pub(super) type SetPtyLock = ioc!(TIOCSPTLCK, b'T', 0x31, InData); +//! +//! // ```c +//! // #define TIOCGPTLCK _IOR('T', 0x39, int) /* Get Pty lock state */ +//! // ``` +//! pub(super) type GetPtyLock = ioc!(TIOCGPTLCK, b'T', 0x39, OutData); +//! } +//! +//! #[derive(Debug, Default)] +//! struct TtyState { +//! is_controlling: bool, +//! is_locked: bool, +//! } +//! +//! impl TtyState { +//! fn ioctl(&mut self, raw_ioctl: RawIoctl) -> Result<()> { +//! use ioctl_defs::*; +//! +//! dispatch_ioctl!(match raw_ioctl { +//! cmd @ SetControlTty => { +//! let _should_steal_tty = cmd.get() == 1; +//! self.is_controlling = true; +//! } +//! cmd @ SetPtyLock => { +//! self.is_locked = cmd.read()? != 0; +//! } +//! cmd @ GetPtyLock => { +//! cmd.write(if self.is_locked { &1 } else { &0 })?; +//! } +//! _ => return_errno_with_message!(Errno::ENOTTY, "the ioctl command is unknown"), +//! }); +//! Ok(()) +//! } +//! } +//! +//! fn fake_tty_ioctl() { +//! let mut state = TtyState::default(); +//! assert!(!state.is_controlling); +//! +//! state.ioctl(RawIoctl::new(0x540E, 0)); +//! assert!(state.is_controlling); +//! } +//! ``` +//! +//! Additionally, we support the following advanced usage: +//! +//! - For [`InOutData`], it is possible to obtain a [`SafePtr`] instance to the underlying data +//! using [`Ioctl::with_data_ptr`]. This allows some structure fields to be treated as input and +//! others as output. `TDX_CMD_GET_REPORT0` is such an example. +//! +//! - For `OutData<[u8]>` (or `InData<[u8]>`), the argument size is encoded in the ioctl command. +//! A [`VmWriter`] (or [`VmReader`]) can be obtained from [`Ioctl::with_writer`] (or +//! [`Ioctl::with_reader`]), which allows for the writing (or reading) of variable-length data. +//! `EVIOCGNAME` is such an example. +//! + +use core::marker::PhantomData; + +use aster_util::safe_ptr::SafePtr; +use sealed::{DataSpec, IoctlCmd, IoctlDir, PtrDataSpec}; + +use crate::{current_userspace, prelude::*}; + +mod sealed; + +/// An ioctl command and its argument in raw form. +#[derive(Clone, Copy, Debug)] +pub struct RawIoctl { + cmd: u32, + arg: usize, +} + +impl RawIoctl { + /// Creates an instance with the given ioctl command and argument. + pub const fn new(cmd: u32, arg: usize) -> Self { + Self { cmd, arg } + } + + /// Returns the ioctl command. + pub const fn cmd(self) -> u32 { + self.cmd + } + + /// Returns the ioctl argument. + pub const fn arg(self) -> usize { + self.arg + } +} + +/// An ioctl command and its argument in strongly typed form. +/// +/// `MAGIC` and `NR` are the Linux "magic" and "number" fields. +/// For legacy commands defined as raw constants, we may set them to the values +/// decoded from the raw number. +/// +/// `IS_MODERN` indicates whether the ioctl is a modern or legacy one. +/// An legacy ioctl uses an arbitrary `u16` value as its number, +/// whereas a modern ioctl adopts a `u32` encoding. +/// +/// `D` is one of [`NoData`], [`InData`], [`OutData`], or [`InOutData`]. +/// It specifies key aspects about the input/output data in the ioctl argument. +pub struct Ioctl { + cmd: IoctlCmd, + arg: usize, + _phantom: PhantomData, +} + +/// Defines an ioctl type. +/// +/// # Legacy encoding +/// +/// For legacy encoding, a 16-bit raw number is specified as the ioctl command. +/// +/// For example, +/// ``` +/// type NoTty = ioc!(TIOCNOTTY, 0x5422, NoData); +/// ``` +/// It is equivalent to: +/// ``` +/// type NoTty = Ioctl<0x54, 0x0E, /* IS_MODERN = */ false, NoData>; +/// ``` +/// +/// # Modern encoding +/// +/// For modern encoding, a magic (type number) and a command ID (sequence number) are specified. +/// These, along with the data direction and size, compose the 32-bit ioctl command. +/// +/// For example, +/// ``` +/// type SetPtyLock = ioc!(TIOCSPTLCK, b'T', 0x31, InData); +/// ``` +/// It is equivalent to: +/// ``` +/// type SetPtyLock = Ioctl>; +/// ``` +macro_rules! ioc { + // Legacy encoding. + ($linux_name:ident, $raw:literal, $data:ty) => { + $crate::util::ioctl::Ioctl::< + { + // MAGIC + $crate::util::ioctl::magic_and_nr_from_cmd($raw).0 + }, + { + // NR + $crate::util::ioctl::magic_and_nr_from_cmd($raw).1 + }, + { + // IS_MODERN + false + }, + $data, + > + }; + // Modern encoding. + ($linux_name:ident, $magic:literal, $nr:literal, $data:ty) => { + $crate::util::ioctl::Ioctl::<{ $magic }, { $nr }, { true }, $data> + }; +} +pub(crate) use ioc; + +/// Extracts the "magic" and "number" fields from an ioctl command. +#[doc(hidden)] +pub const fn magic_and_nr_from_cmd(raw_cmd: u16) -> (u8, u8) { + let cmd = IoctlCmd::new(raw_cmd as u32); + (cmd.magic(), cmd.nr()) +} + +impl + Ioctl +{ + /// Tries to interpret a [`RawIoctl`] as this particular ioctl command. + /// + /// This method succeeds only if the ioctl command matches. Otherwise, this method returns + /// `None`. + pub fn try_from_raw(raw_ioctl: RawIoctl) -> Option { + let cmd = IoctlCmd::new(raw_ioctl.cmd); + + if cmd.magic() != MAGIC { + return None; + } + if cmd.nr() != NR { + return None; + } + + if IS_MODERN { + // For modern encoding, the upper 16 bits should contain the size and direction. + if let Some(size) = D::SIZE + && cmd.size() != size + { + return None; + } + if cmd.dir() != D::DIR { + return None; + } + } else { + // For legacy encoding, the upper 16 bits should be zero. + if cmd.size() != 0 || cmd.dir() != IoctlDir::None { + return None; + } + } + + Some(Self { + cmd, + arg: raw_ioctl.arg, + _phantom: PhantomData, + }) + } +} + +impl + Ioctl +{ + fn with_data_ptr_unchecked_access(&self, f: F) -> R + where + F: for<'a> FnOnce(SafePtr>) -> R, + { + f(SafePtr::new(current_userspace!(), self.arg)) + } +} + +/// No input/output data. +pub struct NoData; + +impl DataSpec for NoData { + const SIZE: Option = Some(0); + const DIR: IoctlDir = IoctlDir::None; +} + +/// Input-only data. +/// +/// `T` describes the data type. +/// `P` describes how the data is passed (by value or by pointer). +pub struct InData(PhantomData, PhantomData

); + +impl DataSpec for InData { + const SIZE: Option = Some(u16_size_of::()); + const DIR: IoctlDir = IoctlDir::Write; +} + +impl PtrDataSpec for InData { + type Pointee = T; +} + +impl + Ioctl> +{ + /// Reads the ioctl argument from userspace. + pub fn read(&self) -> Result { + Ok(self.with_data_ptr_unchecked_access(|ptr| ptr.read())?) + } +} + +macro_rules! impl_get_by_val_for { + { $( $ty:ident )* } => { + $( + impl + Ioctl> + { + /// Gets the ioctl argument. + pub fn get(&self) -> $ty { + self.arg as $ty + } + } + )* + }; +} + +// We can add more types as needed, e.g., `u32`, `i8`. +impl_get_by_val_for! { i32 } + +impl DataSpec for InData<[u8]> { + const SIZE: Option = None; + const DIR: IoctlDir = IoctlDir::Write; +} + +impl + Ioctl> +{ + /// Obtains a [`VmReader`] that can read the dynamically-sized ioctl argument from userspace. + /// + /// The size of the ioctl argument is specified in [`VmReader::remain`]. + #[expect(dead_code)] + pub fn with_reader(&self, f: F) -> Result + where + F: for<'a> FnOnce(VmReader<'a>) -> Result, + { + f(current_userspace!().reader(self.arg, self.cmd.size() as usize)?) + } +} + +/// Output-only data, always passed by pointer. +pub struct OutData(PhantomData); + +impl DataSpec for OutData { + const SIZE: Option = Some(u16_size_of::()); + const DIR: IoctlDir = IoctlDir::Read; +} + +impl PtrDataSpec for OutData { + type Pointee = T; +} + +impl + Ioctl> +{ + /// Writes the ioctl argument to userspace. + pub fn write(&self, val: &T) -> Result<()> { + self.with_data_ptr_unchecked_access(|ptr| ptr.write(val))?; + Ok(()) + } +} + +impl DataSpec for OutData<[u8]> { + const SIZE: Option = None; + const DIR: IoctlDir = IoctlDir::Read; +} + +impl + Ioctl> +{ + /// Obtains a [`VmWriter`] that can write the dynamically-sized ioctl argument to userspace. + /// + /// The size of the ioctl argument is specified in [`VmWriter::avail`]. + #[expect(dead_code)] + pub fn with_writer(&self, f: F) -> Result + where + F: for<'a> FnOnce(VmWriter<'a>) -> Result, + { + f(current_userspace!().writer(self.arg, self.cmd.size() as usize)?) + } +} + +/// Input and output data, passed by pointer to a single object. +pub struct InOutData(PhantomData); + +impl DataSpec for InOutData { + const SIZE: Option = Some(u16_size_of::()); + const DIR: IoctlDir = IoctlDir::ReadWrite; +} + +impl PtrDataSpec for InOutData { + type Pointee = T; +} + +impl + Ioctl> +{ + /// Reads the ioctl argument from userspace. + #[expect(dead_code)] + pub fn read(&self) -> Result { + self.with_data_ptr(|ptr| Ok(ptr.read()?)) + } + + /// Writes the ioctl argument to userspace. + pub fn write(&self, val: &T) -> Result<()> { + self.with_data_ptr(|ptr| Ok(ptr.write(val)?)) + } + + /// Obtains a [`SafePtr`] that can access the ioctl argument in userspace. + pub fn with_data_ptr(&self, f: F) -> Result + where + F: for<'a> FnOnce(SafePtr>) -> Result, + { + self.with_data_ptr_unchecked_access(f) + } +} + +/// A marker that denotes the input is passed by value (i.e., encoded in the ioctl argument). +pub enum PassByVal {} +/// A marker that denotes the input is passed by pointer (i.e., pointed to by the ioctl argument). +pub enum PassByPtr {} + +const fn u16_size_of() -> u16 { + let size = size_of::(); + assert!( + size <= u16::MAX as usize, + "the type is too large to fit in an ioctl command" + ); + size as u16 +} + +/// Dispatches ioctl commands. +/// +/// See [the module-level documentation](self) for how to use this macro and the suggested style to +/// use this macro. +macro_rules! dispatch_ioctl { + // An empty match. + ( + match $raw:ident {} + ) => { + () + }; + + // The default branch. + ( + match $raw:ident { + _ => $arm:expr $(,)? + } + ) => { + $arm + }; + + // A branch that matches multiple ioctl commands. + ( + match $raw:ident { + $ty0:ty $(| $ty1:ty)* => $arm:block $(,)? + $($rest:tt)* + } + ) => { + if <$ty0>::try_from_raw($raw).is_some() + $(|| <$ty1>::try_from_raw($raw).is_some())* + { + $arm + } else { + crate::util::ioctl::dispatch_ioctl!(match $raw { $($rest)* }) + } + }; + + // A branch that matches a single ioctl command. + ( + match $raw:ident { + $bind:ident @ $ty:ty => $arm:block $(,)? + $($rest:tt)* + } + ) => { + if let Some($bind) = <$ty>::try_from_raw($raw) { + $arm + } else { + crate::util::ioctl::dispatch_ioctl!(match $raw { $($rest)* }) + } + }; +} +pub(crate) use dispatch_ioctl; diff --git a/kernel/src/util/ioctl/sealed.rs b/kernel/src/util/ioctl/sealed.rs new file mode 100644 index 000000000..e8b0e045c --- /dev/null +++ b/kernel/src/util/ioctl/sealed.rs @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MPL-2.0 + +use int_to_c_enum::TryFromInt; + +/// An ioctl command that follows Linux-style encoding. +/// +/// Layout (from LSB to MSB): +/// - bits 0..=7 : command number (nr) +/// - bits 8..=15 : type / magic +/// - bits 16..=29 : size (in bytes) +/// - bits 30..=31 : direction +/// +/// Reference: +#[derive(Clone, Copy, Debug)] +pub(super) struct IoctlCmd(u32); + +/// The direction of data transfer for an ioctl command. +/// +/// The meaning is from the *user-space* perspective, +/// following Linux's `_IOC_*` macros: +/// - `None` -> `_IOC_NONE` +/// - `Write` -> `_IOC_WRITE` (user to kernel) +/// - `Read` -> `_IOC_READ` (kernel to user) +/// - `ReadWrite` -> `_IOC_READ | _IOC_WRITE` +/// +/// Reference: +#[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromInt)] +#[repr(u8)] +pub enum IoctlDir { + None = 0, + Write = 1, + Read = 2, + ReadWrite = 3, +} + +impl IoctlCmd { + pub(super) const fn new(cmd: u32) -> Self { + Self(cmd) + } + + pub(super) const fn nr(self) -> u8 { + // Bits 0..=7 + self.0 as u8 + } + + pub(super) const fn magic(self) -> u8 { + // Bits 8..=15 + (self.0 >> 8) as u8 + } + + pub(super) const fn size(self) -> u16 { + // Bits 16..=29 + ((self.0 >> 16) as u16) & 0x3FFF + } + + pub(super) fn dir(self) -> IoctlDir { + // Bits 30..=31 + IoctlDir::try_from((self.0 >> 30) as u8).unwrap() + } +} + +pub trait DataSpec { + const SIZE: Option; + const DIR: IoctlDir; +} + +pub trait PtrDataSpec: DataSpec { + type Pointee; +} diff --git a/kernel/src/util/mod.rs b/kernel/src/util/mod.rs index 5161f8814..2da4df738 100644 --- a/kernel/src/util/mod.rs +++ b/kernel/src/util/mod.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MPL-2.0 mod copy_compact; +pub mod ioctl; mod iovec; pub mod net; mod padded; diff --git a/rustfmt.toml b/rustfmt.toml index 6988880dc..33952eb60 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,4 @@ imports_granularity="Crate" group_imports="StdExternalCrate" reorder_imports=true -skip_macro_invocations = ["chmod", "mkmod"] +skip_macro_invocations = ["chmod", "mkmod", "ioc"]