Demilade Sonuga's blog

All posts

Refactoring VIII

2023-05-13 · 21 min read

There are a lot of improvements that could be done to the code to make it generally better, easier to read, and more organized, but we won't be doing that at the moment. In this post, we'll be focused on figuring out how to go about removing the many instances of unsafety in our code.

What Needs To Be Done

Take a look at these lines in main.rs:

static mut SCREEN: Option<&mut Screen> = None;

static mut GDT: Option<GDT> = None;
static mut TSS: Option<TSS> = None;
static mut IDT: Option<IDT> = None;
static mut PICS: Option<PICs> = None;
static mut KEYBOARD: Option<Keyboard> = None;

Mutable statics are inherently unsafe because they can be modified and read by any code anywhere without restrictions. When there are so many of them in the code, unsafety is unavoidable. So, to reduce unsafety, we need to reduce the instances of mutable statics.

Once and Interior Mutability

To do that, we first need to understand what these structures are used for and why exactly we modeled them as mutable statics.

First, we have the SCREEN mutable static. The purpose of this static is to hold a reference to the memory segment that controls what is displayed on the screen.

#[no_mangle]
extern "efiapi" fn efi_main(
handle: *const core::ffi::c_void,
sys_table: *mut SystemTable,
) -> usize {
// ...Others

let init_graphics_result = init_graphics(boot_services);
// Halt with error if graphics initialization failed
if let Err(msg) = init_graphics_result {
let simple_text_output = sys_table.simple_text_output();
write!(simple_text_output, "{}", msg);
loop {}
}
let screen = init_graphics_result.unwrap();

init_screen(screen);

// ...Others
}

If you can remember from past posts, init_graphics changes the mode of the screen into a graphics mode that allows for drawing pixels. Upon success, the result returned by init_graphics will hold the reference to the screen memory.

Now, this is the reason we used a mutable static here: the address of the screen memory is not known at compile time, only at runtime. And that was a problem because regular safe statics are immutable and only useful when you know the value of what needs to be stored at compile time.

Another reason, mentioned in the Panicking I post, is that the panic handler needs to be able to print panic messages to the screen.

If the screen reference stayed on the stack, the panic handler won't have been able to access it. When we were writing the panic handler, there was no heap, but even now that there is a heap, I still don't think it will be a good idea to keep the reference on the heap. I mean, what if a panic occurs while memory is being allocated for the screen reference? How will the panic message be printed?

Making the static mutable allowed us to set it to the correct value at runtime.

The problem of only being able to set the value at runtime is the reason for making GDT, TSS, and IDT mutable statics. We just can't give them values until runtime, and if you take a close look at the code, after we give them values, we never change them again.

Out of all the statics PICS and KEYBOARD are the only ones that aren't only initialized once. That is, after initializing PICS or KEYBOARD, we still call functions on it that require mutable references, like PICs::end_of_interrupt or Keyboard::interpret_byte.

But GDT, TSS, and IDT, after initialization, are never modified again.

So, the power given to us by mutable statics is way much more than we need, and that can be a recipe for disaster.

What we need to resolve this is some kind of structure that will allow us to mutate a value only once, just to initialize it, and never allow mutation again.

In other words, we need a structure that can allow for one-time initialization. Normally, statics are initialized at compile time with a value and are never changed in a program. One-time initialization allows for initializing statics at runtime with a function to be run once, and after the run of that function, the static is never allowed to be modified again.

For example:

static GDT: Once<GDT> = Once::new();

fn init() {
GDT.call_once(|| { /* initialize the GDT */ });
// GDT is now accessed like a regular static
}

This Once indicated above is a common synchronization primitive in std. But we aren't using std here, so we'll have to implement one ourselves.

If you take a good look at the idea just presented, you'll notice that there's a problem. Regular statics are always marked immutable, but a Once mutates it after the run of the initialization function.

This mutation of apparently immutable values is also another rather common pattern in Rust called interior immutability. It's interior because the mutation happens within the value itself. This is a superpower that's used only when needed.

