[ blog » 2022 » 09_rust-kernel-module ] [PART 3/3] The Colors of Rust (by Philipp Gesang)
2022-09-04

The final objective for this series is to extend the module code from part two to have an actual effect on the system: change the console palette from the default VGA colors to something fancier. The sysctl interface will remain the mode of interacting with userspace but will gain some functionality in the process.

Here’s the plan for part three: The module, now renamed kvtcol, provides a number of predefined color schemes whose palette is hard-coded. It also installs a sysctl at the path dev/tty/kvtcol/scheme, but instead of a single bit it will hold a string with the name of the currently active scheme. Writing a known scheme name to that sysctl causes the corresponding palette to be applied to the kernel console.

Palettes

First we need to define the palettes we want to expose through our sysctl. For simplicity we just take the schemes already defined by vtcol and add them to the module as schemes.rs. The exported items in that file are qualified with pub(crate) so in our module crate we can access them as crate::schemes::*.

The selection of “color schemes” comprises the light and dark variants of the widely used solarized colors, an all green one emulating classic cathode-ray tubes, and a set of dummy colors that is mainly useful for debugging. The function schemes::get_scheme() picks a scheme by its name, returning a shared reference to the static data.

Mutex Idio-sync-rasies

The goal is for schemes to be selectable by their names so we want to store the name of the current scheme in the sysctl. It was already touched on in part two that in the Rust-for-Linux tree the SysctlStorage trait is implemented only for AtomicBool so we will have to provide an implementation for a string type ourselves. For sake of simplicity we will be using a fixed size buffer as the inner type. That array cannot however be used directly as its contents may be mutated from different threads. Furthermore, the functions in SysctlStorage receive shared references to self, forcing us to wrap the backing array in a type that provides interior mutability.

Looking around the toolkit for a replacement for std::sync, the Rust programmer will be happy to find the kernel::sync namespace where Rust wrappers for various kernel synchronization primitives reside, including of course mutexes. However, to those familiar with std::sync::Mutex the Kernel mutexes seem rather unwieldy:

let mtx = Pin::from(Box::try_new(unsafe {
    kernel::sync::Mutex::new(0)
})?);
mutex_init!(mtx.as_ref(), "some::mutex::name");
*mtx.lock() = 1337;

Constructing it is unsafe, requires pinning and a separate initialization step‽ There’s three good reasons already to stay away from these unless one is forced to by the context, e. g. when interacting with C code that has no choice but to use this clunky API.

As an example for the lack of ergonomics, this is what happens if you violate the API contract of kernel::mutex and forget the initialization step:

