Linux Character Driver Write Method – Free Linux Device Drivers Course

PREV_LEC | NEXT_LEC

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

write file operation
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 the ep_chardev driver 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:

ssize_t (*write) (struct file *filp, const char __user *buf,
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

User App: write()

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:

  1. Bounds check the offset. If *f_pos is already past the end of your device’s storage, reject the write with -ENOSPC or -EINVAL depending on your device semantics.
  2. Clamp the count. If the requested count would write past the end of the buffer, shrink it so the write only fills what is left.
  3. Lock the shared resource. Since multiple processes can write concurrently, protect the buffer with a mutex.
  4. Copy from user space. Use copy_from_user(), never a raw pointer dereference, since user pointers cannot be trusted directly.
  5. Advance the position and return. Update *f_pos by 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.

unsigned long not_copied = copy_from_user(dst, src, count);
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

static const struct file_operations ep_fops = {
.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:

$ make
$ sudo insmod ep_chardev.ko
$ sudo mknod /dev/ep_chardev c <major> 0
$ echo “hello kernel” | sudo tee /dev/ep_chardev

Expected output:

hello kernel
$ 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 buf directly instead of using copy_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 like dd.

Best Practices

  • Always validate *f_pos and count before 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 using copy_from_user().
  • Always bounds-check the offset and count before copying.
  • Use a mutex to protect shared driver state from concurrent writers.
  • Update *f_pos and 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.

PREV_LEC | NEXT_LEC

 

Leave a Reply

Your email address will not be published. Required fields are marked *