As with previous assignments, we wil be using GitHub to distribute skeleton code and collect submissions. Please refer to our Git Workflow guide for more details. Note that we will be using multiple tags for this assignment, for each deliverable part.
NOTE: If at all possible, please try to submit using x86. If one of your group
members owns an x86 machine, test on that machine prior to submitting, and do not
commit a .armpls
file. This will make grading much easier for us.
For students on arm64 computers (e.g. M1/M2 machines): if you want your
submission to be built/tested for ARM, you must create and submit a file called
.armpls
in the top-level directory of your repo; feel free to use the
following one-liner:
cd "$(git rev-parse --show-toplevel)" && touch .armpls && git add -f .armpls && git commit .armpls -m "ARM pls"
You should do this first so that this file is present in all parts.
There is a script in the skeleton code named run_checkpatch.sh
. It is a
wrapper over linux/scripts/checkpatch.pl
, which is a Perl script that comes
with the Linux kernel that checks if your code conforms to the kernel coding
style.
Execute run_checkpatch.sh
to see if your code conforms to the kernel style –
it’ll let you know what changes you should make. You must make these changes
before pushing a tag. Passing run_checkpatch.sh
with no warnings and no errors
is required for this assignment.
In addition to the pristine Linux kernel source tree (now under linux/
) we’ve
provided a patch file which will create the syscall stubs for you. You will need
to apply this patch to your repo.
The patch is under the following path:
patch/farfetch.patch
You can use git apply
to apply this patch. First, check which files will be
modified by the patch:
$ git apply --stat patch/farfetch.patch
You should also inspect what the patch is doing by reading the diffs inside. Finally, you can apply the patch with the following:
$ git apply patch/farfetch.patch
Now, when you run git status
, you should see some files modified, as well as
some .c
and .h
files added. After verifying that these changes worked as
intended, commit them.
Build your kernel. Make sure you’re building with a local version that is
different from your fallback (-cs4118
), so you don’t overwrite it; set your
local version to your UNI (i.e. -<uni>-HW7
).
Now, when you build your kernel, you should have the farfetch()
syscall stub
in your kernel.
The syscall you will implement has a cmd
parameter whose possible values
(defined by an enum) are unique to the syscall, and which must be known by the
caller. This means that the enum definition needs to be available in both kernel
and user land. You’ll need to install the farfetch header
(include/uapi/linux/farfetch.h
) from the kernel source tree to userspace.
Once you’ve built your farfetch()
-stubbed kernel, run the following command:
# make headers_install INSTALL_HDR_PATH=/usr
This command will install the headers found under include/uapi/
in your Linux
source tree into /usr/include/
. Now you should be able to #include
<linux/farfetch.h>
from userspace! Additionally, the syscall number should be
available as __NR_farfetch
from #include <asm-generic/unistd.h>
. Try
compiling the userspace utility (see below) to make sure this works.
farfetch
: Fetching Pages from AfarFor this assignment, you will be implementing farfetch()
, a system call that
allows you to manipulate the memory of a specified process. The syscall number
for farfetch()
is 505, and it should be implemented as a dynamically loadable
module.
The function prototype for farfetch()
is the following:
long farfetch(unsigned int cmd, void __user *addr, pid_t target_pid,
unsigned long target_addr, size_t len);
farfetch()
will take in five arguments:
cmd
: indicates whether to read or write the remote memory by specifying
FAR_READ
or FAR_WRITE
, respectively (defined in the UAPI header)
addr
: a pointer to the caller’s user-space buffer
target_pid
: the PID of the process whose memory is to be fetched
target_addr
: the starting memory address in the target process’s virtual
address space
len
: the maximum number of bytes to copy
On success, farfetch()
should return the number of bytes copied (<= len
).
If the user issuing the syscall is not root, fail with the errno value
EPERM
.
If cmd
is not FAR_READ
or FAR_WRITE
, fail with the errno value EINVAL
.
If the specified PID does not exist, fail with the errno value ESRCH
.
If copying to/from addr
fails, fail with the errno value EFAULT
.
If target_addr
is not a valid user-space address, or is unmapped in the
target virtual address space, fail with the errno value EFAULT
.
You will be implementing farfetch()
in this part, but with a few simplifying
limitations; most significantly, you will only be dealing with the single
physical page that is associated with target_addr
, so there’s no need to worry
about traversing to any subsequent pages. You will copy to/from this page up
until either len
bytes or the end of the page (whichever comes first).
There is one restriction on your implementation for this part: you may NOT use
get_user_pages_remote()
/pin_user_pages_remote()
, nor anything which invokes
them. You may reference their implementation for performing a page walk, but
note that the relevant bits are buried in logic that deals with things you don’t
need to worry about (traversing arbitrary address ranges, huge pages, special
mappings, faulting in pages, etc.)—if your module contains such extraneous code,
it will incur a steep deduction. Every line you write should be with purpose, so
avoid haphazardly copy-pasting functions or large chunks of code.
Consequently, you will need to manually perform the 5-level page walk. Some additional simplifying limitations:
If you encounter an entry which is not present in memory, just report
EFAULT
.
Do NOT allow writing to a non-writable PTE; in the event you are asked to do
so by FAR_WRITE
, just report EFAULT
.
If performing a FAR_WRITE
, you should mark the modified page as dirty using
set_page_dirty_lock()
.
To determine if target_addr
is a valid user-space address, it is sufficient to
check against the end of the target process’s virtual address space, which is
evaluated by the TASK_SIZE_OF()
macro; anything >= TASK_SIZE_OF()
cannot be
a valid user address for the task.
Our recommendation is to start with the resources linked below before looking at kernel code, as those more directly get at what you need to implement the page walk.
Able to copy up to a page of memory into/out of any target process.
Doesn’t allow writing to any write-protected PTE.
Does NOT (even indirectly) invoke
get_user_pages_remote()
/pin_user_pages_remote()
.
No significantly extraneous code.
Proper error handling in all specified cases.
Test your implementation as described below.
After testing, answer the following in your written_answers.txt
:
Observe and explain any difference in behavior when using farfetchd
on the
provided mmap
target versus the malloc
target.
Hint: try fetching a full page (i.e. 4096 bytes); how many bytes are actually fetched in each case?
Hint: man mmap
.
Observe and explain any difference in behavior when using farfetchd
on the
provided mmap
target versus the fork
target (in both the parent and
child).
Observe and explain the behavior of farfetchd
on the strlit
target.
Try going through Session 2 without using setarch -R
, which
is used to disable ASLR for the process; that is, run the twecho
target
directly. Briefly describe what ASLR is, and explain how it affects finding
the argv
strings.
/proc/<pid>/maps
(as done in
Session 3) with and without setarch -R
.To submit this part, push the hw7p1handin
tag with the following:
$ git tag -a -m "Completed hw7 part1." hw7p1handin
$ git push origin master
$ git push origin hw7p1handin
For this part, we are lifting the main restriction of Part 1 and encouraging
that you use get_user_pages_remote()
. You can let the internal “GUP” logic
(belonging to the get_user_pages_*
family of functions) handle the details of
the walk.
The use of GUP logic provides the following functionalities which were not required in Part 1:
Deal with arbitrary address ranges (potentially spanning multiple pages)
Modify non-writable memory
Remember to mark any modified pages dirty (as in Part 1).
All the functionality of Part 1.
Able to read/write arbitrary address ranges (potentially > PAGE_SIZE
).
Able to write to non-writable memory (e.g. strlit
target).
Invoke get_user_pages_remote()
exactly once (there should be no need for
repeated calls, e.g. in a loop).
If get_user_pages_remote()
fails, relay the errno back to the user.
If it reports less than the requested number of pages, adjust the length of
the copy (< len
).
Answer the following in your written_answers.txt
:
Ensure that the behavior observed in Part 1 for the fork
target is
remedied; explain generally how the GUP logic handles this case. Feel free
to reference line numbers in mm/gup.c
.
FOLL_*
flag which is pertinent, see where
this is set.To submit this part, push the hw7p2handin
tag with the following:
$ git tag -a -m "Completed hw7 part2." hw7p2handin
$ git push origin master
$ git push origin hw7p2handin
farfetchd
Hacker UtilityWe’ve provided a userspace utility to test your implementation, under the following path:
user/test/farfetchd/
In particular, farfetchd
takes a target PID, address, and maximum length, and
will execute your syscall up to two times; once to FAR_READ
from the target,
and then if you choose to modify any memory, once to FAR_WRITE
it.
You will need to install bvi
before using farfetchd
:
# apt install bvi
You will find the provided target programs useful for testing under the following path:
user/test/targets/
Though feel free to write your own for additional testing.
Linked below are some example shell sessions of testing with farfetchd
, using the
final Part 2 version. Note that the behavior will be different for Part 1 in some cases.
Implement the farfetch()
syscall in a kernel module using the function
pointer technique from HW4.
You will find a module stub in your skeleton repo at the path
user/module/farfetch/
. Implement your modularized system call here
in farfetch.c
.
Don’t modify the existing boilerplate code.
You should start your code in the farfetch()
function.
Feel free to define and call any more functions inside your module.
You can find the farfetch cmd
values (FAR_READ
/FAR_WRITE
) defined for
you in the Linux kernel source tree, under include/uapi/linux/farfetch.h
.
Remember to install these during the setup stage so that you can include
this file from userspace.
You do not have to worry about ensuring your solution is architecture-independent. That is, we will only test your solution on your specified architecture.
Include your answers to Part 1 and Part 2 questions in written_answers.txt
.
This part is optional and will not be graded.
In attempting to inspect the processes of another Linux system, you may find the victim is unable or unwilling to install your patched kernel. Our kernel patch was necessary to add the syscall interface by which our user-space hacker tool communicates with the kernel—but good news, there’s no need! Linux already allows for kernel modules to establish their own interfaces by which user-space may invoke kernel code, namely by registering a pseudo-device to be exposed on the filesystem. These device files are interacted with via standard I/O syscalls.
Every device driver has a major number which identifies it, and every
individual device (whether it’s a real non-pseudo device or not) has a minor
number which identifies it to the driver. For example, /dev/null
, /dev/zero
,
/dev/random
, and several other pseudo-devices all belong to same “devmem”
device driver within the Linux kernel, with the same major number but different
minor numbers. These are all implemented in
drivers/char/mem.c.
If you stat
these on the command line, you can see in the “Device type” field
that the major identifier (the first number) is the same between them,
confirming that they share that same driver.
You will change your kernel module to register a device driver which invokes
farfetch()
, instead of setting the global function pointer for the syscall to
use. To be safe, copy user/module/farfetch/
to a new directory
user/module/farfetch_p3/
, and make all changes there—do NOT submit any changes
to the original directory for your graded tags.
Reference this guide for a basic module example which registers such a driver:
https://lyngvaer.no/log/writing-pseudo-device-driver
You don’t need to worry about the “state control” global variables from the example—we don’t care to track whether our driver is “busy”.
Note how the guide does not use the device’s minor identifier; in our case,
we’re going to use it to identify the PID of the target process for
farfetch()
, s.t. each pseudo-device powered by the farfetch
driver will
read/write the memory of a specific process. You may use iminor(ino)
within
dev_open()
to get the minor number of the device being opened, which you may
treat as target_pid
. All you need to know about the struct inode *
for now
is that it corresponds to the filesystem path being opened—more to come in HW8!
As a consequence of this, we need to allocate as many minor numbers as possible for our driver; to do so, replace the guide’s register/unregister lines with the following:
major = __register_chrdev(0, 0, MINORMASK + 1, "farfetch", &fops);
...
__unregister_chrdev(major, 0, MINORMASK + 1, "farfetch");
Some hints:
fp->private_data
is maintained for the file descriptor across the
open/read/write/release operations. Our solution uses it to hold the struct
pid *
for identifying the target process; however you choose to do this, you
should NOT need to allocate any memory.
Set llseek
in the struct file_operations
to default_llseek
, which is a
global routine exported by the kernel; this will enable a user performing I/O
on our device file to lseek()
to any file position.
As in the guide, use the mknod
command to link a device on the filesystem with
our driver’s major identifier—unlike the guide, the minor number will be the PID
of the target process. Below is a sample session; notice how, since our device
file can be interfaced with via simple read/write I/O syscalls, we don’t need
any special C program to interface with it—just standard shell commands!
$ ../../test/targets/mmap &
[1] 16593
$ 0xffff9f88b000
$ grep farfetch /proc/devices
238 farfetch
$ sudo mknod -m 0666 farfetch-16593 c 238 16593
$ dd if=farfetch-16593 bs=1 skip=$((0xffff9f88b000)) count=20
Hey this is private!20+0 records in
20+0 records out
20 bytes copied, 0.000129289 s, 155 kB/s
$ echo -n HACKED | dd of=farfetch-16593 bs=1 seek=$((0xffff9f88b000))
6+0 records in
6+0 records out
6 bytes copied, 9.7956e-05 s, 61.3 kB/s
$ fg
../../test/targets/mmap
^CHACKEDis is private!
$ sudo rm farfetch-16593
There are some drawbacks to exclusively using the minor number for our PID—for
one thing, we have to deal with mknod
for each and every target process, which
is annoying. But more importantly, the minor identifier is actually limited to
20
bits
(since Linux’s dev_t
type is encoded to pack both major and minor), which
means its maximum value could be lower than the maximum PID value. To cover our
bases, let’s allow a user to set the underlying PID of their opened file
descriptor when the minor device number is 0 (which won’t correspond to an
actual process anyway).
The ioctl()
syscall is used to manipulate the underlying parameters of a
device. To enable our own interface, set unlocked_ioctl
in the struct
file_operations
s.t. a user may invoke ioctl()
on their file descriptor to
set its target PID.
Since we’ll be using our own special device controls, we do require a dedicated
C program to make the necessary IOCTL command before reading/writing. Provided
is user/test/farfetchd_p3/
which contains a version of farfetchd
that does
not use any special syscall, interfacing with /dev/farfetch
via
ioctl
/lseek
/read
/write
. Note that the “request” argument passed to
ioctl()
is ignored; our driver only understands one IOCTL request, so to keep
things simple, we just take the argument after “request” as the PID.
To test, we use mknod
to link /dev/farfetch
to our driver with device minor
number 0 (just once!):
$ grep farfetch /proc/devices
238 farfetch
$ sudo mknod -m 0666 /dev/farfetch c 238 0
And now we can use farfetchd
the same as before, on any process we like, for
as long as our module is inserted. Only now, our kernel module can be built
against anyone’s Linux kernel, assuming the version is close enough to not break
our code. Happy hacking :^)
To submit this part, push the hw7p3handin
tag with the following:
$ git tag -a -m "Completed hw7 part3." hw7p3handin
$ git push origin master
$ git push origin hw7p3handin
Below is some online reading material that you may find helpful for this assignment:
For official Linux documentation on memory management:
The Farfetch’d assignment and reference implementation were designed and implemented by the following TAs of COMS W4118 Operating Systems I, Spring 2022, Columbia University:
Last updated: 2024-04-11