# dmesg -W
[  255.269231] kvtcol: loading out-of-tree module taints kernel.
[  255.271270] kvtcol: init
[  255.271487] BUG: unable to handle page fault for address: 000000011674ee20
[  255.271619] #PF: supervisor read access in kernel mode
[  255.271756] #PF: error_code(0x0000) - not-present page
[  255.271905] PGD 0 P4D 0
[  255.271973] Oops: 0000 [#1] PREEMPT SMP
[  255.272089] CPU: 0 PID: 811 Comm: insmod Tainted: G           O      5.19.0+ #12

Oops indeed!

Luckily we don’t need to expose the mutex across the FFI so we have the luxury to use the alternative kernel::smutex::Mutex, the “simple mutex”, whose ergonomics are superior to both the kernel mutex and the one from std::sync. Compared to the latter it does away with poisoning which is generally considered a design flaw of the standard library implementation. The smutex version of above example boils down to

let mtx = kernel::sync::Mutex::new(0);
*mtx.lock() = 1337;

No pinning shenanigans, no calling unwrap() on the guard – this is ideal for our use case. It is worth mentioning that smutex does not incur mandatory boxing which is another advantage over std::mutex.

The Storage

To keep the name of the color scheme in the Sysctl struct a type is required that implements SysctlStorage. To recap, in part two we had to switch the signature of the read_value function to take a kernel pointer argument instead of a UserSlicePtrWriter. After this change the trait looks as follows:

pub trait SysctlStorage: Sync {
    fn store_value(&self, data: &[u8]) -> (usize, Result);
    fn read_value(&self, data: &mut [u8]) -> (usize, Result);
}

Where the usize member of the return value refers to the number of bytes in data that have been processed. For store_value() if that value is less than the number of elements in data, the function will be called again until all of the input is handled. For read_value() it indicates to the caller the number of bytes written to the output buffer. (The buffer is allocated with kvzalloc() so it would be safe to read even in read_value().)

The storage type is defined as follows:

struct Storage { data: Mutex<[u8; 0xff]> }

where 255 is just an arbitrary value large enough to not get in the way of scheme names and small enough we don’t have to worry about things like page size.

When the sysctl is written to, the kernel at some point down the call stack invokes store_value() with data pointing to a kernel buffer that contains a copy of the bytes received from user space:

fn store_value(&self, data: &[u8]) -> (usize, Result)
{
    let progress = data.len();
    let data = trim_whitespace(data);

    let name = match core::str::from_utf8(data) {
        Err(e) => {
            pr_err!("invalid utf8 in scheme name: {}\n", e);
            return (progress, Err(EINVAL));
        },
        Ok(name) => name,
    };

    match set_scheme(name) {
        Err(e) => (progress, Err(e)),
        Ok(()) => {
            match self.data.lock().get_mut(..data.len()) {
                None => return (progress, Err(EINVAL)),
                Some(dst) => dst.copy_from_slice(data),
            }
            (progress, Ok(()))
        },
    }
}

Some preprocessing is necessary in order to ensure the input upholds the invariants imposed on Rust strings. The name is then fed into set_scheme() which is left unimplemented for the time being, and if that worked out, we take the mutex and store the name in the inner buffer.

The read_value() operation is a bit less involved as we keep the original bytes around and thus can return them back as-is:

fn read_value(&self, data: &mut [u8]) -> (usize, Result)
{
    let guard = self.data.lock();
    let stop = guard.iter().position(|&b| b == 0u8).unwrap_or(STORAGE_SIZE);
    let value = &guard[..stop];

    match data.get_mut(..value.len()) {
        None => return (value.len(), Err(EINVAL)),
        Some(dst) => {
            dst.copy_from_slice(&value);
        },
    };

    match data.get_mut(value.len()) {
        None => return (value.len(), Err(EINVAL)),
        Some(terminate) => *terminate = b'\n',
    };

    (value.len() + 1, Ok(()))
}

Again much of the complexity arises from the use of non-panicking methods that would be unnecessary if this were a user space application.

Tying all this together we register the storage with the sysctl in kernel::Module::init():

let sysctl = Sysctl::register(
    c_str!("dev/tty/kvtcol"),
    c_str!("scheme"),
    Storage::new(),
    Mode::from_int(0o644),
)?;

and can now store known scheme names in the sysctl but not unknown ones:

# insmod kvtcol.ko
# printf solarized >/proc/sys/dev/tty/kvtcol/scheme
# cat /proc/sys/dev/tty/kvtcol/scheme
solarized
# printf xyzzy >/proc/sys/dev/tty/kvtcol/scheme
-bash: printf: write error: Invalid argument
# cat /proc/sys/dev/tty/kvtcol/scheme
solarized

The next step will discuss the implementation of the set_scheme() which was left open so far.

Reaching the Color Map

The kernel console implementation lives in drivers/tty/vt/vt.c with some type declarations and prototypes in include/ for use in other parts of the kernel and by user space. The user space routines that vtcol calls into through ioctl(2) live in the accessory file vt_ioctl.c. Taking inventory on what APIs the ioctl utilizes directly or indirectly to manipulate the palette, it boils down to two steps: 1. setting the default palette and 2. applying that palette to all active consoles.

1. The Default Palette

The defaults are kept in three static byte arrays, default_red[], default_grn[] and default_blu[], giving the three components for a color index. These symbols are exported too so we can reach them easily from our module:

extern "C" {
    static mut default_red: [u8; 16];
    static mut default_grn: [u8; 16];
    static mut default_blu: [u8; 16];
}

However since they contain globally shared, static data we can’t just access those without precautions. That is, we need to acquire the console lock in order to manipulate them. To ensure we don’t forget to release the lock after we’re done we mediate all access to the console APIs with a safe wrapper function:

extern "C" {
    pub fn console_lock();
    pub fn console_unlock();
}

fn with_console_lock<T>(cb: &dyn Fn() -> Result<T>) -> Result<T>
{
    unsafe { console_lock() };
    let res = cb();
    unsafe { console_unlock() };
    res
}

Access to the arrays still requires an unsafe block and we’re in the kernel, so we refrain from using core::ops::Index to assign values in favor of the get_mut() method:

match unsafe { (default_red.get_mut(i), default_grn.get_mut(i), default_blu.get_mut(i)) } {
    (Some(pr), Some(pg), Some(pb)) => {
        *pr = r;
        *pg = g;
        *pb = b;
    },
    wtf => return Err(EINVAL),
}

The result may look a little awkward but this roundabout way ensures that the module stays clear of panics due to failed bounds checks. Assigning values from the palette chosen by the user concludes the first step. At this point, the colors will be applied to each new console that gets allocated by the kernel. Already active consoles however are not affected yet, this we will look into in the next step.

2. The Live Consoles

In order to get the new palette applied, let’s look one more time at what the ioctl_console(2) commands do under the hood. When PIO_CMAP is passed to the ioctl syscall, the kernel calls con_set_cmap() which, upon setting the defaults, copyies the RGB components one color at a time into the .vc_palette member of each active console’s vc_data struct. Since con_set_cmap() is not exported, it cannot be used in kvtcol.

The active consoles, however, can be accessed, we just have to define the equivalents in Rust:

const MAX_NR_CONSOLES: usize = 63;

#[repr(C)]
#[allow(non_camel_case_types)]
#[allow(non_snake_case)]
pub struct vc
{
    d:        *mut vc_data,
    SAK_work: bindings::work_struct,
}

extern "C" {
    static mut vc_cons: [vc; MAX_NR_CONSOLES];
}

So obviously what we need to do is reimplement the necessary steps in Rust as we did with the default palette.

However there’s a catch: vc_data is bloody enormous. Under ordinary circumstances that would be a job for bindgen but going down that path got hairy pretty quick. vc_data itself recursively references arch dependent types like synchronization primitives – spinlock_t, struct mutex – whose definition reaches the abyssal zone of the kernel in arch/*/include/asm/ and thus are out of limits for generated bindings. Also we can’t reasonably get away with dirty tricks like skipping irrelevant members before vc_palette with padding, as the offset of that member varies depending on the platform.

It would be way out of scope for this article to implement vc_data properly on the Rust side, so we should take a step back and rethink our approach.

Cheating Our Way to Success

There are a few functions that set the active console palettes: con_set_cmap() which was mentioned above; do_con_trol() which handles control characters; and reset_palette() which really only copies the default palette to a console and then updates the VGA driver. From its signature, reset_palette() seems the ideal API for kvtcol:

void reset_palette (struct vc_data *vc);

A pointer to struct vc_data poses no problem as we don’t need to the internals of the pointee. So we wrap it in a zero-sized newtype:

#[repr(transparent)]
#[allow(non_camel_case_types)]
pub struct vc_data(ffi::c_void);

for use as an opaque pointer *mut vc_data. Unfortunately, like the other two symbols reset_palette() is inaccessible from our module. Again it seems that we have reached an impasse.

Reflecting on our goals, we are targeting a rather experimental kernel and in practice we don’t have to worry about our module failing to compile and load on every mainline-ish kernel out there. So with some hesitation we do what every self-respecting kernel vendor would do in our situation and patch the kernel to export reset_palette().

After recompiling the kernel with that line added, we can now iterate the active consoles and offload the job of applying the default scheme to reset_palette():

extern "C" {
    pub fn reset_palette(vc: *mut vc_data);
}

unsafe fn vc_cons_allocated(i: usize) -> bool
{
    unsafe { vc_cons.get(i).map(|con| !con.d.is_null()).unwrap_or(false) }
}

fn set_scheme(name: &str, parm_redraw: bool) -> Result<()>
{
    /* … */
    with_console_lock(&|| {
        unsafe {
            vc_cons
                .iter()
                .enumerate()
                .filter(|(i, _)| vc_cons_allocated(*i))
                .for_each(|(_, vc)| {
                    reset_palette(vc.d);
                });
        }

        /* … */
    })
}

