getrandom Character Device
Today I read this article titled Myths about
/dev/urandom. I stumbled upon
it when trying to figure out why writing /dev/random
to a hard disk
was taking so long, and I learned a few interesting things. The most
important being to always use /dev/urandom
because it doesn’t block
and it is just as secure.
I also learned about a syscall that behaves similar to /dev/urandom
but will block when there isn’t enough initial entropy: getrandom
.
I took this opportunity to learn how to create my own character device
that will output getrandom
bytes indefinitely.
The idea is simple: create a character device file and write a kernel
module for it to return the output of getrandom
. As I found out,
however, calling a syscall from a kernel module isn’t possible or
encouraged. Syscalls are, after all, for user space programs to
execute kernel code. So after I learned how to create a kernel
module for a character device, I found a way to get it working using
libfuse.
Creating a Character Device File
Creating a character device file was pretty easy, in fact the command
mknod
does just that! The only thing I needed to do was choose a
major and minor number, which is basically an ID for the device. A list
of major numbers can be found at /proc/devices
. I chose 1
to match
/dev/urandom
. Then the obvious choice for the minor number is 1337
.
$ sudo mknod xrandom c 1 1337
Creating a Kernel Module
A kernel module is essentially a way to dynamically load software into the kernel. One of its uses is for device drivers, and you can write software to handle when a character device is read from or written to.
Below is the full module and how to compile it. Note that I omitted the file opening and closing operations.
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/unistd.h>
#include <linux/syscalls.h>
static struct class *dev_class;
static struct cdev gr_cdev;
static int __init gr_driver_init(void);
static void __exit gr_driver_exit(void);
static ssize_t gr_read(struct file *filp, char __user *buf, size_t len, loff_t *off);
static ssize_t gr_write(struct file *filp, const char *buf, size_t len, loff_t *off);
static struct file_operations fops = {
.owner = THIS_MODULE,
.read = gr_read,
.write = gr_write,
};
// Major + minor number
dev_t dev = MKDEV(1, 1337);
// Initialize kernel module (insmod)
static int __init gr_driver_init(void) {
if (register_chrdev_region(dev, 1, "mem")) {
printk(KERN_ERR "[xrandom] could not register character device\n");
return -1;
}
cdev_init(&gr_cdev, &fops);
if ((cdev_add(&gr_cdev, dev, 1)) < 0) {
printk(KERN_INFO "[xrandom] could not add the device to the system\n");
goto r_class;
}
if ((dev_class = class_create(THIS_MODULE, "gr_class")) == NULL) {
printk(KERN_INFO "[xrandom] could not create the struct class\n");
goto r_class;
}
if (device_create(dev_class, NULL, dev, NULL, "gr_device") == NULL) {
printk(KERN_INFO "[xrandom] could not create the device\n");
goto r_device;
}
printk(KERN_INFO "[xrandom] kernel module inserted successfully\n");
return 0;
r_device:
class_destroy(dev_class);
r_class:
unregister_chrdev_region(dev, 1);
return -1;
}
// Cleanup kernel module (rmmod)
static void __exit gr_driver_exit(void) {
device_destroy(dev_class, dev);
class_destroy(dev_class);
cdev_del(&gr_cdev);
unregister_chrdev_region(dev, 1);
printk(KERN_INFO "[xrandom] kernel module removed successfully\n");
}
static ssize_t gr_read(struct file *filp, char __user *buf, size_t len, loff_t *off) {
if (len > 1) {
len = 1;
}
/* fair dice roll */
return copy_to_user(buf, "4", len) ? -EFAULT : len;
}
static ssize_t gr_write(struct file *filp, const char *buf, size_t len, loff_t *off) {
/* do not return 0 */
return len;
}
// Link callback functions
module_init(gr_driver_init);
module_exit(gr_driver_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("xrandom driver");
MODULE_VERSION("1.0");
MODULE_AUTHOR("miccah");
obj-m += xrandom.o
KDIR = /lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(shell pwd) modules
clean:
make -C $(KDIR) M=$(shell pwd) clean
install: all
sudo insmod xrandom.ko
uninstall:
sudo rmmod xrandom
Fuse and Cuse
As I mentioned earlier, we cannot perform a syscall
from a kernel module. Instead, I got it working with
libfuse, or more accurately cuse
(character device in userspace).
FUSE (Filesystem in Userspace) is an interface for userspace programs to export a filesystem to the Linux kernel. We can register callbacks in userspace for character device reading and writing. In this way, we can call the syscall from userspace and still have it linked to our special device character file.
Below is the C program I modified from the cuse
example.
It registers a callback with libfuse for our major and minor numbers.
To unregsiter it, kill the process that was spawned by the program
(found with ps aux | grep cuse
).
#define FUSE_USE_VERSION 31
#include <cuse_lowlevel.h>
#include <fuse_opt.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/random.h>
static void gr_read(fuse_req_t req, size_t size, off_t off,
struct fuse_file_info *fi) {
(void)fi;
if (size > 256) {
size = 256;
}
char buf[256];
getrandom(&buf, size, 0);
fuse_reply_buf(req, buf, size);
}
static void gr_write(fuse_req_t req, const char *buf, size_t size,
off_t off, struct fuse_file_info *fi) {
(void)fi;
fuse_reply_write(req, 0);
}
struct cusexmp_param {
unsigned major;
unsigned minor;
char *dev_name;
};
static const struct cuse_lowlevel_ops gr_clop = {
.read = gr_read,
.write = gr_write,
};
int main(int argc, char **argv) {
struct cusexmp_param param = { 1, 1337, "xrandom" };
const char *dev_info_argv[] = { "DEVNAME=xrandom" };
struct cuse_info ci;
memset(&ci, 0, sizeof(ci));
ci.dev_major = param.major;
ci.dev_minor = param.minor;
ci.dev_info_argc = 1;
ci.dev_info_argv = dev_info_argv;
return cuse_lowlevel_main(argc, argv, &ci, &gr_clop, NULL);
}
$ gcc -Wall xrandom-cuse.c $(shell pkg-config fuse3 --cflags --libs) -o cuse
$ sudo ./cuse
Conclusion
Even though there is a lot of technical knowledge for writing kernel modules, the concept is pretty simple. Because everything in Linux is a file, we are simply writing handlers for operations on special files.
Also, I think it will be fun to write my own device driver for my creations now that I know how!