2015/weensy2

From CS61
Jump to: navigation, search

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's me variables! (You will want to place a sys_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