Linux Character Driver Write Method
Free Linux Kernel Development Course — Chapter 4.7
The linux character driver write method is how your driver receives data from a user-space application and moves it into kernel memory or hardware. If you are following this free linux device drivers course, you already have a working ep_chardev driver with open() and release() in place. In this lecture we add the missing piece: the write() file operation, implemented the modern kernel 6.x way, with a working, testable example.
Keywords covered in this lecture
copy_from_user
character device driver
free linux kernel development course
kernel 6.x
What You Will Learn
- The exact prototype of the
write()file operation and what each argument means - Why the write position (
*pos) must always be validated before touching memory - How
copy_from_user()safely moves data from user space into kernel space - How to add a working
write()method to theep_chardevdriver from the previous lecture - How to build, load, and test the driver, and what output to expect
Prerequisites
- Lecture 4.5 and 4.6 of this free linux device drivers course (major/minor numbers,
file_operations,open()/release()) - A Linux VM or machine running kernel 6.x with headers installed (
linux-headers-$(uname -r)) - Basic familiarity with pointers and structs in C
The write() Method Prototype
Every character driver that accepts data from user space defines a write() callback inside its file_operations structure. Its signature looks like this:
size_t count, loff_t *f_pos);
Here buf is the user-space buffer holding the data the application wants to write, count is how many bytes it wants to send, and f_pos is a pointer to the current file position. The return value is the number of bytes actually written, or a negative error code such as -EFAULT or -EINVAL on failure.
How Data Flows From App to Kernel
write() Call Path
→
VFS Layer
→
Driver ep_write()
→
copy_from_user()
→
Kernel Buffer
When an application calls the standard C library write(), the kernel’s Virtual File System (VFS) resolves the file descriptor back to your driver’s registered file_operations and calls your write() callback with the user’s buffer, byte count, and current offset.
Steps Inside a write() Implementation
A well-behaved write() method follows a consistent sequence of checks before touching any memory:
- Bounds check the offset. If
*f_posis already past the end of your device’s storage, reject the write with-ENOSPCor-EINVALdepending on your device semantics. - Clamp the count. If the requested
countwould write past the end of the buffer, shrink it so the write only fills what is left. - Lock the shared resource. Since multiple processes can write concurrently, protect the buffer with a mutex.
- Copy from user space. Use
copy_from_user(), never a raw pointer dereference, since user pointers cannot be trusted directly. - Advance the position and return. Update
*f_posby the number of bytes written and return that count.
Why copy_from_user() Instead of memcpy()
User-space pointers can point to unmapped, swapped-out, or invalid memory, and a malicious process could pass a kernel-space address to trick a naive driver. copy_from_user() performs the necessary access checks and page faults safely, returning the number of bytes that could not be copied. A return value of zero means full success.
if (not_copied)
return -EFAULT;
Full Example: Adding write() to ep_chardev
This continues the original ep_chardev driver from the previous lecture. We add a fixed-size in-memory buffer and a mutex-protected ep_write() function.
#define EP_BUF_SIZE 256
struct ep_chardev {
struct cdev cdev;
struct mutex lock;
char buffer[EP_BUF_SIZE];
size_t data_len;
};
static ssize_t ep_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos)
{
struct ep_chardev *dev = filp->private_data;
ssize_t ret;
if (*f_pos >= EP_BUF_SIZE)
return -ENOSPC;
if (*f_pos + count > EP_BUF_SIZE)
count = EP_BUF_SIZE – *f_pos;
if (mutex_lock_interruptible(&dev->lock))
return -ERESTARTSYS;
if (copy_from_user(dev->buffer + *f_pos, buf, count)) {
ret = -EFAULT;
goto unlock;
}
*f_pos += count;
if (*f_pos > dev->data_len)
dev->data_len = *f_pos;
ret = count;
unlock:
mutex_unlock(&dev->lock);
return ret;
}
Notice that mutex_lock_interruptible() is used instead of a plain mutex_lock(). This lets a process waiting on the lock be interrupted by a signal (like Ctrl+C) instead of hanging forever, which is the recommended pattern for user-facing driver code on kernel 6.x.
Registering the write Callback
.owner = THIS_MODULE,
.open = ep_open,
.release = ep_release,
.write = ep_write,
};
Building and Testing the Driver
Build the module with a standard out-of-tree Makefile and load it:
$ sudo insmod ep_chardev.ko
$ sudo mknod /dev/ep_chardev c <major> 0
$ echo “hello kernel” | sudo tee /dev/ep_chardev
Expected output:
$ dmesg | tail -n 3
[ 102.334521] ep_chardev: opened by pid 4821
[ 102.334610] ep_chardev: wrote 13 bytes at offset 0
If you see -EFAULT style errors instead, double-check that you are using copy_from_user() and not directly dereferencing buf.
write() Return Values
| Return Value | Meaning |
|---|---|
| Positive number | Bytes successfully written (can be less than requested) |
| 0 | Nothing was written; usually only valid if count was 0 |
| -EFAULT | Failed to copy data from the user buffer |
| -ENOSPC | No space left at the current offset |
| -ERESTARTSYS | Interrupted while waiting for the lock; VFS will retry the call |
Common Mistakes
- Dereferencing
bufdirectly instead of usingcopy_from_user()— this crashes or corrupts memory. - Forgetting to clamp
count, causing a buffer overflow inside the kernel. - Using a plain
mutex_lock()in code reachable from user-triggered syscalls without considering signal interruption. - Not updating
*f_pos, which breaks sequential writes and tools likedd.
Best Practices
- Always validate
*f_posandcountbefore any copy. - Protect shared buffers with a mutex, and prefer the interruptible variant for code called from user context.
- Return partial writes rather than failing outright when only some of the request fits.
- Keep the critical section (the locked region) as short as possible.
Performance and Security Considerations
Holding a mutex across a large copy_from_user() call blocks every other writer for the duration of the copy. For high-throughput devices, consider copying into a temporary kernel buffer first and only locking while updating shared state. On the security side, never trust count from user space without clamping it, and never skip copy_from_user() even for “trusted” callers, since any process with the right permissions can open your device node.
Summary
- The
write()file operation moves data from a user-space buffer into the kernel usingcopy_from_user(). - Always bounds-check the offset and count before copying.
- Use a mutex to protect shared driver state from concurrent writers.
- Update
*f_posand return the actual number of bytes written.
FAQ
Why can’t I just use memcpy() in the write() method?
Because buf is a user-space pointer. memcpy() does not validate that the memory is mapped and accessible, while copy_from_user() performs the required checks and handles page faults safely.
What happens if copy_from_user() partially fails?
It returns the number of bytes that could not be copied. Most drivers treat any non-zero return as a full failure and respond with -EFAULT.
Do I always need a mutex in the write method?
Yes, if the buffer or state you are writing to can be accessed by more than one process or CPU at the same time, which is true for almost every real device.
Can write() be called with count equal to zero?
Yes, some applications do this intentionally. Returning 0 in that case is correct behavior.
Why use mutex_lock_interruptible() instead of mutex_lock()?
It allows a blocked process to be woken by a signal instead of waiting indefinitely, which is safer for code reachable from user-space system calls.
Conclusion
You have now added a safe, modern write() method to the ep_chardev driver, completing one half of the read/write pair every character driver needs. In the next lecture of this free linux kernel development course, we implement the matching read() method so user-space applications can retrieve the data you just learned to store.
Continue the Free Linux Kernel Development Course
More free embedded linux course and free embedded systems course lectures are on the way.
