CS 45 Lab 2: Implementing System Calls

Checkpoint: Monday, February 19 (3-minute demos during lab)
Due: Sunday, February 25 @ 11:59 PM

Handy References:


Week 1 Lab Audio
Week 2 Lab Audio

Lab 2 Goals:

Overview

For this lab, you'll be adding two new system calls to the Linux kernel. The first will retrieve the current time and return it by copying memory into the calling userspace process. The second will modify the PCB (in Linux, the task_struct) of a chosen process so that it no longer appears in the process list.

Requirements

  1. System call 1: getcurrenttime

    You should implement a getcurrenttime system call that takes two parameters: a flag that specifies whether or not to print when executing and a pointer to a struct timespec. Your implementation should print a simple debugging status message using printk() if the flag evaluates to true. It should then verify that the pointer given in the timespec is valid, and if so, lookup the current time and copy it into the pointer's destination.

    To a userspace application, the call might look like:
    /*
     *  pflag: if non-zero print inside the system call (using printk)
     *  tspec: pointer passed to your system call, the current time
     *         will be "returned" through this parameter
     *  returns: 0 on success, -1 on error
     */
    long getcurrenttime(int pflag, struct timespec  *tspec)
    
    On success, your system call should return 0. If it fails, it should return an error code that describes what went wrong. You are responsible for detecting and reporting the following errors:
    • Verifying that the struct timespec pointer is writable userspace memory using access_ok. If this fails, you should return the constant -EINVAL (invalid argument).
    • Copying the time value to the userspace timespec struct using copy_to_user. If this fails, you should return the constant -EFAULT (bad address).

    Note that in userspace, you will not see the error value directly in the return value of your system call. Instead, you'll see -1 and the errno variable will be set to the value your system call returned. This behavior will allow you to use perror() to decode the error message.

    The Linux kernel has many functions for dealing with time. A good place to start looking at the available functions is in kernel/time/, which has files like timekeeping.c and time.c. The corresponding headers, which you'll need to include in your system call's .c file to use the time functionality, are in include/linux. The ktime.h header seems to include most of the other headers that may be of interest.

  2. System call 2: stealth

    The ps ax command will print a list of all processes on the system. You can use grep to help narrow the list (e.g., ps ax | grep vim to show only processes with 'vim' in the name). The leftmost column of the output is the process id (PID), and the rightmost column is the process's name. In Linux, ps reads the information about all processes from a special pseudo-file system in /proc. If you execute ls /proc, you'll see lots of directories named with a number, each of which corresponds to a PID. The files in those directories contain information about the corresponding processes. Note that these files are not stored permanently on any disk. The info is all stored in the OS's data structures, and the /proc file system is simply the interface it uses to make that information available to users.

    You should implement a stealth system call that will hide (or unhide, if called a second time) a process from being listed in the /proc file system. By hiding the process in /proc, it will no longer show up in ps's output list.

    To a userspace application, the call might look like:
    /*
     *  pid: the process id of the process whose stealth status should be toggled
     *  returns: 0 on success, -1 on error
     */
    long stealth(pid_t pid)
    
    On success, your system call should return 0. If it fails, it should return an error code that describes what went wrong. You are responsible for detecting and reporting the following errors:
    • Verifying that the provided PID matches a real process by looking for the corresponding process's task_struct. If this fails, you should return the constant -ESRCH (no such process).

    To implement your stealth system call, you'll need to add a flag representing a process's stealth status to the Linux task_struct, which can be found on line 1390 of include/linux/sched.h. You'll also need to update the INIT_TASK macro in include/linux/init_task.h to initialize your new flag when a new task_struct gets created. Your stealth system call should toggle this flag's value. When accessing instances of the task_struct, you need to be holding locks. I would suggest looking around at other places where task_structs are used to see how the locking is used.

    You will also need to edit the implementation of the /proc file system so that it skips over any processes whose stealth flag is set. The /proc implementation lives in the fs/proc directory, with fs/proc/root.c being the "main" file (this is unlikely to be the file where you make your changes, but it should help you to get started with discovering how the file system works).

  3. Userspace test applications

    Having implemented your system calls, you need to test them! You should write two small userspace test applications, one for each of your two system calls, to test that each works as intended. Your test programs should attempt to test as many cases as possible (e.g., calls that will generate errors in addition to correct runs).

    In the kernel, each system call is given a unique integer, and you'll need to know the integer assigned to your system calls (see "Defining System Calls" below). To invoke your new system call, you can use the syscall() function. The first parameter is the system call number, and then you can pass as many additional arguments as your system call needs. For example, to call getcurrenttime(), you could invoke:
    int pflag = 1;
    struct timespec ts;
    
    int result = syscall(326, pflag, &ts);  // Assumes 326 is the number you assigned to getcurrenttime()
    
    Since the above method of making system calls is ugly, I would suggest defining a simple macro for each call to give it a more reasonable name:
    #define getcurrenttime(arg1, arg2) syscall(326, arg1, arg2)
    
    /* Now you can call getcurrenttime() normally: */
    int result = getcurrenttime(pflag, &ts);
    

