iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🕌

Fun with Linux Kernel Development (Part 1): Triggering a Kernel Panic on `rm -rf /`

に公開

Introduction

There are many things to do to learn Linux kernel development. A common approach is to learn about individual components and then build something actually useful. While this works if you do it properly, it is a very sober process and can be boring for anyone who isn't deeply interested in the kernel. Therefore, I thought it would be interesting to write an article that aims to implement a specific function using methods unique to the kernel... so I decided to write the first installment here.

The target audience is those with development experience in C-like programming languages. It's even better if you understand C pointers. If possible, I'd also like you to have some basic knowledge of the OS kernel.

Background

From the birth of UNIX to the present day, there has been no end to incidents of blowing away all files with rm -rf /. The rm command in GNU coreutils has a preserve-root option enabled by default, which treats operations on the root directory ("/") as a special case to prevent tragedy from occurring easily. However, humans sometimes accidentally add the --no-preserve-root option, disabling this feature even at such times.

Therefore, I decided to create a fail-safe function that causes a kernel panic (displaying what is known as a Blue Screen in Windows) when a user tries to execute rm -rf /.

Target Kernel

  • linux v5.3

The source code for this kernel can be obtained with the following commands:

$ git clone -b v5.3 --depth 1 https://github.com/torvalds/linux
...                    # Please note that this will consume several GB of disk space
$ git checkout v5.3
...
$ 

Changes

The content of the patch file 0001-panic-if-user-tries-to-run-rm-rf.patch, which shows the changes for this feature, is as follows (the contents will be explained later). The license is GPL v2.

From 27a3af9519c8b07c527bd48ef19b4baf9f6d4a9c Mon Sep 17 00:00:00 2001
From: Satoru Takeuchi <satoru.takeuchi@gmail.com>
Date: Sun, 6 Oct 2019 15:53:34 +0000
Subject: [PATCH] panic if user tries to run rm -rf /

---
 fs/exec.c | 37 +++++++++++++++++++++++++++++++++++++
 1 file changed, 37 insertions(+)

