Project 15: Kernel Module Development

Write a loadable kernel module that registers a character device and logs events.

Quick Reference

Attribute Value
Difficulty Advanced
Time Estimate 12-18 hours
Main Programming Language C
Alternative Programming Languages Rust (kernel module)
Coolness Level Very High
Business Potential Medium (kernel tooling)
Prerequisites device files, kernel build basics
Key Topics module lifecycle, char devices, user-kernel copy

1. Learning Objectives

By completing this project, you will:

  1. Write a loadable kernel module with init/exit hooks.
  2. Register a character device and create a /dev node.
  3. Safely copy data between user and kernel space.
  4. Handle concurrency and module unloading correctly.

2. All Theory Needed (Per-Concept Breakdown)

Kernel Module Lifecycle and Safety

Fundamentals

Kernel modules extend the kernel at runtime. They must define init and exit functions, register any devices, and clean up all resources on unload. Because kernel code runs in privileged mode, bugs can crash the entire system. Safe modules validate user pointers, avoid buffer overflows, and handle concurrency with locks. The kernel module API provides helper functions like copy_from_user to safely access user memory.

Deep Dive into the concept

A kernel module is compiled against the running kernel headers and loaded with insmod. The init function runs in kernel context and should allocate resources, register devices, and log status. The exit function must undo all allocations and unregister devices. The kernel uses reference counts to prevent unloading a module that is still in use. If you open a device file from a module, the module’s reference count increases, preventing removal until the file is closed.

Character device registration involves obtaining a major/minor number, registering a cdev, and implementing file operations. The VFS will call your read/write handlers. You must use copy_to_user and copy_from_user to move data across the kernel-user boundary. Directly dereferencing user pointers is unsafe and can trigger faults.

Concurrency is important because multiple processes can open and write to the device simultaneously. You must protect shared state with mutexes or spinlocks. Avoid sleeping in atomic contexts. For this project, a mutex is sufficient.

Debugging kernel code is harder than user space. Use dmesg to inspect logs and rely on careful error handling. Always implement the failure path: if a step in init fails, you must undo any previous steps to avoid leaks. These patterns are central to safe kernel development.

How this fit on projects

This concept builds on Project 10 (device files) and Project 8 (syscall boundary). It also supports Project 16 (network stack instrumentation).

Definitions & key terms

  • Module init/exit: entry/exit functions for a module.
  • cdev: character device structure.
  • copy_from_user: safe user->kernel copy.
  • Reference count: prevents unloading active modules.

Mental model diagram (ASCII)

insmod -> init -> register device -> /dev node
rmmod -> exit -> unregister device -> cleanup

How it works (step-by-step)

  1. Module init registers char device.
  2. Create /dev node with major/minor.
  3. User opens and reads/writes.
  4. Module handles data and logs.
  5. Module unload cleans up.

Minimal concrete example

static int __init mymod_init(void) { printk("loaded\n"); return 0; }
static void __exit mymod_exit(void) { printk("unloaded\n"); }

Common misconceptions

  • “Kernel crashes are rare”: small bugs can crash instantly.
  • “You can read user pointers directly”: must use copy helpers.

Check-your-understanding questions

  1. Why must a module clean up on failure paths?
  2. What does copy_from_user protect against?
  3. Why can a module not unload while a device is open?

Check-your-understanding answers

  1. To avoid leaked resources and stale devices.
  2. Invalid user pointers and page faults.
  3. Reference counts prevent unloading in-use code.

Real-world applications

  • Device drivers, kernel instrumentation.

Where you’ll apply it

  • This project: Section 3.2, Section 3.7, Section 5.10 Phase 2.
  • Also used in: Project 10, Project 16.

References

  • Linux Kernel Development Ch. 6-7
  • LDD3 (Linux Device Drivers)

Key insights

Kernel modules are powerful but unforgiving; correctness and cleanup are mandatory.

Summary

By writing a module, you learn kernel APIs, safety constraints, and driver structure.

Homework/Exercises to practice the concept

  1. Add ioctl support for a custom command.
  2. Add a ring buffer to store last N writes.
  3. Add a module parameter for buffer size.

Solutions to the homework/exercises

  1. Implement unlocked_ioctl handler.
  2. Use circular buffer with mutex.
  3. Use module_param and validate range.

3. Project Specification

3.1 What You Will Build

A kernel module that registers a character device /dev/mymod, logs read/write operations, and exposes a small buffer to user space.

3.2 Functional Requirements

  1. Module loads/unloads cleanly.
  2. /dev/mymod supports read and write.
  3. Data copied safely to/from user space.
  4. Concurrency is safe under multiple readers.

3.3 Non-Functional Requirements

  • Performance: read/write <1 ms for small buffers.
  • Reliability: no kernel oops under stress.
  • Usability: clear dmesg logs.

3.4 Example Usage / Output

$ sudo insmod mymod.ko
$ echo "ping" > /dev/mymod
$ dmesg | tail -n 1
mymod: ping