When such primitives with interior mutability, like Once, are created, they typically use unsafe operations which are manually verified to be safe and then present a safe outwards interface for use. In other words, when interior mutability is used, it is the job of the programmer to verify that its use doesn't result in the violation of any borrow checking rules or Rust safety guarantees.

Interior mutability is needed here because of the goals of the Once: to allow a value to be initialized once at runtime, and never be mutated again.

The value is not fully mutable. You can't mistakenly modify GDT. The mutation is strategic and controlled safely through the safe interface presented by Once.

Mutex

As for KEYBOARD and PICS, these statics are mutated continuously. When PICs::end_of_interrupt is called, the ports controlled by the structure are written to. When Keyboard::interpret_byte is called, the Keyboard::code_is_extended field of the keyboard is (sometimes) mutated.

Another problem we've been encountering is the occasional panic due to random and unpredictable double faults. Common causes of bugs of this kind are races (using shared data without any form of synchronization).

All this talk about synchronization brings us to another mutable static in the project, in allocator.rs:

static mut ALLOCATOR: Option<LinkedListAllocator> = None;

And its use:

In boxed.rs

impl<T> Box<T> {
pub fn new(val: T, allocator: *mut dyn Allocator) -> Box<T> {
// ...Others
}
}

In vec.rs

impl<T: Clone> Vec<T> {

// Creates a vector with the stated capacity
pub fn with_capacity(capacity: usize, allocator: *mut dyn Allocator) -> Vec<T> {
// ...Others
}
}

In boxed_fn.rs

impl BoxedFn {
// Creates a new BoxedFn from the given function-thing
pub fn new<F>(func: F, allocator: *mut dyn Allocator) -> Self where F: FnMut(EventInfo) {
// ...Others
}
}

The Allocator trait objects are not safe references or boxes. They're all raw pointers. And data behind raw pointers can be easily accessed without any guarantees of synchronization. This lack of synchronization is the sort of thing that can lead to races.

Even though this code is not multithreaded, there is still a form of concurrency, through interrupts.

One primitive that we can use to handle this synchronization issue is the Mutex, which stands for mutual exclusion. The Mutex is a structure that allows only one line of execution to access a piece of data at a time. In other words, when one line of execution has access to the data in a Mutex, no other line of execution can access that data until the first one lets go.

You can think of a Mutex as a coordinator that gives mutable references to a piece of data to one thread at a time.

It can also allow for interior mutability in a multithreaded scenario.

With a Mutex, we could refactor our KEYBOARD and PICS from mutable statics to:

static KEYBOARD: Mutex<Keyboard> = Mutex::new();
static PICS: Mutex<Pics> = Mutex::new()

And safely access and modify it, like so:

KEYBOARD.lock().interpret_byte(byte);
PICS.lock().end_of_interrupt(val);

Effectively getting rid of all the unsafety.

The allocator no longer has to be a mutable static, but instead, it should be in a Mutex that will control accesses to it. Mutation will no longer be done anyhow and anywhere. It will be done in a safe synchronized manner, using interior mutability.

No, we need a Mutex too. There is a Mutex in std, but we aren't using std here. We need to create one ourselves.

What We Need To Do Now

Before we can get refactoring with a Mutex and a Once, we first need to create them by ourselves.

This presents to us a new todo list:

  1. Create a Once.
  2. Create a Mutex.

We'll start tackling the Once in the next post

Take Away

  • Interior mutability is a pattern in Rust where supposedly immutable variables provide a safe and contained interface to mutate values within them.
  • A Once is used for one-time initialization.
  • A Once uses interior mutability by providing a safe interface to mutate the contained data only once for initialization purposes.
  • A Mutex is used to prevent multiple threads from accessing shared data at the same time.
  • A Mutex uses interior mutability by providing a safe interface to allow code to obtain exclusive mutable access to some data

In The Next Post

We'll start implementing a Once.

References