I came across the following Toot today on Mastodon:

A++-grade bait

and I immediately got nerd-sniped into trying this. I've seen the code for some small Linux kernel modules before, I know that kernel modules can create files and filesystems, so how hard could it be?

Very, as it turns out. At least for someone like me who does not know what they are doing.

Essentially I would need to build a Linux kernel modules that defines a new device, /dev/scream. When someone reads from this device, all they get back is an infinite stream of 'a' bytes, with the occasional 'A' byte.

Building a Linux kernel module is supposed to be quite simple. You import a few headers, declare a startup and a shutdown function, and that creates a .ko file that you can load into the kernel.

The most basic module looks something like this:

#include <linux/module.h>

MODULE_LICENSE("GPL");

static int __init my_module_init(void) {
    return 0;
}

static void __exit my_module_exit(void) {
}

module_init(my_module_init);
module_exit(my_module_exit);

In the module startup, you can do additional things, which you should effectively undo when you exit.

This gets built by a Makefile which imports most of its logic from the Linux kernel headers code. Mine looked like this:

KERNEL_DIR=/lib/modules/$(shell uname -r)/build

obj-m += scream.o

all:
	make -C $(KERNEL_DIR) M=$(PWD) modules
clean:
	make -C $(KERNEL_DIR) M=$(PWD) clean

Note that the name of the object has to match the name of the source file, which matches the name of the extension. i.e., foo.c becomes foo.o which produces foo.ko.

I have yet to figure out how to change that, how to include additional source files, and how to move things around into subdirs (i.e. src/foo.c produces obj/foo.o which results in bin/foo.ko).

After a lot of research and finding outdated examples (it turns out that over time the kernel function signatures have changed) and even some fun Stack Overflow examples with subtle errors that don't work (and to think that Copilot/GPT gets trained on this junk!!), I eventually had a working sample that created a new device class, created a new device in that class, and then handled function calls for that device. This paragraph alone was about 2-3 hours of experimentation and failure.

And just when I thought I was done, I found based on one random comment on a forum, that there is an easier way to do this all with miscdevice. Since I'm building something very simple and don't need any additional complexities that can be done by the more powerful APIs that I was using, I can remove a whole bunch of that complexity with simpler APIs.

The other advantage is that when creating a miscdevice I can specify a mode for the file - i.e. rw-rw-r-- style permissions for my device. This was quite useful because with the existing APIs only root could use my device, and I wanted any user to hear the computer screen. The only other way I could find would be to chmod it myself but that felt hacky.

So if you are trying to build a custom device driver on Linux, and it is fairly simplistic such as a joke/meme device, I highly recommend looking into the Miscellaneous Devices API.

For me, my device was simple. Opening the file doesn't need to do anything. Closing the file doesn't need to do anything. When reading the file, since I want to scream indefinitely, I just want to completely fill the output buffer.

The actual value (if you can call it that) in my driver comes down to one simple, unoptimised loop, which writes one byte at a time back from kernel space to user space:

static ssize_t scream_device_read(struct file *file, char __user *buf, size_t count, loff_t *offset) {
    uint8_t rand;

    for (size_t i = 0; i < count; i++)
    {
        get_random_bytes(&rand, sizeof(rand));

        char value;

        if (rand > 200) {
            value = 'A';
        } else {
            value = 'a';
        }

        if (copy_to_user(buf + i, &value, 1)) {
            return -EFAULT;
        }
    }

    return count;
}

Then with everything hooked up correctly, it all works:

yaakov@ubuntu-vm:~/Desktop/dev-scream$ ls -l /dev/scream
cr--r--r-- 1 root root 10, 122 Aug 11 18:02 /dev/scream

yaakov@ubuntu-vm:~/Desktop/dev-scream$ head -c50 /dev/scream && echo
aaaAAaAaAaaaaaaaaaaaaaaaaaAaaaAaaAAaaaAaaAaaaaaaAa

You can find the full code for this on my GitHub or on my little ForgeJo instance.