Process Isolation and Weensy OS
The process abstraction is among the most powerful abstractions with which we've dealt this semester. It is one you have used from the very first program you wrote -- in this unit, we've begun identifying the key mechanisms that provide this abstraction -- today we will try to uncover any remaining mechanisms.
Learning Objectives
- Identify the different mechanism that the operating system uses to provide process isolation
- Be comfortable using and debugging on Weensy OS so you can tackle Assignment 6
Get the Code
We'll be working today with code that is practically identical to that
from which you'll start Assignment 6 (we've made a couple of minor
changes to facilitate the exercises, but nothing worth noticing).
Today's code is in the cs61-exercises
repository under l24
, but
understand that it's really the same code from which you'll start
assignment 6.
NOTE: This will run on the appliance; this may run on other Linux boxes, assuming they have QEMU installed; this will not run on other platforms.
What can I do with Weensy OS?
Weensy is a real live operating system that has been designed for you! It's small enough that you can master it and functional enough that you can learn some really good stuff.
So, let's see what Weensy can do. (Try these things.)
Note: Sometimes you can end up with QEMU running even though you don't
have any foreground processes running in your terminal window. In this
case, use ps
to find the pid of QEMU and the use kill PID
to kill
the QEMU process.
1. You can run Weensy's default workload: 4 processes that simply try
to allocate memory.
make run
2. You can stop Weensy from running: simply type q
in the console
window. (The console is the pretty screen displaying the multi-colored
picture). If you want to know what the picture is telling you, check out
the assignment.
3. You can try to make your system fork processes. After you've done
make run
, type an f
in the console. Unfortunately, if you tried
that, you will have discovered that Weensy doesn't yet support fork
--
you'll add fork as part of Assignment 6. In fact, the following letters
typed in the console do the following:
- a -- run allocator (default) programs (p-allocator.c)
- f -- run fork program (p-fork.c)
- e -- run fork-exit program (p-forkexit.c)
- q -- quit
4. You can debug Weensy! If you type make run-gdb
you'll see that the
console window pops up, but the terminal in which you typed the command
will have you at that familiar GDB prompt! You can break anywhere you
want. We suggest putting a breakpoint in kernel
and stepping around to
see how your tiny operating system works. You'll notice that you get
both the line of C displayed as well as the set of assembly
instructions. Aren't you glad you spent all that time learning to read
assembly?
While you're in GDB, step and next around to see if you can answer the following questions:
4.1 There is a secret command and process that this version of Weensy can start up. Can you find it? What is its name and on what line does it get started?
4.2 current
is a global variable that stores the currently running
process's information. Where does it get set?
4.3 What does it have to do to run a process?
4.4 Why must the kernel to an iret
instead of a ret
?
Providing process isolation
5. Once the kernel runs a process, how does it ever gain control again?
Let's take a look at the canonical mischievous process -- we've added a
program to the Weensy distribution, called p-badguy.c. Take a look at
it. What does it do? You can run badguy
by typing a b
in the console
window. Try that.
6. Notice that the way Weensy starts that process is by rebooting the
entire machine! You'll find that you're back at the breakpoint you set
at kernel
. This time, you get different behavior than before; why?
7. OK, once you've seen badguy write all over your console, type q
to
quit. Take a look at log.txt
. Given what you see there, how would you
answer the question, "How does the OS gain control again once it starts
running a process?" Set a breakpoint in Weensy that you think will let
you regain control while programs are running.
The timer and timer interrupts are another one of those kernel mechanisms that helps the OS provide process isolation. No matter what a process does, if the OS regains control every time the timer goes off, then the OS can ensure that all processes eventually get to run.
So, what else could a mischievous process try to do? What if a process
could turn off interrupt? That would be problematic. Let's see what
happens if we try that. We now know how to sneak assembly code into our
C programs -- let's try that! The instruction to turn interrupts off is
cli
. Edit p-badguy.c
and instead of (or inanition to) printing a
message, try inserting the following code:
asm volatile ("cli\n");
8. What happened?
9. Use the x86.h
file to help you figure out what that means. What
does it mean?
Maintaining process isolation requires that unprivileged processes not be allowed to issue dangerous instructions.
10. Can you think of any other examples of dangerous instructions? Try them and see what happens.
The VM System and Process Isolation
We've also discussed how virtual memory provides process isolation, preventing processes from stepping on the operating system and preventing processes from accessing each other's memory. Let's see how Weensy responds to such attempts.
Overwriting the Dispatch Table
While we were unable to explicitly turn off interrupts, what if we could get the operating system to invoke our unprivileged code on a timer interrupt? Wouldn't that be cool! OK, in order to that, we need to overwrite the operating system's dispatch table entry for the timer interrupt. To do that, we'll have to find the address of the dispatch table.
The file obj/kernel.sym
contains the operating system's symbol table.
Try the following:
- Find the symbol for
interrupt_descriptors
. - Use the
grep
command to figure out which entry is for the timer interrupt.
grep INT_TIMER *
grep INT_HARDWARE *
Note: Grep is a pretty handy tool for various things, not the
least of which is finding where things are in code. If you are not
familiar with this command, read the man page and play with it.
- At this point, you know that we want to override the 32nd entry of the interrupt_descriptors array. The code to do this is a bit tricky, so we'll explain how you could figure it out, but then we'll show you the code we used.
- The interrupt dispatch table must contain entries of type
x86_gatedescriptor
- We use the SETGATE macro (defined in
x86.h
) to set up a gate entry.
So, here are the code snippets to add to p-badguy
// Put this before main
void
evil_interrupt_handler(void)
{
for (int i = 1; ; i++) {
if (i % 1000000 == 0)
app_printf(10, "I own your machine! %d\n", i);
}
}
// Put this at the beginning of main
x86_gatedescriptor *p = (void *)0x50180;
SETGATE(*p, 0, 8, evil_interrupt_handler, 3);
OK -- now, if the OS is properly protected, we should get a protection fault when we run badguy. Try it!
If you made all the right changes, when you type b in the console,
you'll suddenly see a bunch of messages indicating that our evil program
has taken over your machine! Furthermore, you'll notice that typing q
in the console window doesn't help! That's because we've overwritten the
hardware interrupt handler. Oops! A Control-C
from the window in which
you typed make run
should kill your evil, evil process. One of the
first things you'll do in A6 is prevent the nefarious badguy from taking
over your machine.
Overwriting the Interrupt Handler
Let's try another way that badguy might take over your machine. While the timer interrupts prevent badguy's infinite loop from locking out the operating system, what if we could make the operating system itself loop infinitely!? That might be really cool, because the operating system will have already turned off interrupts, so there will be nothing one can do! How might we go about doing this?
- Figure out where the sys49_int_handler lives in memory. (Hint: check
out
obj/kernel.sym
) - Now, we need to figure out what the instruction is for creating an
infinite loop. Can you think of a 1-line assembly language program you
could write that jumps to itself? Try placing that 1 line of assembly
in a file called
x.S
and then assemble and examine it by typing:
cc -c x.S
objdump -D x.o
You should see a single 2-byte instruction. Now, make that into a 2-byte hex number (remember that the display shows you the low-order byte first, so construct this number correctly!
- Finally, edit
p-badguy.c
and add two lines to overwrite the sys49_int_handler with the infinite loop instruction. Just fill in the lines below with the values you found.
unsigned short *sp = (void *)SYS49_INT_HANDLER_ADDRESS;
*sp = INFINITE_LOOP_INSTRUCTION;
If you did this correctly, then when you run Weensy and type b
into
the console window, your entire Weensy instance will hang! You can
should be able to kill QEMU and Weensy by typing a control-C into the
window from which you ran it.
At this point, we have firmly established the fact that Weensy is not
protecting itself (the operating system) from naught unprivileged
programs. Fortunately, you know how to do this and will need to do so
the first part of Assignment 6. The secret is all about setting the
proper permissions in PTE entries to prohibit unprivileged processes
from writing into kernel memory. This requires only a single line of
code -- see if you can figure out where to insert the proper call to
virtual_memory_map
to prevent the kinds of attacks we've just
demonstrated.
Clobbering Other Processes Address Spaces
We'll play with the allocator process since we can already run multiple
instances of it. Add a call to app_printf
to have each process print
its pid (p) in p-allocator.c
each time it allocates a page. (This will
destroy your display, but that's OK for now.)
Make sure everything still builds.
Ok, now for the fun! Let's have allocator1 clobber the pid for each of the other allocators!
- Find the memory location for the pids in each of allocator2,
allocator3, and allocator4. (Hint: Symbol tables are in the
obj
directories.) - Now, in
p-allocator.c
, before you enter the while loop, if you are the process with pid = 1, see if you can sneak your own PID into each of the other process'sme
variables! (You will want to place asys_yield()
before this code so that it runs after the other processes have started.)
If you've done this correctly, when you run Weensy, it will appear that only process 1 ever allocates pages!
Clearly Weensy is not protecting user processes from one another! This is a problem you will solve in part 2 of the assignment.
Wrapping Up
By now, we hope that you are comfortable playing with Weensy. The beauty of running your own operating system inside a virtual machine (which happens to be inside another virtual machine) is that you can tweak things and try things out without breaking your whole system! Go ahead and experiment. This assignment brings together all the skills you've developed this semester: understanding how memory is allocated, being able to read assembly, using GDB. And mostly, it requires thought, not writing a large number of lines of code.
Please complete this short survey and then have a wonderful break