Lots of unsafe in there but that is expected due to the invariants we must uphold: the console lock must be held, we must not access vc_cons[] out of bounds, and reset_palette() requires its argument to point to an initialized console.

Parameters

To finalize this module, let’s briefly touch on the subject of module parameters. Parameters go in the params part of the module!() declaration:

params: {
    set: str {
        default: b"",
        permissions: 0o644,
        description: "Set a color scheme when loading the module",
    },
    redraw: bool {
        default: true,
        permissions: 0o644,
        description: "Redraw the screen after applying the scheme",
    },
},

(Note that the Rust for Linux authors provide an entire sample file showing the various ways of parametrizing a module.)

A C-ish aspect of how parameters are implemented on the Rust end is that they end up being declared as constants with the same name that they are bound to throughout the module. As a consequence of that const-ness, that identifier can interfere with pattern matching. For instance, declaring a module parameter foobar like this:

module! {
    /* … */
    foobar: str {
        default: b"",
        permissions: 0o644,
        description: "test",
    },
}

makes rustc trip over the following:

let foobar = 42;

with a rather cryptic error message:

22 | / module! {
23 | |     type: Vtcol,
24 | |     name: "kvtcol",
25 | |     author: "Philipp Gesang",
...  |
44 | |     },
45 | | }
   | |_- constant defined here
