Double
OS in Rust
Announcements
- Action Items:
- Testing done.
- First handler assigned, more to come.
Citations
- CPU exceptions are algorithmically interesting but technically tedious.
- Double Faults
Negation
- Not to be confused with the
doubletype. - That is floating point type and has no place in an OS class.
Recall
Exceptions
- CPU exceptions occur in various erroneous situations
- Accessing an invalid memory address
- Dividing by zero
- We have to set up an interrupt descriptor table..
- This week, our kernel will be able to catch double faults.
Types
On x86, there are about 20 different CPU exception types.
- Double Fault: When an exception occurs, the CPU tries to call the corresponding handler function. If another exception occurs while calling the exception handler, the CPU raises a double fault exception.
- This exception also occurs when there is no handler function registered for an exception.
Handle It
What is it?
- A special exception that occurs when the CPU fails to invoke an exception handler.
- For example, a page fault is triggered without a page fault handler registered in the IDT.
Python
- Perhaps a generalized exception.
Behavior
- A double fault behaves like a normal exception.
- It has the vector number
8and we can define a normal handler function for it in the IDT. - It is really important to provide a double fault handler, because if a double fault is unhandled, a fatal triple fault occurs.
- Triple faults can’t be caught, and most hardware reacts with a system reset.
Triggering a Double Fault
- We don’t have a page fault handler, so it will double (and triple) fault.
Description
- We use
unsafeto write to a literal. - This address lacks meaning.
- A virtual (software-visible) address.
- Regard as a key in key-value storage that maps software-visible addresses to hardware addresses (the MMU).
- The key is not present in the data structure.
Result
- So, when we attempt to assign a value based on that key, it explodes.
- The emulated device goes into a reboot loop.
- Why?
Steps
- The CPU tries to write to
0xdeadbeef, which causes a page fault. - The CPU indexes into the IDT for a page fault and no handler function is specified so a double fault occurs.
- The CPU indexes into the IDT for double fault handler and no handler function is specified so a triple fault occurs.
- A triple fault is fatal. QEMU (or hardware) issues a system reset.
Handle it
- To prevent this triple fault, we need to either:
- Provide a handler function for page faults, or
- Provide a double fault handler.
- We want to avoid triple faults in all cases
- Let’s start with a double fault handler.
Throwback
src/interrupts.rs
#![allow(static_mut_refs)]
static mut IDT: x86_64::structures::idt::InterruptDescriptorTable =
x86_64::structures::idt::InterruptDescriptorTable::new();
pub fn init_idt() {
unsafe {
IDT.breakpoint.set_handler_fn(breakpoint_handler);
IDT.load();
}
}
// Love these `cargo fmt` newlines
extern "x86-interrupt" fn breakpoint_handler(
stack_frame: x86_64::structures::idt::InterruptStackFrame,
) {
crate::println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame);
}Write a Handler
- For now, just rip a panic.
- “Ah!” - that’s what I sound like when I panic.
- The handler has to diverge because the hardware doesn’t allow returns from a double fault.
- After all, it’s bad. Twice as bad, even.
One Example
- Short and sweet.
Wait a minute
- Blog, in its infinite wisdom, claims we can safely ignore the error code because “it’s always zero”.
- I’ll be the judge of that.
By the way
- This is how it looks with
cargo fmt
I bet that works
- It doesn’t, run it.
- Yet another reboot loop.
- But we wrote a function!
- Friends - did we add it to our IDT?
Update Init
- This should do it!
Test it
Behind the Curtain
- Typically at this point I would show you what QEMU looks like at this time.
- I can trivially do that with the screendump->ppm2png->base64->html pipeline.
- It is much cooler to just dump the error message to console.
- But how?
For Serial
- Naturally, we can write to console via the serial port from out test environment.
- So, let’s quickly write a test case for the double-fault handler.
- It is basically a glorified
should_panic.rs.
What I start with
test/double.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(osirs::_test_runner)]
#[panic_handler]
fn test_panic(_info: &core::panic::PanicInfo) -> ! {
osirs::serial_println!("[Pass]");
osirs::qemu_quit(osirs::QEMU_PASS);
loop {}
}
fn bad() {
assert!(false);
}
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
osirs::_test_runner(&[&bad]);
osirs::qemu_quit(osirs::QEMU_FAIL);
loop {}
}Changes
- We want to:
- Print the error message via
serial_println! - Hit a panic from within a double-fault handler.
- Print the error message via
- Easy enough, we’ll just also
initand then modify the panic handler.- (We’ll change it back once we get the text out.)
Change bad
- Was:
- Is now:
Change panic
- Was:
test/should_panic.rs
- Is now:
Change _start
- Was:
test/should_panic.rs
- Is now:
The Stack Frame:
- I didn’t have to manually type this:
Clean up
- I kept the test and just reverted the panic handler.
- Actually, maybe that variant should be factored to
src/lib.rs - Software engineering… it’s everywhere…
- Actually, maybe that variant should be factored to
- (Do this on your own time).
- (Also stop using the test runner on singleton tests).
Back to Work
What happened?
- The CPU tries to write to
0xdeadbeef, which causes a page fault. - Like before, the CPU looks at the corresponding entry in the IDT and sees that no handler function is defined. Thus, a double fault occurs.
- The CPU jumps to the – now present – double fault handler.
Vegan
- Like me, the CPU now abstains from dead beef.
- The triple fault (and the boot-loop) no longer occurs, since the CPU can now call the double fault handler.
- Now we’re all done!
- (We aren’t)
Complications
- We have now caught a (singular, one) double fault.
- And a contrived one at that!
- It so happens there are other things that can also go wrong with computers.
- Do any of you remember the infinite recursion implementation that could’ve been SO COOL if Rust was just normal about it.
Causes of Double Faults
- Before we look at the special cases, we need to know the exact causes of double faults.
- Above, we used a pretty vague definition:
A double fault is a special exception that occurs when the CPU fails to invoke an exception handler.
Failure
- What does “fails to invoke” mean exactly?
- The handler is not present?
- The handler is swapped out?
- And what happens if a handler causes exceptions itself?
Aside: Swaps
Swap 1
Swap 2
Swap 3
Swap 4
Swap 5
- I had to increase my swap space that time I was dumping QEMU traces to text files and filled my harddrive.
- I don’t remember what I did exactly.
- Here’s a Stack Overflow question
Swap 6
- This tutorial seems fine.
- How to increase the size of your swapfile
- Go to the QUAD center and bother a Linux lifestylist
Back to Work
Handling Problems
- What if a breakpoint exception occurs, but the handler function is swapped out?
- What if a page fault occurs, but the handler is swapped out?
- What if a divide-by-zero handler causes a breakpoint exception, but the breakpoint handler is swapped out?
- What if our kernel overflows its stack and the guard page is hit?
Read the Docs!
- The AMD64 manual, a real thing that actually exists, is also actually helpful here.
- Wait that link is broken.
- We’ll trust the blog.
a double fault exception can occur when a second exception occurs during the handling of a prior (first) exception handler
Combo
| First Exception | Second Exception |
|---|---|
| Divide-by-zero, Invalid TSS, Segment Not Present, Stack-Segment Fault, General Protection Fault |
Invalid TSS, Segment Not Present, Stack-Segment Fault, General Protection Fault |
| Page Fault | Page Fault, Invalid TSS, Segment Not Present, Stack-Segment Fault, General Protection Fault |
Examples
- A divide-by-zero fault followed by a page fault is fine.
- The page fault handler is invoked.
- A divide-by-zero fault followed by a general-protection fault is not.
- Leads to a double fault.
- Security hat on: a user-mode program may be attempting privilege escalation.
We Now Know 1
- If a breakpoint exception occurs and the corresponding handler function is swapped out, a page fault occurs and the page fault handler is invoked.
- If a page fault occurs and the page fault handler is swapped out, a double fault occurs and the double fault handler is invoked.
We Now Know 2
- If a divide-by-zero handler causes a breakpoint exception, the CPU tries to invoke the breakpoint handler.
- If the breakpoint handler is swapped out, a page fault occurs and the page fault handler is invoked.
Another case
- An exception without a handler function in the IDT:
- CPU derefs the corresponding IDT entry.
- The entry is invalid so a general protection fault occurs.
- We lack a general protection fault handler, so another general protection fault occurs.
- This leads to a double fault.
Four!
Let’s look at the fourth question:
What happens if our kernel overflows its stack and the guard page is hit?
En Garde!
- A guard page denotes the bottom of a stack to detect stack overflows.
- The page is not mapped to any physical frame, so accessing page faults.
- It is the idea of memory that doesn’t exist.
- The Rust
optionwould’ve been great here!
- The bootloader sets up a guard page so overflow causes a page fault.
Kernel Stack Overflow
- When a page fault occurs, the CPU looks up the page fault handler in the IDT and tries to push the [interrupt stack frame] onto the stack.
- However, the current stack pointer still points to the non-present guard page.
- Thus, a second page fault occurs, which causes a double fault (according to the above table).
Visually
- The “interrupt stack frame”:
2s to 3s
- On the second page fault, the CPU tries to call the double fault handler now.
- This would push yet another stack frame, which…
- … still points to the guard page.
- Third page fault, triple fault, reboot loop.
Count to Three
- Easy enough, I just make a new test.
- Add a function and call from
_start
Run Just This Test
- I have a lot of tests now, so…
- Good to run all of them though!
- Anyways it will fail somehow (boot loop into time out).
Fix
- How can we avoid this problem?
- We can’t omit the pushing of the exception stack frame.
- Done by hardware.
- We need to ensure somehow that the stack is always valid when a double fault exception occurs.
- Fortunately, the x86_64 architecture has a solution to this problem.
- We can’t omit the pushing of the exception stack frame.
Switcheroo
- The x86_64 architecture operates multiple stacks.
- Maintained in (effectively) a linear data structure.
- Interrupt Stack Table (IST)
- There’s 7.
Handle it
- Each exception handler, can choose a stack from the IST through the
stack_pointersfield in the corresponding IDT entry. - For example, our double fault handler could use the first stack in the IST.
- I don’t know if it’s zero or one indexed.
- This preempts the push to prevent the triple.
TSS
- We recall other segments: Code (CS), Data (DS), Stack (SS).
- Now, the “Task State Segment”
- Developed for 32-bit “hardware context switching” that was not brought to 64-bit due to complexities/inefficiencies.
- Still around and now used here.
- The 64-bit TSS and 32-bit TSS are wildly different and this doesn’t matter.
The 64-bit
| Field | Type |
|---|---|
| (reserved) | u32 |
| Privilege Stack Table | [u64; 3] |
| (reserved) | u64 |
| Interrupt Stack Table | [u64; 7] |
| (reserved) | u64 |
| (reserved) | u16 |
| I/O Map Base Address | u16 |
- I/O Map Base Address supports both 32 and 64 bit port-mapped I/O.
PST
- Not a timezone.
- Privilege Stack Table
- Privilege is bits 12-13 of the code segment, or the code segment “descriptor privilege level.
- We recall all actually existing OSes:
Usage
- An unprivileged executable divides-by-zero.
- The handler is called.
- The handler most be privileged, it’s part of the OS.
- So it grabs the privileged stack.
- (This is stack 0 since privilege 0 is the most privileged, mostly)
- We ignore for now (we have no software).
Creating a TSS
- Let us create a new TSS that contains a separate double fault stack in its interrupt stack table. For that, we need a TSS struct.
- By “us” we mean the
x86_64crate which contains aTaskStateSegmentstruct. - We’ll work within
src/gdt.rs- Global Descriptor Table - we’ll add more latter.
First
- We need to
pub modthis insrc/lib.rs
- Add the initializer for the GDT to the
initfunction too.
Start on the GDT
- First, let’s make a TSS.
- It will come as a shock to no one that I will use
static mutto solve a problem wherein a language-level alias is used for actually existing, persistent, mutable hardware.
Initialize
- Not too bad - we set the double fault index to be the stack index plus the stack size.
- We just made an arbitrarily sized (ish) stack in a
static mut(which requiresunsafe)
I bet this will work
- It won’t.
- We made a TSS but to tell the CPU that it should use it.
- We need to add a new segment descriptor to the GDT.
- Then we can load our TSS with the respective GDT index.
Make a GDT
- Yes, yes, the same trick.
src/gdt.rs
- The GDT, like the TSS, is a 32-bit holdover but still used to:
- Hold the TSS, obviously, and
- Swap between kernel (privileged) and user (software/unprivileged) modes.
I bet this will…
- We further have to add entries to the GDT, naturally.
- I add the kernel code segment
- I add the TSS we made.
- I use the crate to do this.
Now it will…
- Test it, but…
- The GDT segments are “not yet active” because the segment and TSS registers still contain the values from the old GDT.
- We also need to modify the double fault IDT entry so that it uses the new stack.
So we…
- Reload code segment register: We changed our GDT, so we should reload CS, the code segment register. The old segment selector could now point to an old e.g. TSS.
- Load the TSS: We loaded a GDT that contains a TSS selector, but we still need to tell the CPU that it should use that TSS.
- ** Update the handler**: Our double fault handler must know to use the novel stack we have created.
Not too bad…
- Load the GDT.
Update Segments
- Basically, we got the kernel CS, and TSS.
- We turned them into descriptors.
- Either “set” or “load” these descriptors into the GDT.
Full File
src/gdt.rs
#![allow(static_mut_refs)]
// GDT
static mut GDT: x86_64::structures::gdt::GlobalDescriptorTable =
x86_64::structures::gdt::GlobalDescriptorTable::new();
// TSS
const STACK_SIZE: usize = 4096 * 5;
static mut TSS: x86_64::structures::tss::TaskStateSegment =
x86_64::structures::tss::TaskStateSegment::new();
static mut STACK: [u8; STACK_SIZE] = [0; STACK_SIZE];
pub const DOUBLE_FAULT_IST_INDEX: usize = 0;
pub fn init_gdt() {
unsafe {
TSS.interrupt_stack_table[DOUBLE_FAULT_IST_INDEX] =
x86_64::VirtAddr::from_ptr(&raw const STACK) + STACK_SIZE;
let kcs = GDT.add_entry(x86_64::structures::gdt::Descriptor::kernel_code_segment());
let tss = GDT.add_entry(x86_64::structures::gdt::Descriptor::tss_segment(&TSS));
GDT.load();
use x86_64::instructions::segmentation::Segment;
x86_64::instructions::segmentation::CS::set_reg(kcs);
x86_64::instructions::tables::load_tss(tss);
}
}Insufficient
- I still triple fault on this.
- Why?
- We added a stack.
- We added it’s location to the GDT.
- We added a double fault handler, but
- We did not direct the double fault handler to change stacks.
- We return to the handler (NOT the GDT) to update it…
Handle it
- You probably have:
Update
- Update to change a stack index
This “worked” for me
- Prefer panic to boot loop.
Tying this off
- This is… incomplete.
- As a test.
- For the OS, it is fine.
- You need to still:
- Create a test case
- Modify various handlers
- Exit successfully when double, rather than triple, faulting.
- Would’ve been this weeks HW.