Writing char drivers
Contract
| Field | Bound contract |
|---|---|
| Trigger | Writing or reviewing a Linux char driver: struct file_operations, cdev registration, kernel-user copies, ioctl command design, device memory mmap, or blocking reads with poll. |
| Authority | Read-only. Writes nothing. Chat output only. No remote mutation. |
| Side effect | Returns proposed driver code and review findings. No source files are modified. |
| Done | Every requested operation has driver code, and every finding names one failure-table row. |
Inputs
- Target tree and kernel (required): where the driver builds. The examples assume 6.12 or later, which covers this tree's floor (LTS 6.18, mainline 7.2).
- Operations to write (required): registration,
read,write,ioctl,mmap, orpoll. - Device window (conditional): physical base, allowed length, and permissions; needed for
mmap. - Userspace contract (optional): the
ioctlcommand set and struct layout the user side already speaks.
Procedure
Register the char device with a dynamic major number; a fixed major collides with another driver.
#include <linux/fs.h> #include <linux/cdev.h> #include <linux/uaccess.h> static dev_t devno; static struct cdev my_cdev; static struct class *my_class; static const struct file_operations my_fops = { .owner = THIS_MODULE, .open = my_open, .release = my_release, .read = my_read, .write = my_write, .unlocked_ioctl = my_ioctl, /* .llseek stays unset: an unset field means no seek support */ }; static int __init my_init(void) { int ret = alloc_chrdev_region(&devno, 0, 1, "mydev"); if (ret) return ret; cdev_init(&my_cdev, &my_fops); ret = cdev_add(&my_cdev, devno, 1); if (ret) goto err_cdev; /* class_create takes the name as its only argument since 6.4 */ my_class = class_create("mydev"); if (IS_ERR(my_class)) { ret = PTR_ERR(my_class); goto err_class; } device_create(my_class, NULL, devno, NULL, "mydev"); return 0; err_class: cdev_del(&my_cdev); err_cdev: unregister_chrdev_region(devno, 1); return ret; }Since kernel 6.12 removed the
no_llseeksymbol, leave.llseekunset for a non-seekable device; an unset field already means no seek support. Inside a platform driver, use thedevm_*variants and register fromprobe. Done when: the error path releases every resource acquired before the failure, and the caller sees thecdev_adderror code.Copy between kernel and user memory only through
copy_to_userandcopy_from_user. Never dereference a__userpointer. Both return the number of bytes not copied, so nonzero means-EFAULT. They can sleep, so the handler holds no spinlock.static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { char kbuf[128]; size_t len; if (*ppos < 0 || (size_t)*ppos >= sizeof(kbuf)) return 0; len = min(count, sizeof(kbuf) - (size_t)*ppos); /* fill every byte first; an unfilled byte leaks kernel stack */ memset(kbuf, 0, sizeof(kbuf)); memcpy(kbuf, "data", 4); if (copy_to_user(buf, kbuf + *ppos, len)) return -EFAULT; *ppos += len; return len; }Done when: the handler copies only through the copy functions, initializes every byte of a stack buffer before it, and advances
*pposafter a full copy.Define
ioctlcommands with the_IOmacro family; each macro encodes magic, number, direction, and data size.#include <linux/ioctl.h> #define MY_IOC_MAGIC 'k' #define MY_IOC_RESET _IO(MY_IOC_MAGIC, 0) #define MY_IOC_SET _IOW(MY_IOC_MAGIC, 1, int) static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { switch (cmd) { case MY_IOC_RESET: return 0; case MY_IOC_SET: { int val; if (copy_from_user(&val, (void __user *)arg, sizeof(val))) return -EFAULT; return 0; } default: return -ENOTTY; } }Use
_IOWRwith a fixed-size struct when a command both reads and writes. Serve 32-bit callers on a 64-bit kernel throughcompat_ioctlwhen the command carries a pointer. Share one command header between kernel and userspace so magic and numbers cannot drift. Done when: every command macro encodes all four fields and the default case returns-ENOTTY.Map device memory with
mmaponly after validating the request against the device window.static int my_mmap(struct file *filp, struct vm_area_struct *vma) { unsigned long size = vma->vm_end - vma->vm_start; phys_addr_t phys = device_phys_base; /* from the platform data */ if (size > DEVICE_WINDOW_SIZE) return -EINVAL; vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); return remap_pfn_range(vma, vma->vm_start, phys >> PAGE_SHIFT, size, vma->vm_page_prot); }pgprot_noncachedmarks MMIO regions uncached. For a DMA buffer, check the size and caller permissions before the mapping. Done when: a length outside the device window is rejected beforeremap_pfn_rangeruns.Support blocking reads with
.polland a wait queue. Implement.pollwithpoll_waiton the queue, callwake_up_interruptiblewherever new data arrives, and register SIGIO delivery throughfasync_helperfor async readers. Done when: every blocking site has one wake source and the fops carries.poll.Review the finished code against the failure table. Return the driver code and a findings list where each finding names one row. Route deeper work:
platform-device-modelfor the probe context that owns the device,kernel-concurrencyfor locking inside file operations,kernel-debugging-advancedfor tracing the ioctl path. Done when: every finding names one row and every requested operation has reviewed code.
Failure and recovery
| Symptom | Cause | Recovery |
|---|---|---|
-EFAULT on copy |
Bad user pointer or faulting page | Rely on the copy functions' return values; access_ok is an optional early filter. |
-ENOTTY in userspace |
Magic or number mismatch | Build both sides from one shared command header. |
| Major number collision | Fixed major already taken | Register with alloc_chrdev_region. |
SIGSEGV in an mmap region |
Cached mapping over device memory | Set pgprot_noncached before remap_pfn_range. |
scheduling while atomic |
Sleeping call under a spinlock in an op | Release the lock before any call that can sleep. |
| Late init failure | class_create or device_create failed |
Unwind in reverse: cdev_del, device_destroy, class_destroy, unregister_chrdev_region. |
no_llseek undeclared |
Kernel 6.12 removed the symbol | Delete the .llseek assignment; unset means no seeking. |
class_create argument error |
Kernel 6.4 dropped the module argument | Pass the class name as the single argument. |
Output
Driver code for each requested operation: registration, user copies, ioctl, mmap, or poll; the findings list from the final review, each finding tied to one failure-table row.