...
56 |               let foobar = 42;
   |                   ^^^^^^   -- this expression has type `{integer}`
   |                   |
   |                   expected integer, found struct `__kvtcol_foobar`
   |                   `foobar` is interpreted as a constant, not a new binding
   |                   help: introduce a new binding instead: `other_foobar`

The reason why this fails is of course that let bindings take a pattern on the left hand side, let <pattern> in <expr>;, and constants are substituted with their values by the compiler – thus in let foobar = 42; we are attempting to match some integer type with a constant struct, which rustc rightly rejects as nonsensical. That behavior can be bit irritating, it would be desirable to at least optionally be able to specify a different identifier for the constant to avoid such conflicts. AFAICs the proc macro that generates the parameter implementations does not allow for that possiblity yet.

As for the two parameters set and redraw we can now access them in the initializer function, which requires locking for values that can’t be atomically read:

let parm_redraw = {
    let lock = module.kernel_param_lock();
    let parm_set = set.read(&lock);
    let parm_set = core::str::from_utf8(parm_set)?;
    let parm_redraw = *redraw.read(&lock);

    if !parm_set.is_empty() {
        set_scheme(parm_set, parm_redraw)?;
    }
    parm_redraw
};

Note how we bind the inner value of set to parm_set to avoid the clash with the constant set. The semantics are simple: redraw=1 makes kvtcol call redraw_screen() after setting the palette to switch the entire visible area to the new palette and not just the regions that are updated afterwards. set=FOO applies the scheme FOO, if known to the module, immediately during initalization:

# insmod kvtcol.ko set=garbage redraw=0
insmod: ERROR: could not insert module kvtcol.ko: Invalid parameters
# insmod kvtcol.ko set=solarized redraw=0

Wrapping it Up

This third and final part of the series concludes with a functional module for changing the console palette to one of five predefined schemes. In the process it was shown how in kernel space some concepts can work differently from user space Rust – notably synchronization primitives and dynamic allocation – and how to access the functionality that is already implemented on the C side of things from Rust. Finally, parameters were added to allow the module to behave in different ways depending on the arguments it receives from the kernel command line, insmod, modprobe and the likes. Also, certain pitfalls were discussed like forgetting to initialize a kernel mutex, the lack of an exported symbol and interference from module parameter names.

The Rust for Linux project has already reached a solid level of maturity. Its scope, as this series of articles hopefully showed, extends beyond the mere integration of the Rust compiler into the Linux toolchain. The set of tools and convenience features it provides make for a very ergonomic and seamless programming experience, at least as far as writing modules is concerned.

Here’s hoping for a speedy merge with v6.1!

gallery

gallery image thumbnail gallery image thumbnail