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!