Checkpoint

For the checkpoint, you should be able to demonstrate that:

Defining System Calls

To add a new system call, you'll need to define it in a few places:

  1. Add a new system call number to the kernel.
    The Linux kernel associates a unique integer with each system call, so you'll need to assign a new value for each of your two system calls. For the x86 architecture, the values are stored in a table in the file arch/x86/entry/syscalls/syscall_64.tbl. You can use any integer that isn't already in use (the next free value is 326). You will only be operating your kernel in 64-bit mode, so the 32-bit/64-bit distinction doesn't have much impact. You should follow the same naming format for the other columns (e.g., name the system call normally and then again with sys_ prepended to the front of it).

  2. Add your system call prototype to the syscalls header file.
    Next, you'll need to declare your system call in the file include/linux/syscalls.h. The function names should be prefixed with sys_, and they should return the same type as all the others (asmlinkage long). For pointers coming from userspace in parameters (e.g., the struct timespec in getcurrenttime), you need to add "__user" to the type declaration. Take a look at the other system calls for an example.

  3. Add the code for your system call.
    Finally, you'll need to implement your system call! You should add a new .c file to the kernel directory (e.g., kernel/stealth.c). In that file, you'll need to #include kernel headers to get access to helper functions, like printk(). I would suggest always including:
    #include <linux/errno.h>    // For error constants.
    #include <linux/kernel.h>   // For printk().
    #include <linux/syscalls.h> // For syscall macros.
    
    Each of your system calls may also need other headers that are specific to their purpose (e.g., getcurrenttime will need asm/uaccess.h for the access_ok and copy_to_user functions).

    You can define your system call using the SYSCALL_DEFINEX macro, where X is the number of parameters your system call takes. For example, suppose you were adding a system call named testcall that takes two arguments, an integer and a pointer to a userspace string. Your definition would look like:
    SYSCALL_DEFINE2(testcall, int, int_param, char __user *, string_param) {
      /* Body of system call implementation goes here.*/
    
      /* If an error occurs, return a negative constant that starts with E
       * (e.g., EINVAL or ESRCH). */
      if (error) {
        return -EINVAL;
      }
    
      /* On success, return zero. */
      return 0;
    }
    
    After implementing the system call, you need to make sure it gets compiled and used. Since you added a .c file, you'll need to tell the kernel's build system to build and incorporate the corresponding .o file. The easiest way is to add file.o to kernel/Makefile in the obj-y section at the top.

Tips

Submitting

Please remove any excessive debugging output prior to submitting.

To submit your code, commit your changes locally using git add and git commit. Then run git push while in your lab directory.

Please ONLY submit any Linux source files that you have modified or added along with a README.md file containing the paths of those files within the source tree. DO NOT submit the entire Linux kernel source tree. Please add any userspace testing code to the "userspace" directory.

For example, suppose you:

You should submit test-feature.c in the provided userspace directory. You should submit sched.h and newfile.c, along with a README.md, in the root of the repository. The README.md should contain the path of each file (relative to your kernel's base directory), e.g.:

sched.h:   include/linux/sched.h
newfile.c: kernel/newfile.c