College students: Attendance at section is required and recorded for all college students. All college students should fill out this attendance form when attending section. For more on the attendance policy and what to do if you miss section refer to the syllabus here and here.
Extension School students: Extension School students are required to self-report weekly section participation using this participation form. There are two ways to participate in section: (1) attend section live (for most students, this is one of the zoom sections, but local students are welcome to attend in-person sections) OR (2) watch a section recording (Canvas > Zoom > Published Recordings). Both participation methods are equally valid towards your participation grade.
The pset for this unit concerns our tiny WeensyOS operating system. Our
section introduces WeensyOS, and especially its internal interfaces
for dealing with virtual memory.
The coding exercises in this section use the cs61-sections/kernels1
subdirectory. This version of WeensyOS uses identity mappings; all processes
share the same page table (the kernel page table), but the kernel is isolated.
This resembles the state of your pset 3 after phase 1.
Part A: WeensyOS architecture
WeensyOS is a full operating system for x86-64 machines that fits in one
directory. In the first part of section, TFs will walk you through the way
files are laid out in the operating system and where different operating
system functions are located, and show you how to run the operating system in
different modes.
Update your cs61-sections
repository and change into the kernels1
subdirectory. Run docker. make run-hello
to see the exciting result of the
p-hello.cc
program! Type q
to quit.
-
Process code lives in files called p-*.cc
. The process_main
function is
the equivalent of main
in C++ programs; when the process starts,
its code starts running in process_main
.
-
Processes can write to the screen using a function called console_printf
.
This function writes to an area of memory called the CGA console, which
is a specific region of memory reserved for the screen. (Talk about the
address.)
-
Discuss the basic layout of WeensyOS memory: kernel code lives at addresses
near 0x40000 (256 KiB); the kernel stack grows downward from address 0x80000;
the process code segment starts at address 0x100000; and the initial process
stack segment grows downward from 0x140000.
+-----+--------------------+----------------+--------------------+---------/
| | Kernel Kernel | : I/O | App 1 App 1 | App 2
| | Code + Data Stack | : Memory | Code + Data Stack | Code ...
+-----+--------------------+----------------+--------------------+---------/
0 0x40000 0x80000 0xA0000 0x100000 0x140000
^
| \___ PROC_SIZE ___/
PROC_START_ADDR
-
Many C library functions are available, but not all. WeensyOS is a
standalone operating system: it doesn’t use a shared C library. Check out
lib.hh
and lib.cc
for the available library functions. These are present
in both processes and the kernel.
-
Before process code runs, the virtual machine that runs WeensyOS emulates a
“boot” (short for
bootstrapping) process, like
the process that runs when a real computer turns on. The first WeensyOS code
that runs is in boot.cc
. This code loads the kernel into memory and then starts running it.
-
The kernel is the portion of operating systems code that runs with full
machine privilege, meaning that it can control all of memory and other
hardware devices. The kernel continues running for as long as the computer
has power; its function is to manage processes and hardware. The kernel’s
starting point is kernel_start
in kernel.cc
. This function initializes
the hardware and loads the initial process(es). All of the code you will
write for the problem set lives in kernel.cc
.
-
A powerful thing you can do in kernel code is call the log_printf
function, which prints information to a file called log.txt
. log_printf
is a good debugging tool.
-
The other k-*
C++ files (k-hardware.cc
, k-memviewer.cc
, k-vmiter.cc
,
k-vmiter.hh
) provide abstractions that simplify hardware interactions.
You’ll deal most deeply with vmiter
, the virtual memory iterator
structure; we’ll look into it more deeply in this section.
-
A running computer continually switches between process code and kernel
code. For instance, the kernel takes control when a process makes a system
call by executing a syscall
instruction; and when the kernel finishes
handling the system call, it switches modes back to the process. As part of
this protected control transfer into the kernel, the process switches
privilege modes, saves some registers, and starts executing kernel code.
This mode switch is outside the C++ abstract machine, so it can’t be
implemented in C++ alone. The entry sequence is implemented in hand-written
assembly in k-exception.S
. Look there if you’re interested!
-
Show: make run
, make run-PROCESS
(e.g. make run-hello
runs p-hello.cc
), and make STOP=1 run
+ gdb -ix weensyos.gdb
(see Exercise B1 for an example).
Part B: Virtual memory mappings and permissions
Virtual memory is the
processor feature that supports process isolation for primary
memory (the part of the computer that stores data and has
addresses). It allows different processes to have completely different
views of memory. For instance, the data stored at address 0x100000 in
the two processes might be totally different, even though the
addresses are the same.
Virtual memory is a powerful technique with many uses, though isolation is its
most important use now.
x86-64 virtual memory is implemented by page table structures the kernel
maintains. A page table maps a virtual address, which is the kind of
address used in software and machine instructions, to a physical address,
which names a specific set of transistors and capacitors on machine memory
chips capable of holding a byte. The virtual-to-physical mapping can be almost
arbitrary! All virtual addresses might map to the same set of physical
addresses. There is just one constraint: Page tables work in units of
aligned chunks of memory 4,096 bytes (212 bytes) big. These
aligned units are called pages.
Page tables also associate permissions with each mapping. A mapping can be
read-only, which prevents software from writing to memory, and/or
kernel-only, which prevents processes from reading or writing memory.
A specific register, the %cr3
register, holds the current page table. If two
processes run with different page tables (different %cr3
values), then they
might have completely different views of memory. This feature lets the kernel
isolate the processes from one another.
Examining mappings and permissions
vmiter
and ptiter
are WeensyOS iterators that process page tables.
vmiter
answers the question “In the context of page table pt
, what does
the virtual address va
map to in terms of physical address and permissions?”
It also lets you modify mappings, thereby changing a process’s view of memory.
ptiter
answers the question “Which physical pages are used to represent this
page table?” This lets you free a page table relatively easily.
About virtual memory iterators (vmiter
)
The vmiter
class examines and modifies x86-64 page tables, especially
their virtual-to-physical address mappings.
Examining page tables
vmiter(pt, va)
creates a vmiter
object examining page table pt
and with current virtual address va
.
The va
method returns the iterator’s current virtual address.
The pa
method returns the physical address mapped at the current virtual
address:
x86_64_pagetable* pt = ...;
uintptr_t pa = vmiter(pt, va).pa(); // returns uintptr_t(-1) if unmapped
The kptr
method returns a pointer corresponding to the physical address.
Use this method if you need to examine the physical memory located at
va
, or you need to pass that memory to kfree
. If the virtual address
is mapped, then kptr
’s address value is the same as pa
(because the
kernel is identity mapped). kptr
returns nullptr
if the virtual
address is unmapped.
The perm
method returns the permissions of the current mapping.
if ((vmiter(pt, va).perm() & PTE_W) != 0) {
// then `va` is present and writable in `pt`
}
perm
returns a bitwise-or of flags, possibly including PTE_P
(numeric
value 1), PTE_W
(numeric value 2), and PTE_U
(numeric value 4).
PTE_P
marks Present pages (pages that are mapped). PTE_W
marks
Writable pages. PTE_U
marks User-accessible pages—pages
accessible to unprivileged processes. Kernel memory should be mapped with
permissions PTE_P|PTE_W
, which allows the kernel to read or write the
memory, but prevents all access by processes.
(Note that perm()
’s return value may include other bits than PTE_P
, PTE_W
, and
PTE_U
, because the processor automatically modifies some bits as it
runs. There are convenient shorthands—present()
, writable()
, and
user()
—for PTE_P
, PTE_W
, and PTE_U
.)
Traversing page tables
The find(va)
method changes the iterator’s current virtual address to
va
. You can also say it += DELTA
, which increments the current virtual
address by DELTA
.
It is common to use vmiter
in loops. This loop prints all present
mappings in the lower 64KiB of memory, moving one page at a time:
for (vmiter it(pt, 0); it.va() < 0x10000; it += PAGESIZE) {
if (it.present()) {
log_printf("%p maps to %p with permissions %x\n",
it.va(), it.pa(), it.perm());
}
}
Modifying page tables
The vmiter::try_map
function modifies mappings in a page table. This
code maps physical page 0x3000 at virtual address 0x2000, with permissions
P, W, and U:
int r = vmiter(pt, 0x2000).try_map(0x3000, PTE_P | PTE_W | PTE_U);
if (r < 0) {
// there was an error; mappings remain unchanged
}
vmiter::try_map
returns -1
if it cannot add a mapping (this happens if
it fails to allocate memory for a page table page).
You can also use try_map
to change a mapping’s permissions; this adds
PTE_W
to the permissions for virtual address va
:
vmiter it(pt, va);
assert(it.present());
int r = it.try_map(it.pa(), it.perm() | PTE_W);
Interface summary
// `vmiter` walks over virtual address mappings.
// `pa()` and `perm()` read current addresses and permissions;
// `map()` and `try_map()` modify mappings.
class vmiter {
public:
// Initialize a `vmiter` for `pt`, with initial virtual address `va`.
inline vmiter(x86_64_pagetable* pt, uintptr_t va);
inline vmiter(const proc* p, uintptr_t va);
// Return the page table this `vmiter` is examining.
inline x86_64_pagetable* pagetable() const;
// ADDRESS QUERIES
// Return current virtual address.
inline uintptr_t va() const;
// Return one past last virtual address in this mapping range.
inline uintptr_t last_va() const;
// Return physical address mapped at `this->va()`,
// or `(uintptr_t) -1` if `this->va()` is unmapped.
inline uint64_t pa() const;
// Return a kernel-accessible pointer corresponding to `this->pa()`,
// or `nullptr` if `this->va()` is unmapped.
template <typename T = void*>
inline T kptr() const;
// PERMISSIONS FUNCTIONS
// Returns current permissions at `this->va()`: 0 unless `PTE_P` is set.
inline uint64_t perm() const;
// Return true iff `this->va()` is present (`PTE_P`).
inline bool present() const;
// Return true iff `this->va()` is present and writable (`PTE_P|PTE_W`).
inline bool writable() const;
// Return true iff `this->va()` is present and unprivileged
// (`PTE_P|PTE_U`).
inline bool user() const;
// ADVANCED PERMISSIONS
// Return true iff `(this->perm() & desired_perm) == desired_perm`.
inline bool perm(uint64_t desired_perm) const;
// Return intersection of permissions in [this->va(), this->va() + sz).
uint64_t range_perm(size_t sz) const;
// Return true iff `(this->range_perm(sz) & desired_perm) == desired_perm`.
inline bool range_perm(size_t sz, uint64_t desired_perm) const;
// TRAVERSAL
// Move to virtual address `va`; return `*this`.
inline vmiter& find(uintptr_t va);
// Advance to virtual address `this->va() + delta`; return `*this`.
inline vmiter& operator+=(intptr_t delta);
// Advance to virtual address `this->va() + 1`; return `*this`.
inline vmiter& operator++();
inline void operator++(int);
// Advance to virtual address `this->va() - delta`; return `*this`.
inline vmiter& operator-=(intptr_t delta);
// Advance to virtual address `this->va() - 1`; return `*this`.
inline vmiter& operator--();
inline void operator--(int);
// Move to next larger page-aligned virtual address, skipping large
// non-present regions.
void next();
// Move to `this->last_va()`.
void next_range();
// MAPPING MODIFICATION
// Change the mapping in `this->pagetable()` for `this->va()` (the
// current virtual address) to `pa` with permissions `perm`.
// `this->va()` must be page-aligned. Can call `kalloc` to allocate
// page table pages. Panics if `kalloc` fails (returns `nullptr`).
inline void map(uintptr_t pa, int perm);
// Same, but map a kernel pointer
inline void map(void* kptr, int perm);
// Change the mapping in `this->pagetable()` for `this->va()` (the
// current virtual address) to `pa` with permissions `perm`.
// `this->va()` must be page-aligned. Can call `kalloc` to allocate
// page table pages. Returns -1 without modifying mappings if a
// `kalloc` fails (returns `nullptr`); otherwise changes the mapping
// and returns 0.
[[gnu::warn_unused_result]] int try_map(uintptr_t pa, int perm);
[[gnu::warn_unused_result]] inline int try_map(void* kptr, int perm);
};
About physical memory iterators (ptiter
)
The ptiter
type iterates through the physical memory used to represent a page table.
(x86-64 page tables are hierarchical structures that can comprise multiple pages
of memory.) ptiter
is useful mostly when freeing page table structures.
class ptiter {
public:
// initialize a `ptiter` for `pt`
inline ptiter(x86_64_pagetable* pt);
inline ptiter(const proc* p);
// Return kernel-accessible pointer to current page table page.
inline x86_64_pagetable* kptr() const;
// Return physical address of current page table page.
inline uintptr_t pa() const;
// Return first virtual address mapped by this page table page.
inline uintptr_t va() const;
// Return one past the last virtual address mapped by this page table page.
inline uintptr_t last_va() const;
// Move to next page table page in depth-first order.
inline void next();
// ...
};
ptiter
visits the individual page table pages in a multi-level page
table, in depth-first order (so all level-1 page tables under a level-2
page table are visited before the level-2 is visited). A ptiter
loop can
easily visit all the page table pages owned by a process, which is usually
at least 4 page tables in x86-64 (one per level):
for (ptiter it(pt); it.va() < MEMSIZE_VIRTUAL; it.next()) {
log_printf("[%p, %p): level-%d ptp at pa %p\n",
it.va(), it.last_va(), it.level() + 1, it.kptr());
}
A WeensyOS process might print the following:
[0x0, 0x200000): level-1 ptp at pa 0x58000
[0x200000, 0x400000): level-1 ptp at pa 0x59000
[0x0, 0x40000000): level-2 ptp at pa 0x57000
[0x0, 0x8000000000): level-3 ptp at pa 0x56000
Note the depth-first order: the level-1 page table pages are visited
first, then level-2, then level-3. This makes it safe to use a ptiter
to
free the pages in a page table. ptiter
never visits the topmost page
table page, so that must be freed separately. Note also that
ptiter::level
is one less than you might expect (it returns a number
between 0 and 3, rather than between 1 and 4).
EXERCISE B1. Let’s use vmiter
and log_printf
to log information
about kernel_pagetable
’s mappings for the following virtual addresses:
- The
syscall_entry
function;
- The
kernel_pagetable
;
- The
p-hello
process’s entry point, process_main
.
First log the physical addresses mapped for these virtual addresses
immediately before the call to process_setup
in kernel.cc
. What are they?
We know we can use vmiter
to find a physical address corresponding to a
virtual address. But how can we find the virtual addresses for these
entities? One way is just to ask GDB, which knows the addresses for
things.
cs61-user@9d076324193b:~/cs61-sections/kernels1$ make run
* Run `gdb -x weensyos.gdb` to connect gdb to qemu.
...
kohler@elsewhere$ gdb -x weensyos.gdb
GNU gdb (GDB) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
...blah blah blah...
(gdb) p syscall_entry
$1 = {<text variable, no debug info>} 0x40ad6 <syscall_entry()>
(gdb) p process_main
$2 = {void (void)} 0x100000 <process_main()>
(gdb) p kernel_pagetable
$3 = 0x59000 <kernel_pagetable>
(gdb)
(You may see different addresses.) Another way is to take advantage of the
symbol and assembly files created in the obj/
directory. If you’re not
sure what file to check, use grep
to check them all:
cs61-user@9d076324193b:~/cs61-sections/kernels1$ grep syscall_entry obj/*
Binary file obj/kernel matches
obj/kernel.asm:0000000000040ad6 <syscall_entry()>:
obj/kernel.asm: wrmsr(MSR_IA32_LSTAR, reinterpret_cast<uint64_t>(syscall_entry));
Binary file obj/kernel.full matches
obj/kernel.sym:0000000000040ad6 T _Z13syscall_entryv
Binary file obj/k-exception.ko matches
Binary file obj/k-hardware.ko matches
However you get these addresses, you discover their mappings using vmiter::pa
:
log_printf("syscall_entry pa: %p\n", vmiter(kernel_pagetable, 0x40ad6).pa());
log_printf("kernel_pagetable pa: %p\n", vmiter(kernel_pagetable, 0x59000).pa());
log_printf("process_main pa: %p\n", vmiter(kernel_pagetable, 0x100000).pa());
Unfortunately, adding log_printf
calls and recompiling can move the
symbols around! After recompiling, make sure you use obj/kernel.sym
, or
gdb
, to check that the addresses haven’t changed. If they do change, one
more round of editing should stabilize them.
We see (less log.txt
after make run-hello
):
syscall_entry pa: 0x40ad6
kernel_pagetable pa: 0x59000
process_main pa: 0x100000
As advertised, the kernel page table is identity mapped. (You may see
different results for syscall_entry
and kernel_pagetable
, but your
process_main
should match exactly.)
You can avoid hard-coding constants into the kernel by referring to the
actual functions and objects. The tricky bit is process_main
; we extract
that address by reading it from p-hello
’s program image.
log_printf("syscall_entry pa: %p\n", vmiter(kernel_pagetable, (uintptr_t) syscall_entry).pa());
log_printf("kernel_pagetable pa: %p\n", vmiter(kernel_pagetable, (uintptr_t) kernel_pagetable).pa());
program_image pgm("hello"); // "hello" = name of process
log_printf("process_main pa: %p\n", vmiter(kernel_pagetable, pgm.entry()).pa());
EXERCISE B2. Now log the physical addresses immediately after the call to
process_setup
. Do the values change? Why or why not? Refer to specific
properties of process_setup
.
They don’t change. In this simple version of WeensyOS, all process pages
are identity mapped. The process_setup
function changes the
permissions associated with specific mappings, but not the physical
addresses to which they are mapped.
EXERCISE B3. Log the permissions associated with those virtual
addresses immediately before the call to process_setup
. What are they?
What permissions do they correspond to? (Look for the PTE_
constants in
x86-64.h
to understand the meaning of each permission bit.)
They are all 0x3, which equals PTE_P | PTE_W
: user-inaccessible.
EXERCISE B4. What about these permissions indicate that the kernel
appears to implement kernel isolation (in terms of virtual memory access)?
The kernel is keeping its code and data isolated from process code by
removing the PTE_U
bit from most memory mappings.
EXERCISE B5. Log the permissions associated with those virtual
addresses immediately after the call to process_setup
. Did they change?
Indeed they did! The process_main
permission has PTE_U
now. Also,
oddly, some more bits appear to have been spontaneously added to the
permissions for syscall_entry
and process_main
. These bits—PTE_A
and
PTE_D
—are set automatically by the hardware to indicate that a page of
memory has been Accessed (that is, read) and Dirtied (that is,
modified). Some virtual memory features make use of this information.
Part C: Warped virtual memory
Run make run-bigdata
. Boo!
EXERCISE C1. Add one line of virtual memory map manipulation code to
process_setup
so that the bigdata
process prints CS 61 Is Amazing
.
Don’t change p-bigdata.cc
or the contents of physical memory, just change
memory mappings. If you get stuck, uncomment the console_printf
lines in
p-bigdata.cc
: what do you see?
p-bigdata
initially prints CS 61 Is Awful
. But notice that the
big_data
buffer is page-aligned, and the string is placed exactly 4086
bytes into that buffer: the first part of the string and the last part are
on different virtual pages. Using colors to mark the page boundary:
CS 61 Is Awful
What if we change the process’s virtual memory mappings so that both
virtual pages of the buffer map to the same physical memory? For
instance, if we map virtual address 0x102000 to physical address 0x103000,
then memory will look like this after running the two calls to strcpy
:
@va 0x102000: mazing
@va 0x102ff6: CS 61 Is A
@va 0x103000: mazing
This is because the strcpy
to virtual address &big_data[0]
would
modify the same physical memory as the memory starting at
&big_data[4096]
. That means mazing\n
would overwrite wful\n
, and
when we print the bytes at virtual address 0x102ff6
, we will see CS 61 Is Amazing
.
The following line of code added to process_setup
performs this mapping:
vmiter(ptable[pid].pagetable, 0x102000).map(0x103000, PTE_P|PTE_W|PTE_U);
If we didn't want to rely on the fact that the pagetable is an identity
mapping, we could look up the physical address of virtual address 0x103000
and use that for the mapping:
vmiter(ptable[pid].pagetable, 0x102000).map(vmiter(ptable[pid].pagetable, 0x103000).pa(), PTE_PWU);
Make sure you remove your VM manipulation code before moving on to another
problem.
Part D: Using iterators
In the problem set, you will implement fork
and exit
system calls. At the
center of these system calls are operations on virtual memory that you’ll use
vmiter
and ptiter
to implement. In section we investigate related problems
for practice.
EXERCISE D1. Use vmiter
to implement the following function.
void copy_mappings(x86_64_pagetable* dst, x86_64_pagetable* src) {
// Copy all virtual memory mappings from `src` into `dst`
// for addresses in the range [0, MEMSIZE_VIRTUAL).
// You may assume that `dst` starts out empty (has no mappings),
// and that all calls to `vmiter::map` succeed.
// After this function completes, for any va with
// 0 <= va < MEMSIZE_VIRTUAL, dst and src should map that va
// to the same pa with the same permissions.
...
}
The key here is to use two vmiter
objects with synchronized addresses,
and to copy the physical addresses and permissions from one vmiter
into
the other.
void copy_mappings(x86_64_pagetable* dst, x86_64_pagetable* src) {
vmiter srcit(src, 0);
vmiter dstit(dst, 0);
for (; srcit.va() < MEMSIZE_VIRTUAL; srcit += PAGESIZE, dstit += PAGESIZE) {
dstit.map(srcit.pa(), srcit.perm());
}
}
The fork
system call, which you’ll implement in the pset, is the original
system call that Unix-based operating systems used to start new processes.
fork
works by creating a new process, called the child process, that is
essentially a copy of the parent process (the process that called fork
).
The child process has a copy of the parent process’s memory, as well as its
registers and other state. After the system call completes, then, there are
two processes whose contents of memory are identical. The memory contents
quickly diverge, though: changes to the child process’s memory are not visible
in the parent’s memory or vice versa.
EXERCISE D2. Does copy_mappings
suffice to implement the memory copying
required by fork
? Why or why not?
It isn’t suitable on its own, because it copies mappings, not
memory. When copy_mappings
completes, the destination page table
dst
maps the same physical memory as the source page table src
,
rather than a copy of the source’s visible memory. A full fork
solution
will have some features of copy_mappings
as well as other code to handle
copying memory data.
EXERCISE D3. Use vmiter
and ptiter
to implement the following function.
void free_everything(x86_64_pagetable* pt) {
// Free the following pages by passing their kernel pointers
// to `kfree(void*)`:
// 1. All memory accessible via unprivileged mappings in `pt` from virtual
// addresses in [0,MEMSIZE_VIRTUAL).
// 2. All page table pages that are part of `pt`.
...
}
void free_everything(x86_64_pagetable* pt) {
for (vmiter it(pt, 0); it.va() < MEMSIZE_VIRTUAL; it += PAGESIZE) {
if (it.user()) {
kfree(it.kptr());
}
}
for (ptiter it(pt); !it.done(); it.next()) {
kfree(it.kptr());
}
kfree(pt);
}
The exit
system call allows a process to stop executing. All memory
belonging to or representing that process must be returned to the kernel for
reuse. This will include some memory that is accessible only to the kernel,
such as memory storing the process’s page table.
EXERCISE D4. Does free_everything
suffice to implement the memory
freeing required by exit
? Why or why not?
Yes, with one exception: It’s most likely a mistake to free the
user-visible page at 0xB8000, because that is the console—the shared
memory representing the screen—and it was never allocated.
Offline Part E: Spawn
This and the following parts won’t be covered in section by default. They are
optional practice to prepare for the problem set or exams. We suggest groups
of students get together and work through them on their own.
In the first part, we start a new process in WeensyOS. Starting a new process
requires a couple things: choosing a struct proc
member of the ptable
array, initializing memory, initializing registers, and marking the process as
runnable. The fork
system call in pset 3 phase 5
starts a process by copying an already-running process, but other interfaces
are possible. The spawn
interface, for example, starts a new process from
scratch.
EXERCISE E1. The p-spawn.cc
process tries to start another process—a
copy of the p-alice
program—by calling sys_spawn("alice")
. Use make run-spawn
(or make run-console-spawn
, etc.) to run this program.
- The system call is not working. How can you tell?
- Complete the system call implementation so that
p-spawn.cc
successfully
starts a new process.
- Modify
p-spawn.cc
so that it tries to start two copies of alice
. What
happens and why? How could you fix this? Refer to the phases of pset 3.
- Does your implementation of
syscall_spawn
check its arguments for
validity? If not, how would you change it to do this?
Offline Part F: Confused deputy
A confused deputy attack occurs when a low-privilege attacker convinces a
privileged “deputy” to complete an attack on its behalf. In the context of
operating systems, a process is unprivileged, while the kernel has full
privilege. However, a system call asks the kernel to perform an operation on
behalf of a process, making the kernel act as a privileged deputy. A confused
deputy attack occurs if the process, by invoking system calls, can somehow
convince the kernel to perform a function that violates process isolation.
EXERCISE F1. p-eve.cc
and p-alice.cc
may be familiar from class. Use
make run-friends
to run Alice and Eve together.
- It is possible to change one argument of one system call in
p-eve.cc
to execute a successful confused deputy attack that breaks the
kernel or otherwise prevents Alice from running. What is the system call?
What is the confused deputy attack?
- Complete the attack.
- Modify the kernel’s system call checking to prevent the attack. The kernel
might kill the Eve process upon detecting the attack, or (better perhaps)
return -1 from the relevant system call.