Frontend Labs
⋯

51. Character device drivers: file_operations, cdev, and copy_to/from_user

A character device driver is what turns module_init from Section 50 into something a user-space program can open()/read()/write() as /dev/something — the kernel-side implementation of every /dev node Section 48's examples opened from the outside.

// chardev.c — a device that remembers the last message written to it
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>   // copy_to_user / copy_from_user
#include <linux/device.h>
#include <linux/version.h>   // LINUX_VERSION_CODE / KERNEL_VERSION, for the class_create() shim below

#define DEVICE_NAME "mychardev"
#define BUF_SIZE    128

static char    message[BUF_SIZE];
static size_t  message_len;
static dev_t   dev_num;
static struct cdev my_cdev;
static struct class *my_class;

static int dev_open(struct inode *inode, struct file *file) {
    (void)inode; (void)file;
    return 0;    // nothing to set up for this simple device
}

static ssize_t dev_read(struct file *file, char __user *buf, size_t len, loff_t *offset) {
    (void)file;
    if (*offset >= message_len) return 0;              // EOF
    size_t remaining = message_len - (size_t)*offset;
    size_t to_copy = len < remaining ? len : remaining;

    // copy_to_user validates the user pointer AND does the copy — a raw memcpy would
    // let a malicious/buggy caller pass a kernel address and read arbitrary kernel memory
    if (copy_to_user(buf, message + *offset, to_copy)) return -EFAULT;

    *offset += (loff_t)to_copy;
    return (ssize_t)to_copy;
}

static ssize_t dev_write(struct file *file, const char __user *buf, size_t len, loff_t *offset) {
    (void)file; (void)offset;
    size_t to_copy = len < BUF_SIZE - 1 ? len : BUF_SIZE - 1;   // never overflow `message`

    if (copy_from_user(message, buf, to_copy)) return -EFAULT;
    message[to_copy] = '\0';
    message_len = to_copy;
    return (ssize_t)to_copy;
}

static const struct file_operations fops = {
    .owner = THIS_MODULE,
    .open  = dev_open,
    .read  = dev_read,
    .write = dev_write,
};

static int __init chardev_init(void) {
    if (alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME) < 0) return -1;

    cdev_init(&my_cdev, &fops);
    if (cdev_add(&my_cdev, dev_num, 1) < 0) {
        unregister_chrdev_region(dev_num, 1);
        return -1;
    }

#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 4, 0)
    my_class = class_create(THIS_MODULE, DEVICE_NAME);   // two-arg form: kernels before 6.4
#else
    my_class = class_create(DEVICE_NAME);                 // 6.4+ dropped the owner argument
#endif
    device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME);  // makes /dev/mychardev appear

    printk(KERN_INFO "chardev: /dev/%s ready\n", DEVICE_NAME);
    return 0;
}

static void __exit chardev_exit(void) {
    device_destroy(my_class, dev_num);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
}

module_init(chardev_init);
module_exit(chardev_exit);
MODULE_LICENSE("GPL");
make && sudo insmod chardev.ko
echo "hello kernel" > /dev/mychardev
cat /dev/mychardev          # hello kernel
sudo rmmod chardev

Why copy_to_user/copy_from_user instead of memcpy (Section 10's usual tool): kernel and user space are separate address spaces with separate page tables. A user-space pointer is only meaningful in the context of that process's mappings — memcpying through it directly from kernel code would either fault or, worse, silently read/write whatever the raw address happens to resolve to in kernel space. copy_to_user/copy_from_user validate that the user address is actually mapped and accessible to that process before touching it, returning an error instead of corrupting memory — this single pair of functions is the kernel-space equivalent of Section 29's entire "trust nothing that crosses a boundary" chapter, with the boundary being the user/kernel split instead of the network.


Generated from real-life-c-guide.md · 8 Oct 2026 16:36