diff --git a/fs/exec.c b/fs/exec.c
index f7f6a140856a..8d2c1441b64c 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -1816,6 +1816,43 @@ static int __do_execve_file(int fd, struct filename *filename,
        if (retval < 0)
                goto out;

+       // Panic if user tries to execute `rm -rf /`
+       if (bprm->argc >= 3) {
+               struct page *page;
+               char *kaddr;
+               char rm_rf_root_str[] = "rm\0-rf\0/";
+               char buf[sizeof(rm_rf_root_str)];
+               int bytes_to_copy;
+               unsigned long offset;
+
+               bytes_to_copy = min(sizeof(rm_rf_root_str), bprm->p % PAGE_SIZE);
+               page = get_arg_page(bprm, bprm->p, 0);
+               if (!page) {
+                       retval = -EFAULT;
+                       goto out;
+               }
+               kaddr = kmap(page);
+               offset = bprm->p % PAGE_SIZE;
+               memcpy(buf, kaddr + offset, bytes_to_copy);
+               kunmap(page);
+               put_arg_page(page);
+
+               if (bytes_to_copy < sizeof(rm_rf_root_str)) {
+                       page = get_arg_page(bprm, bprm->p + bytes_to_copy, 0);
+                       if (!page) {
+                               retval = -EFAULT;
+                               goto out;
+                       }
+                       kaddr = kmap(page);
+                       memcpy(buf + bytes_to_copy, kaddr, sizeof(rm_rf_root_str) - bytes_to_copy);
+                       kunmap(page);
+                       put_arg_page(page);
+               }
+
+               if (!memcmp(rm_rf_root_str, buf, sizeof(rm_rf_root_str)))
+                       panic("`rm -rf /` is detected");
+       }
+
        would_dump(bprm, bprm->file);

        retval = exec_binprm(bprm);
--
2.17.1

How to run it

First, a word of caution. Perform this on a VM that you don't mind breaking. If the operation verification test fails, all files could be blown away, and even if it succeeds, data residing in dirty page caches will be lost.

First, apply the patch file as follows, then build, install, and reboot.

$ git apply 0001-panic-if-user-tries-to-run-rm-rf.patch
...                                                 # Apply the patch needed to add the feature
$ sudo apt install kernel-package flex bison libssl-dev
...                                                 # Install packages required for kernel building
$ make localmodconfig
...                                                 # Configure for the build. Press ENTER repeatedly if asked anything
$ make -j$(grep -c processor /proc/cpuinfo)
...                                                 # Build
$ sudo make modules_install && make install
...                                                 # Install the new kernel and its modules
$ sudo /sbin/reboot # Please modify accordingly if GRUB is set to boot a specific kernel next time

Confirm that the kernel version has changed after rebooting.

$ uname -r
5.3.0+
$ 

If it shows 5.3.0+, it's a success. The trailing "+" is automatically added for custom kernels.

Finally, execute that command.

$ rm -rf /

If no response returns, it's a success. If you are watching the machine's console, you can see the kernel logs from the panic. If you ran the command via a terminal emulator in a GUI or through ssh, the screen will likely just appear to freeze.

For reference, here is the execution result from my environment.

Please note that this feature does not consider who executed rm -rf /; it will trigger a kernel panic without mercy even if a general user without permission to delete files under the root directory runs this command.

After this, reboot the machine and, if necessary, delete the kernel installed by this article or change the default kernel used by GRUB.

Explanation of the Patch

First, I'll briefly explain the code before applying the patch.

  1. A user process calls the execve() system call to execute a new program.
  2. The handler function in the kernel that processes this system call starts running, and in the process, it calls the __do_execve_file() function, which is also in the patch.
  3. Information about command-line arguments and environment variables given to the execve() system call is extracted and read into the kernel's memory. copy_strings(bprm->argc, argv, bprm) in the patch corresponds to the content of the command-line arguments.
  4. The actual processing of the execve() system call is performed. The currently running process is replaced with the new program, and execution starts from that program's entry point.

This patch adds a process between steps 3 and 4 that causes a kernel panic if the arguments of the execve() system call passed by the user are rm -rf /.

Now, I'll explain the patch with line numbers.

  1 From 27a3af9519c8b07c527bd48ef19b4baf9f6d4a9c Mon Sep 17 00:00:00 2001
  2 From: Satoru Takeuchi <satoru.takeuchi@gmail.com>
  3 Date: Sun, 6 Oct 2019 15:53:34 +0000
  4 Subject: [PATCH] panic if user tries to run rm -rf /
  5
  6 ---
  7  fs/exec.c | 37 +++++++++++++++++++++++++++++++++++++
  8  1 file changed, 37 insertions(+)
  9
 10 diff --git a/fs/exec.c b/fs/exec.c
 11 index f7f6a140856a..8d2c1441b64c 100644
 12 --- a/fs/exec.c
 13 +++ b/fs/exec.c
 14 @@ -1816,6 +1816,43 @@ static int __do_execve_file(int fd, struct filename *filename,
 15         if (retval < 0)
 16                 goto out;
 17
 18 +       // Panic if user tries to execute `rm -rf /`
 19 +       if (bprm->argc >= 3) {
 20 +               struct page *page;
 21 +               char *kaddr;
 22 +               char rm_rf_root_str[] = "rm\0-rf\0/";
 23 +               char buf[sizeof(rm_rf_root_str)];
 24 +               int bytes_to_copy;
 25 +               unsigned long offset;
 26 +
 27 +               bytes_to_copy = min(sizeof(rm_rf_root_str), bprm->p % PAGE_SIZE);
 28 +               page = get_arg_page(bprm, bprm->p, 0);
 29 +               if (!page) {
 30 +                       retval = -EFAULT;
 31 +                       goto out;
 32 +               }
 33 +               kaddr = kmap(page);
 34 +               offset = bprm->p % PAGE_SIZE;
 35 +               memcpy(buf, kaddr + offset, bytes_to_copy);
 36 +               kunmap(page);
 37 +               put_arg_page(page);
 38 +
 39 +               if (bytes_to_copy < sizeof(rm_rf_root_str)) {
 40 +                       page = get_arg_page(bprm, bprm->p + bytes_to_copy, 0);
 41 +                       if (!page) {
 42 +                               retval = -EFAULT;
 43 +                               goto out;
 44 +                       }
 45 +                       kaddr = kmap(page);
 46 +                       memcpy(buf + bytes_to_copy, kaddr, sizeof(rm_rf_root_str) - bytes_to_copy);
 47 +                       kunmap(page);
 48 +                       put_arg_page(page);
 49 +               }
 50 +
 51 +               if (!memcmp(rm_rf_root_str, buf, sizeof(rm_rf_root_str)))
 52 +                       panic("`rm -rf /` is detected");
 53 +       }
 54 +
 55         would_dump(bprm, bprm->file);
 56
 57         retval = exec_binprm(bprm);
 58 --
 59 2.17.1
 60

At line 17, the arguments of execve() are stored in kernel memory. From here, the changes of this patch begin.

The command we want to detect is rm -rf /, and the number of arguments at this time is 3. In addition to this, bprm->argc contains the number of arguments passed to execve(). Therefore, an if statement with the condition bprm->argc >= 3 filters out unrelated command executions. I could have used == instead of >=, but even something like rm -rf / foo (where the number of arguments is 4) still leads to a disastrous result, so I did it this way.

Command-line arguments are arranged in a specific area within kernel memory as data separated by null characters ('\0'). For example, if you run the echo command with the string hello as an argument, it becomes data like "echo\0hello". Note that the command name is also one of the arguments. In the case of rm -rf /, it should be "rm\0-rf\0/". Inside the aforementioned if statement, line 22 sets the data as it "should be," compares it with the actual argument values at line 51, and if they match, triggers a kernel panic at line 52.

Retrieving data from kernel memory is a bit tricky. This corresponds to lines 27-49. The required data exists across one or two pages of memory (the unit by which the CPU manages memory through a feature called virtual memory; 4KB on the x86_64 architecture). In most cases, it fits within one page. In this case, it ends with just lines 27 to 37. If it spans two pages, lines 39 to 49 are executed. Here, I'll only write about the case where the data fits in one page.

Lines 27-37 have the following meanings:

  • Line 27: The size of the data to be retrieved from kernel memory. If the data fits in one page, it is 9 bytes.
  • Lines 28-32: Obtain data called a page structure that points to the page containing the data, and handle any errors. This page structure is released at line 37.
  • Line 33: Obtain the address of the page pointed to by the page structure. It is a rule to call the corresponding kunmap() for addresses obtained with kmap(), as seen in line 36.
  • Lines 34 and 35: Copy the necessary data into buf.

I've explained this in a hurry, but for now, I think it's fine if you just read it with a general feeling and understand it roughly.

Conclusion

I wrote this as the first installment, but whether there will be a second one or later depends on the response to the article and my motivation.

Discussion