3.5 Data Formats / Schemas / Protocols

  • Internal buffer: newline-delimited text.

3.6 Edge Cases

  • Write more than buffer size.
  • Read from empty buffer.
  • Concurrent writes.

3.7 Real World Outcome

3.7.1 How to Run (Copy/Paste)

make
sudo insmod mymod.ko
sudo mknod /dev/mymod c 240 0

3.7.2 Golden Path Demo (Deterministic)

  • Use fixed writes and deterministic buffer contents.

3.7.3 If CLI: exact terminal transcript

$ echo "hello" > /dev/mymod
$ cat /dev/mymod
hello

Failure demo (deterministic):

$ cat /dev/mymod
cat: /dev/mymod: Operation not permitted

Exit codes:

  • 0 success
  • 2 invalid args
  • 3 permission error

4. Solution Architecture

4.1 High-Level Design

Module init -> register device -> file ops -> module exit

4.2 Key Components

| Component | Responsibility | Key Decisions | |———–|—————-|—————| | Init/exit | register/unregister | static major number | | File ops | read/write handlers | simple buffer | | Sync | protect buffer | mutex |

4.3 Data Structures (No Full Code)

struct mydev_state {
    char buf[1024];
    size_t len;
    struct mutex lock;
};

4.4 Algorithm Overview

Key Algorithm: read

  1. lock mutex
  2. copy data to user
  3. update offset
  4. unlock

Complexity Analysis:

  • Time: O(n) bytes
  • Space: O(1)

5. Implementation Guide

5.1 Development Environment Setup

sudo apt-get install linux-headers-$(uname -r) build-essential

5.2 Project Structure

project-root/
|-- mymod.c
|-- Makefile
`-- README.md

5.3 The Core Question You’re Answering

“How do you safely extend a running kernel without crashing the system?”

5.4 Concepts You Must Understand First

  1. Module lifecycle hooks.
  2. copy_to_user/copy_from_user.
  3. Concurrency primitives in kernel.

5.5 Questions to Guide Your Design

  1. What happens if init fails mid-way?
  2. How will you handle buffer overflow?
  3. What permissions should /dev node have?

5.6 Thinking Exercise

List three reasons kernel code is more dangerous than user code.

5.7 The Interview Questions They’ll Ask

  1. Why can a kernel module crash the system?
  2. How does module reference counting work?

5.8 Hints in Layers

Hint 1: Start with a hello module.

Hint 2: Add char device registration.

Hint 3: Add read/write handlers.

5.9 Books That Will Help

| Topic | Book | Chapter | |——-|——|———| | Kernel modules | Linux Kernel Development | 6 | | Devices | LDD3 | 3-4 |

5.10 Implementation Phases

Phase 1: Hello module (2-3 hours)

Goals: init/exit logs.

Phase 2: Device node (4-6 hours)

Goals: register char device.

Phase 3: Read/write (4-6 hours)

Goals: buffer + copy_to_user.

5.11 Key Implementation Decisions

| Decision | Options | Recommendation | Rationale | |———-|———|—————-|———–| | Buffer | fixed vs dynamic | fixed | simpler and safe | | Locking | mutex vs spinlock | mutex | sleeps allowed |


6. Testing Strategy

6.1 Test Categories

| Category | Purpose | Examples | |———-|———|———-| | Unit | buffer ops | size limits | | Integration | load/unload | insmod/rmmod | | Stress | concurrent access | parallel reads |

6.2 Critical Test Cases

  1. Read after write returns expected data.
  2. Long write truncates safely.
  3. Module unload fails if device open.

6.3 Test Data

write="hello"

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

| Pitfall | Symptom | Solution | |——–|———|———-| | Using user pointer directly | kernel oops | use copy_from_user | | Forgetting cleanup | device persists | unregister in exit | | Missing lock | data races | add mutex |

7.2 Debugging Strategies

  • Use dmesg and printk logs.
  • Add WARN_ON for unexpected states.

7.3 Performance Traps

  • Excessive logging in read/write path.

8. Extensions & Challenges

8.1 Beginner Extensions

  • Add ioctl to reset buffer.

8.2 Intermediate Extensions

  • Add polling support for select().

8.3 Advanced Extensions

  • Add async notifications via fasync.

9. Real-World Connections

9.1 Industry Applications

  • Device drivers and kernel instrumentation.
  • Linux kernel drivers/char.

9.3 Interview Relevance

  • Kernel module lifecycle questions.

10. Resources

10.1 Essential Reading

  • Linux Kernel Development Ch. 6-7

10.2 Video Resources

  • Kernel module tutorials

10.3 Tools & Documentation

  • man insmod, man rmmod

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain module lifecycle.
  • I can explain copy_to_user.

11.2 Implementation

  • Module loads/unloads cleanly.

11.3 Growth

  • I can debug kernel code safely.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • Module loads and logs.

Full Completion:

  • /dev node read/write works.

Excellence (Going Above & Beyond):

  • Polling/ioctl support.