Module nix::sys::ioctl

source ·
Expand description

Provide helpers for making ioctl system calls.

This library is pretty low-level and messy. ioctl is not fun.

What is an ioctl?

The ioctl syscall is the grab-bag syscall on POSIX systems. Don’t want to add a new syscall? Make it an ioctl! ioctl refers to both the syscall, and the commands that can be sent with it. ioctl stands for “IO control”, and the commands are always sent to a file descriptor.

It is common to see ioctls used for the following purposes:

  • Provide read/write access to out-of-band data related to a device such as configuration (for instance, setting serial port options)
  • Provide a mechanism for performing full-duplex data transfers (for instance, xfer on SPI devices).
  • Provide access to control functions on a device (for example, on Linux you can send commands like pause, resume, and eject to the CDROM device.
  • Do whatever else the device driver creator thought made most sense.

ioctls are synchronous system calls and are similar to read and write calls in that regard. They operate on file descriptors and have an identifier that specifies what the ioctl is. Additionally they may read or write data and therefore need to pass along a data pointer. Besides the semantics of the ioctls being confusing, the generation of this identifer can also be difficult.

Historically ioctl numbers were arbitrary hard-coded values. In Linux (before 2.6) and some unices this has changed to a more-ordered system where the ioctl numbers are partitioned into subcomponents (For linux this is documented in Documentation/ioctl/ioctl-number.rst):

  • Number: The actual ioctl ID
  • Type: A grouping of ioctls for a common purpose or driver
  • Size: The size in bytes of the data that will be transferred
  • Direction: Whether there is any data and if it’s read, write, or both

Newer drivers should not generate complete integer identifiers for their ioctls instead preferring to use the 4 components above to generate the final ioctl identifier. Because of how old ioctls are, however, there are many hard-coded ioctl identifiers. These are commonly referred to as “bad” in ioctl documentation.

Defining ioctls

This library provides several ioctl_*! macros for binding ioctls. These generate public unsafe functions that can then be used for calling the ioctl. This macro has a few different ways it can be used depending on the specific ioctl you’re working with.

A simple ioctl is SPI_IOC_RD_MODE. This ioctl works with the SPI interface on Linux. This specific ioctl reads the mode of the SPI device as a u8. It’s declared in /include/uapi/linux/spi/spidev.h as _IOR(SPI_IOC_MAGIC, 1, __u8). Since it uses the _IOR macro, we know it’s a read ioctl and can use the ioctl_read! macro as follows:

const SPI_IOC_MAGIC: u8 = b'k'; // Defined in linux/spi/spidev.h
const SPI_IOC_TYPE_MODE: u8 = 1;
ioctl_read!(spi_read_mode, SPI_IOC_MAGIC, SPI_IOC_TYPE_MODE, u8);

This generates the function:

pub unsafe fn spi_read_mode(fd: c_int, data: *mut u8) -> Result<c_int> {
    let res = libc::ioctl(fd, request_code_read!(SPI_IOC_MAGIC, SPI_IOC_TYPE_MODE, mem::size_of::<u8>()), data);
    Errno::result(res)
}

The return value for the wrapper functions generated by the ioctl_*! macros are nix::Errors. These are generated by assuming the return value of the ioctl is -1 on error and everything else is a valid return value. If this is not the case, Result::map can be used to map some of the range of “good” values (-Inf..-2, 0..Inf) into a smaller range in a helper function.

Writing ioctls generally use pointers as their data source and these should use the ioctl_write_ptr!. But in some cases an int is passed directly. For these ioctls use the ioctl_write_int! macro. This variant does not take a type as the last argument:

const HCI_IOC_MAGIC: u8 = b'k';
const HCI_IOC_HCIDEVUP: u8 = 1;
ioctl_write_int!(hci_dev_up, HCI_IOC_MAGIC, HCI_IOC_HCIDEVUP);

Some ioctls don’t transfer any data, and those should use ioctl_none!. This macro doesn’t take a type and so it is declared similar to the write_int variant shown above.

The mode for a given ioctl should be clear from the documentation if it has good documentation. Otherwise it will be clear based on the macro used to generate the ioctl number where _IO, _IOR, _IOW, and _IOWR map to “none”, “read”, “write_*”, and “readwrite” respectively. To determine the specific write_ variant to use you’ll need to find what the argument type is supposed to be. If it’s an int, then write_int should be used, otherwise it should be a pointer and write_ptr should be used. On Linux the ioctl_list man page describes a large number of ioctls and describes their argument data type.

Using “bad” ioctls

As mentioned earlier, there are many old ioctls that do not use the newer method of generating ioctl numbers and instead use hardcoded values. These can be used with the ioctl_*_bad! macros. This naming comes from the Linux kernel which refers to these ioctls as “bad”. These are a different variant as they bypass calling the macro that generates the ioctl number and instead use the defined value directly.

For example the TCGETS ioctl reads a termios data structure for a given file descriptor. It’s defined as 0x5401 in ioctls.h on Linux and can be implemented as:

ioctl_read_bad!(tcgets, TCGETS, termios);

The generated function has the same form as that generated by ioctl_read!:

pub unsafe fn tcgets(fd: c_int, data: *mut termios) -> Result<c_int>;

Working with Arrays

Some ioctls work with entire arrays of elements. These are supported by the ioctl_*_buf family of macros: ioctl_read_buf, ioctl_write_buf, and ioctl_readwrite_buf. Note that there are no “bad” versions for working with buffers. The generated functions include a len argument to specify the number of elements (where the type of each element is specified in the macro).

Again looking to the SPI ioctls on Linux for an example, there is a SPI_IOC_MESSAGE ioctl that queues up multiple SPI messages by writing an entire array of spi_ioc_transfer structs. linux/spi/spidev.h defines a macro to calculate the ioctl number like:

#define SPI_IOC_MAGIC 'k'
#define SPI_MSGSIZE(N) ...
#define SPI_IOC_MESSAGE(N) _IOW(SPI_IOC_MAGIC, 0, char[SPI_MSGSIZE(N)])

The SPI_MSGSIZE(N) calculation is already handled by the ioctl_*! macros, so all that’s needed to define this ioctl is:

const SPI_IOC_MAGIC: u8 = b'k'; // Defined in linux/spi/spidev.h
const SPI_IOC_TYPE_MESSAGE: u8 = 0;
ioctl_write_buf!(spi_transfer, SPI_IOC_MAGIC, SPI_IOC_TYPE_MESSAGE, spi_ioc_transfer);

This generates a function like:

pub unsafe fn spi_message(fd: c_int, data: &mut [spi_ioc_transfer]) -> Result<c_int> {
    let res = libc::ioctl(fd,
                          request_code_write!(SPI_IOC_MAGIC, SPI_IOC_TYPE_MESSAGE, data.len() * mem::size_of::<spi_ioc_transfer>()),
                          data);
    Errno::result(res)
}

Finding ioctl Documentation

For Linux, look at your system’s headers. For example, /usr/include/linux/input.h has a lot of lines defining macros which use _IO, _IOR, _IOW, _IOC, and _IOWR. Some ioctls are documented directly in the headers defining their constants, but others have more extensive documentation in man pages (like termios’ ioctls which are in tty_ioctl(4)).

Documenting the Generated Functions

In many cases, users will wish for the functions generated by the ioctl macro to be public and documented. For this reason, the generated functions are public by default. If you wish to hide the ioctl, you will need to put them in a private module.

For documentation, it is possible to use doc comments inside the ioctl_*! macros. Here is an example :

ioctl_read! {
    /// Make the given terminal the controlling terminal of the calling process. The calling
    /// process must be a session leader and not have a controlling terminal already. If the
    /// terminal is already the controlling terminal of a different session group then the
    /// ioctl will fail with **EPERM**, unless the caller is root (more precisely: has the
    /// **CAP_SYS_ADMIN** capability) and arg equals 1, in which case the terminal is stolen
    /// and all processes that had it as controlling terminal lose it.
    tiocsctty, b't', 19, c_int
}

Macros