In this final lecture we connect the semester's Rust foundation to real systems development. We examine the bug classes that make kernel work dangerous, what Rust prevents and what it does not, how small kernel-space contributions work in practice, and the safe experimentation workflow that makes participation possible.
Lecture Date
๐ May 4, 2026
Standard
I/O and Networking
Topics Covered
Memory SafetyRust for LinuxKernel ModulesSafe ContributionSystems Engineering Workflow
The previous lecture traced two end-to-end actions inside a living Linux system:
Walkthrough 1 (device) โ A dd read from /dev/urandom revealed the full U-bend: the syscall boundary, VFS dispatch, driver code, and data return โ all visible through strace
Walkthrough 2 (network) โ A TCP client/server exchange through nc showed the socket lifecycle, port binding, the accept() discovery pattern, and the transport layer โ all observable through strace and ss
Linux's windows โ /proc, /sys, and /dev exposed kernel state as structured, readable surfaces
Kernel modules โ Linux extends its own functionality at runtime through loadable modules that run with full kernel privilege
Observability โ Tools like ps, strace, dmesg, ip, ss, lsmod, and modinfo made the kernel's behavior visible from user space
Linux is no longer a black box. You can trace an action from a user-space process through the kernel and back, name the abstractions involved, and observe them with standard tools. The system is legible.
That legibility raises the question this lecture addresses. If Linux is built from all of these cooperating abstractions โ drivers, protocol stacks, schedulers, memory managers, filesystems โ and if all of that code runs in the privileged, unforgiving environment we called "code is lava" in LN19, then how do real engineers safely contribute to this system? And what role does the language we have been learning all semester play in making that contribution safer?
Today's Agenda
Why Kernel and Driver Code Is Dangerous โ Privilege, asynchrony, and system-wide consequences
The Bug Classes That Matter โ and Rust's Response โ Memory-safety failures, how Rust eliminates them, and the honest boundaries of those guarantees
Why Rust Fits Linux โ Connecting the semester's Rust lens to a real operating-system context
Small Contributions, Not Heroics โ Modules, pseudo-devices, and realistic contribution scope
Safe Experimentation Workflow โ Building rustecho โ A live demo: a Rust character device and a side-by-side comparison with the C version
Looking Beyond the Course โ What you should now believe about entering systems work
Why Kernel and Driver Code Is Dangerous
Every program you have written in this course ran in user space. The operating system protected you from your own mistakes. A null pointer dereference killed your process, not the machine. An infinite loop consumed your time slice, not every process's. A bad memory access triggered a segfault, and the kernel cleaned up after you. User space is a sandbox with a safety net.
Kernel code has no safety net.
When code runs in kernel space โ Ring 0, full privilege, direct access to hardware and every byte of physical memory โ the consequences of bugs change qualitatively. A null pointer dereference does not kill a process; it panics the entire kernel. A buffer overrun does not corrupt one program's heap; it can overwrite kernel data structures that manage every process on the machine. A race condition does not produce a wrong answer in one thread; it can corrupt the scheduler's view of what is runnable, the filesystem's view of what is on disk, or the network stack's view of what connections exist.
This is the "code is lava" principle from LN19 made concrete. When we studied the U-bend, we noted that driver code runs in a context with different rules โ no sleeping in interrupt context, reentrancy requirements, shared state with hardware. Those rules are not arbitrary restrictions. They exist because the kernel has no external authority to catch it when it fails. The kernel is the authority.
The Asymmetry of Consequence
The core insight is an asymmetry. In user space, the OS limits the blast radius of your mistakes. In kernel space, there is nothing above you to limit anything. The same bug that would be a minor annoyance in a user-space program becomes a system-wide catastrophe in a driver or kernel module.
Environment
Who catches your mistake?
Blast radius
User space
The kernel (signals, segfaults, OOM killer)
One process
Kernel space
Nothing โ the hardware faults or the kernel panics
The entire machine
The Asymmetry of Consequence
Closer to hardware means greater privilege and larger blast radius. Hover a zone.
This asymmetry is why the language you use in kernel space matters far more than the language you use in a web application. A memory-safety bug in a REST API handler might leak some data or crash a server process that gets restarted automatically. The same category of bug in a kernel driver can take down every process on the machine, corrupt the filesystem, or open a privilege-escalation vulnerability that compromises the entire system.
๐ก Connection to LN19: This is the same argument we made when discussing why driver code is dangerous. The difference now is that we have seen real kernel code paths in LN23 โ real strace output, real device nodes, real socket lifecycles. The danger is not hypothetical. It applies to every module loaded into the kernel, including the ones we listed with lsmod.
The Bug Classes That Matter โ and Rust's Response
Not all bugs are equally dangerous in systems software. Some categories of failure have caused disproportionate damage over decades of operating-system and driver development. Understanding which bug classes matter most โ and which of them a memory-safe language like Rust can actually eliminate โ is the bridge between the danger we just established and the Linux community's eventual decision to allow a second language into their codebase.
Use-After-Free
A pointer continues to be used after the memory it points to has been deallocated. The memory may have been reallocated for a completely different purpose. Reading from it produces garbage. Writing to it silently corrupts whatever now lives there.
In kernel space, this is catastrophic. The freed memory might now hold another process's page table, a socket buffer, or a scheduler queue entry. Corrupting it can compromise any subsystem.
structdevice_data *dev = kmalloc(sizeof(*dev), GFP_KERNEL);
/* ... use dev ... */
kfree(dev);
/* Later, in a different code path: */
dev->status = READY; // use-after-free: dev's memory may now hold anything
Double Free
Memory is freed twice. The allocator's internal bookkeeping is corrupted, which can cause future allocations to return overlapping regions. Two unrelated subsystems now believe they own the same memory.
kfree(buffer);
/* ... other code ... */
kfree(buffer); // double free: allocator metadata is now corrupt
Buffer Overrun
A write exceeds the bounds of an allocated buffer. In user space, this might trigger a segfault if you are lucky. In kernel space, the adjacent memory likely belongs to another kernel data structure, and the overwrite proceeds silently.
Buffer overruns are the single most exploited vulnerability class in the history of computer security. They are the foundation of stack smashing, return-oriented programming, and countless privilege-escalation attacks.
char buf[64];
memcpy(buf, user_data, user_len); // if user_len > 64, adjacent kernel memory is overwritten
Data Races
Two execution contexts โ say, a process-context code path and an interrupt handler โ access the same memory concurrently, and at least one of them writes. Without proper synchronization, the result is undefined. The data structure may end up in a state that neither code path intended, and the corruption may not manifest until much later.
In the kernel, data races are particularly insidious because the concurrent contexts are often invisible. A driver's read() handler runs in process context. An interrupt arrives and runs the driver's interrupt handler. Both touch the same device state. If the locking is wrong โ or absent โ the device state becomes inconsistent.
/* Process context: */
shared->count++;
/* Interrupt handler, running concurrently on the same CPU or another: */
shared->count++;
// Without a lock or atomic operation, the final value of count is unpredictable
Invalid Pointer Dereference
A pointer that was never initialized, or that has been set to an invalid address, is dereferenced. In user space, the MMU catches this and delivers a segfault. In kernel space, the kernel itself is the one that would need to catch it โ and if the address happens to map to valid kernel memory, the dereference succeeds silently, reading or writing the wrong data.
The Common Thread
Every one of these bug classes involves memory: accessing it after it is gone, freeing it incorrectly, writing past its boundaries, sharing it without coordination, or following a pointer to the wrong place. They are not logic bugs (where the algorithm is wrong) or design bugs (where the architecture is flawed). They are mechanical failures in how the program interacts with memory.
This distinction matters because it defines what a programming language can realistically prevent.
Memory Safety: The Common Thread
Every critical bug class converges on unsupervised memory access. Hover to see the damage.
These Are Not Hypothetical
These bug classes are not textbook curiosities. They are the dominant source of real-world systems vulnerabilities, and they are being found and fixed right now in active projects.
The uutils/coreutils project โ a complete rewrite of GNU coreutils in Rust โ has been systematically discovering and eliminating these bugs as it reimplements tools like dd, cp, sort, and ls. The GNU C originals of these tools have been maintained for decades by expert programmers, and they still contain buffer-handling bugs, partial-write errors, and edge cases that the Rust type system catches at compile time. As of 2026, Ubuntu has adopted the Rust coreutils in place of the GNU versions, starting with Ubuntu 25.10.
The same story plays out in the kernel. The Linux kernel's own security team, along with independent analyses from Microsoft and Google, consistently reports that 60โ70% of critical vulnerabilities in systems software are memory-safety failures โ the exact bug classes cataloged above. These are not rare edge cases in obscure code paths. They are the routine, predictable consequence of writing millions of lines of memory-unsafe code.
๐ Key Point: The Linux kernel has been written in C for over 30 years. C trusts the programmer to manage memory correctly. When that trust is misplaced โ and in millions of lines of code maintained by thousands of contributors, it inevitably is โ the result is one of the bug classes above.
How Rust Responds โ and Where Its Boundaries Are
The Rust features you have been using all semester โ ownership, borrowing, lifetimes, and the type system โ were designed to eliminate the exact bug classes catalogued above at compile time, before the code ever runs. But Rust's safety guarantees have clear boundaries. It is important to understand both halves honestly rather than treating Rust as a panacea.
Concern
Rust's Answer
Mechanism
Use-after-free
Prevented at compile time
Ownership + lifetimes โ references cannot outlive the data they point to
Double free
Prevented at compile time
Single ownership; exactly one Drop per value
Buffer overrun
Prevented in safe code
Bounds-checked slice access; iterators replace manual index arithmetic
Data races
Prevented at compile time
Exclusive &mut; Send/Sync traits gate cross-thread sharing; Mutex/RwLock/atomics required for shared mutable state
Invalid dereference
Prevented at compile time
References are non-null by construction; Option<T> makes absence explicit
Logic bugs
Not prevented
The compiler cannot know your intent
Deadlocks
Not prevented
Lock ordering is engineering discipline
Resource leaks
Partially prevented
Drop covers memory; non-memory resources still need explicit handling
unsafe misuse
Not prevented
The programmer's promise replaces the compiler's proof
Bad architecture
Not prevented
Language cannot substitute for design judgment
This is not magic. The compiler is performing a static analysis of your program's memory-access patterns and rejecting programs it cannot prove are memory-safe. The cost is conservatism โ some programs that would be correct are also rejected. The benefit is that every program that passes the compiler's checks is free of the memory-safety bug classes above, outside of unsafe blocks.
๐ก Connection to LN2โLN4: This is why ownership, borrowing, and lifetimes felt so strict when you first learned them. They are not stylistic preferences. They are the mechanism that eliminates the bug classes that have historically made kernel and driver development so hazardous.
What "Not Prevented" Actually Means
The bottom half of the table deserves elaboration. These are the failure modes that Rust does not eliminate, and confusing them with the memory-safety guarantees would be a mistake.
Logic bugs. Rust will not stop you from implementing the wrong algorithm, using the wrong formula, or making an incorrect architectural decision. If your driver reads from the wrong register offset, the compiler cannot catch that โ the code is memory-safe, just semantically wrong.
Deadlocks. Rust prevents data races (concurrent unsynchronized access) but does not prevent deadlocks (two threads each waiting for a lock the other holds). Locking discipline is still an engineering responsibility.
Resource leaks. While Rust's Drop trait ensures memory is freed, other resources โ file descriptors, hardware locks, network connections โ can still leak if the cleanup logic is incorrect.
unsafe misuse. Rust allows unsafe blocks where the programmer takes responsibility for invariants the compiler cannot check. In kernel development, unsafe is unavoidable โ interacting with hardware registers, calling C functions through FFI, and manipulating raw pointers all require it. An unsafe block is a promise by the programmer that the code inside upholds the safety invariants Rust expects. If that promise is broken, all the same bug classes return.
Bad system design. No language prevents you from building a poorly designed system. Rust gives you stronger mechanical guarantees about memory, but architecture, modularity, error handling strategy, and operational discipline remain human responsibilities.
๐ค Key Framing: Rust does not make systems programming easy. It makes one specific, historically devastating class of failure significantly harder to introduce. That is a meaningful change, but it is not a complete solution. Safe systems work still requires engineering discipline, careful design, and deep understanding of the system you are modifying.
Why Rust Fits Linux
This course has used Rust as its systems programming language from the first lecture. That choice was not arbitrary. Rust is the first language other than C to be accepted into the Linux kernel's source tree โ a codebase that has been exclusively C (with some assembly) for over three decades.
Understanding why the Linux community made this decision connects everything in the course.
The Problem Rust Addresses in Linux
Linux is maintained by thousands of contributors, many of whom write driver and subsystem code. As we saw in LN23, drivers are loaded as kernel modules โ they run with full kernel privilege, share the kernel's address space, and interact with hardware and concurrent execution contexts. Every driver is a potential source of the bug classes we just studied.
The Linux kernel's own security team has reported that a majority of kernel vulnerabilities originate in drivers. This is not because driver authors are careless. It is because C provides no compiler-enforced protection against the memory-safety failures that are most dangerous in privileged code. Every driver author must manually uphold the invariants that Rust's compiler enforces automatically.
What Rust in Linux Looks Like
Rust does not replace C in the Linux kernel. The kernel's core โ the scheduler, the memory manager, the VFS layer โ remains in C and is unlikely to be rewritten. What Rust provides is a safer option for new code, particularly drivers and modules, where memory-safety failures are most common and most dangerous.
The Rust-for-Linux project provides kernel-safe abstractions โ Rust wrappers around kernel APIs that allow module authors to interact with kernel services (allocating memory, registering devices, managing locks) through safe Rust interfaces. The unsafe code that bridges Rust and the kernel's C internals is concentrated in these wrapper layers, reviewed carefully, and shared across all Rust modules. Individual module authors write mostly safe Rust.
This architecture mirrors a pattern you should recognize. The kernel's C API is the hardware-like layer. The Rust safe wrappers are the driver-like abstraction. Individual modules are the user-facing code. It is the U-bend again โ but applied to the language boundary rather than the hardware boundary.
๐ก Connection to LN19: The U-bend taught us that crossing a privilege boundary requires careful mediation โ the syscall interface, the driver dispatch, the interrupt handler protocol. The Rust/C boundary in the kernel is another instance of the same principle: unsafe wrappers mediate the crossing, concentrating danger in reviewed, shared code so that the wider module ecosystem can operate safely.
Recent Kernel Contributions in Rust
This is not a future plan. Rust code is being merged into the Linux kernel right now, and the contributions illustrate the patterns we have been discussing.
The register! macro (merged early 2026) provides a safe, ergonomic API for hardware register access in Rust drivers. Instead of raw pointer arithmetic to read device registers โ exactly the kind of code where buffer overruns and invalid dereferences hide โ the macro generates type-checked accessor methods. The Nova GPU driver and the Rust PCI sample driver already use it.
A C linked-list interop module (2026) provides safe Rust iteration over the kernel's internal doubly-linked lists โ data structures that are shared across subsystems and historically prone to use-after-free bugs when traversed concurrently. The Rust wrapper ensures that iteration follows the kernel's locking protocol without requiring every user to get it right manually.
The Rust scull character device sample (merged into kernel samples in 2024) reimplements the classic teaching device from the Linux Device Drivers book in safe Rust. It demonstrates the full module lifecycle โ init, cleanup, read, write, device registration โ using the Rust-for-Linux abstractions. We will look at code very similar to this when we build our own module later in this lecture.
These contributions share a common shape: they are small, well-scoped, and focused on wrapping dangerous C patterns in safe Rust interfaces. None of them rewrote a major subsystem. All of them made the kernel meaningfully safer.
Rust Is Rewriting the Toolbox, Not Just the Kernel
The kernel is the highest-stakes target for Rust, but it is not the only one. A broader movement is rewriting the classic Unix command-line tools โ the same tools you used in LN23 โ in Rust. These projects demonstrate that the language's safety and performance benefits extend from kernel space all the way up to everyday user-space utilities.
The uutils coreutils project is particularly significant. Ubuntu โ one of the most widely used Linux distributions โ replaced its GNU coreutils with the Rust versions starting in Ubuntu 25.10, with the 26.04 LTS release continuing the transition. This means that ls, cp, dd, sort, and dozens of other tools that millions of users run every day are now Rust binaries. The rewrite found and eliminated real bugs in the process โ buffer-handling errors, partial-write failures, and edge cases that had survived decades of maintenance in C.
๐ค The Bigger Picture: Rust is not confined to one layer of the system. It is being used to write kernel modules at Ring 0, device drivers that bridge hardware and software, system utilities that run as user-space processes, and high-performance CLI tools. The same ownership and borrowing model you learned in this course applies at every level. That is what makes it a true systems language rather than a niche tool for one domain.
Why This Matters for You
You have spent the semester learning ownership, borrowing, and lifetimes. You have used Mutex and mpsc channels. You have thought about when data moves versus when it is borrowed. All of that was preparation for exactly this kind of work.
A Rust developer approaching Linux module development is not starting from zero. The mental model you built in this course โ that memory has an owner, that shared access requires explicit coordination, that the compiler is a partner in enforcing invariants โ is the same mental model that makes writing a safe kernel module possible.
And if kernel work is not where your interests lead, the same foundation applies to the user-space Rust ecosystem: contributing to ripgrep, uutils, or any of the tools in the table above uses the same language, the same mental model, and the same open-source contribution workflow. The skills transfer.
Small Contributions, Not Heroics
There is a common misconception that contributing to an operating system requires understanding the entire system. It does not. Real systems are built from many small, well-scoped contributions, not from individual heroics. The Linux kernel has over 30 million lines of code and thousands of contributors. No single person understands all of it. The architecture is designed so that they do not need to.
Modules as Contribution Boundaries
The kernel module system we explored in LN23 is the mechanism that makes bounded contribution possible. A module is a self-contained unit of kernel functionality with a defined interface: an initialization function, a cleanup function, and a set of operations it registers with the kernel. The module author needs to understand the kernel's API for the subsystem they are extending, but not the internal implementation of every other subsystem.
This is the same principle of abstraction that runs through the entire course. Just as a process does not need to understand the scheduler's implementation to call fork(), a module author does not need to understand the memory manager's internals to call kmalloc(). The API boundary is the scope boundary.
What a Realistic Contribution Looks Like
The kinds of contributions that are educational and achievable โ the kinds that match the scope of what you have learned โ tend to be small:
A character device module that exposes a new pseudo-device in /dev, responding to read() and write() with custom behavior
A procfs or sysfs entry that exposes some computed kernel-side information to user space
A protocol or format handler that processes data according to a specific structure
A sensor or hardware interface module that bridges a specific device to the kernel's driver framework
None of these require understanding the entire kernel. Each one requires understanding the relevant subsystem API, the module lifecycle, and the safety constraints of kernel-space execution. That is a bounded, learnable scope.
Contribution Scope Ladder
Meaningful work starts at the bottom. Hover a rung to learn more.
A Real Contribution: 27 Lines That Fixed dd
To make this concrete, consider a real pull request merged into the uutils/coreutils project in January 2026. The contributor โ who noted "this is almost my first snippet in Rust" โ fixed a bug in the Rust implementation of dd.
The problem: dd was using line-buffered stdout, which meant that partial writes could silently lose data when piping output to another process. The fix was 27 lines of additions and 3 deletions across 2 files, switching from line-buffered to block-buffered stdout.
This contribution is worth studying because of what it is:
Small โ a 30-line diff across two files
Meaningful โ it fixed a real data-loss bug in a tool used by millions of people
Grounded in I/O concepts โ the bug was about buffering behavior, exactly the kind of systems concern this course teaches
Written by a beginner โ the author was new to Rust
Reviewed and merged โ it went through code review and was accepted by the project maintainers
That is what a realistic contribution looks like. Not a new filesystem. Not a rewrite of the scheduler. A small, well-understood fix to a real problem, made possible by understanding how I/O buffering works at the systems level.
What a Kernel Contribution Looks Like
On the kernel side, the Rust scull character device sample (merged into the kernel's samples/rust/ directory) demonstrates the same principle at a higher privilege level. It implements a simple character device โ one that stores bytes written to it and returns them when read โ using the Rust-for-Linux abstractions. The module registers a device, wires up read and write handlers through the kernel's file_operations equivalent, and manages its internal buffer with safe Rust ownership.
The full module is small enough to read end to end in a few minutes. It exercises real kernel APIs โ device registration, memory allocation, file operations โ and runs with full kernel privilege. But it is scoped tightly enough that a single developer can understand every line. Later in this lecture, we will build something very similar.
Scoping as an Engineering Skill
Choosing the right scope for a contribution is itself an engineering skill. A well-scoped contribution:
Is small enough to understand end to end โ you can trace every code path the module takes
Is large enough to be meaningful โ it exercises real kernel APIs and real privilege constraints
Has observable behavior โ you can verify it works through dmesg, /proc, /dev, or other kernel surfaces
Is safely reversible โ you can unload the module and return the system to its previous state
This is not a compromise. It is how professional systems engineering works. Large systems are maintained through many small, comprehensible changes, each of which is reviewed, tested, and observable.
๐ Key Point: The goal is not to write a filesystem or a network stack. The goal is to write something small that runs in kernel space, exercises real kernel APIs, and demonstrates that you understand the privilege, safety, and lifecycle constraints that kernel code demands. That is a genuine systems contribution, not a toy exercise. The dd fix and the scull sample both prove the point โ meaningful work happens at small scale.
Safe Experimentation Workflow โ Building rustecho
Writing kernel code is dangerous. We have established that. But dangerous does not mean inaccessible. It means that the workflow surrounding the code matters as much as the code itself. Rather than describe this workflow abstractly, we will walk through it live by building a small kernel module called rustecho โ a character device that echoes back whatever you write to it.
The Environment: Disposable VMs
In LN23, we set up a Multipass VM as an inspection environment. That same VM serves a second critical purpose: it is a disposable sandbox for kernel experimentation. If your module panics the kernel, you lose a VM โ not your development machine, not your data, not your other running processes. You restart the VM and try again.
This is not a workaround. It is how kernel developers actually work. The Linux kernel development community routinely uses VMs and emulators (QEMU, virtme, syzkaller's VM harness) precisely because kernel bugs are machine-level failures. A good workflow makes those failures cheap and recoverable rather than catastrophic.
What We Are Building
rustecho is a character device module. When loaded, it creates a device node at /dev/rustecho. You can write data to it, and when you read from it, you get back whatever was last written. It is conceptually an echo โ not the shell command, but the idea of a kernel-level buffer that stores and returns data through the standard read()/write() interface.
This exercises every layer we have studied:
Module lifecycle โ init registers the device, cleanup removes it (LN23)
File operations โ the module provides read, write, open, and release handlers that the kernel dispatches through VFS (LN18, LN19)
Kernel memory โ the module allocates and manages a buffer in kernel space
Privilege boundary โ user-space programs interact through /dev/rustecho via syscalls; the module code runs in kernel space (LN19)
Observability โ we verify every step through dmesg, ls /dev/, strace, and lsmod (LN23)
The Development Loop
Safe kernel development follows a tight, observable loop:
We will follow this loop end to end for rustecho. The full source โ including a tiny C "skeleton" module you can build first to validate that your toolchain and insmod/rmmod/dmesg workflow are wired up correctly โ is in the Addendum at the bottom of this page. The lecture walks through the concepts and the key code; the Addendum provides everything needed to reproduce the demo.
Edit
Step 1 of 8
Code compiles locally
1 / 8
Building rustecho in Rust
This is where the course's Rust investment pays off. We are going to register a character device so that a node appears in /dev, and we are going to do it in Rust.
Before we write the Rust version, it helps to see what the kernel expects at the C level, since C is the kernel's native language and the interface Rust must ultimately connect to. In C, a character device wires up its handlers through a file_operations struct โ the same dispatch table from LN19:
This is the kernel's C dispatch table. Each field is a function pointer. When a user-space process calls read() on your device, the kernel looks up .read in this struct and calls whatever function is there. The C version requires the developer to manually manage memory (kmalloc/kfree), manually lock shared state (mutex_lock/mutex_unlock), and manually copy data across the user/kernel boundary (copy_to_user/copy_from_user). Every one of those manual steps is an opportunity for the bug classes we cataloged earlier.
The Rust version replaces that manual ceremony with the language features you have been learning all semester. Here is the core of the Rust rustecho module:
Read through this carefully โ every major design choice maps onto something you learned earlier in the course:
module! macro + impl kernel::InPlaceModule replaces C's module_init/module_exit. The init function returns an impl PinInit<Self, Error> โ a pin-initializer that the kernel runtime drives in place. Initialization failures propagate through the same Result/? pattern you have used all semester. There is no cleanup function to write โ when the module is unloaded, the kernel runs Drop for every field of RustEcho (in this case, the MiscDeviceRegistration), which deregisters the device automatically.
global_lock! { static BUFFER: Mutex<KVec<u8>> = KVec::new(); } declares a kernel-wide shared buffer protected by a mutex. The macro emits both the static and a marker type so the compiler can verify that every access goes through .lock(). You cannot read or write BUFFER without holding the guard, and the guard auto-releases when it leaves scope. Multiple processes calling read()/write() on /dev/rustecho all serialize through this single lock โ exactly the behavior we want for an echo buffer that "remembers" the last write across opens.
#[vtable] impl MiscDevice for RustEchoDevice is the modern Rust-for-Linux equivalent of C's struct file_operations. Each method is a syscall handler. open runs when a process opens the device. write_iter runs on write(). read_iter runs on read(). The _iter suffix reflects that the kernel passes the user-space buffer as an iovec (a vector of memory ranges) โ a more efficient interface than the raw (buf, len) pair used in older C drivers.
IovIterSource / IovIterDest + copy_from_iter_vec / simple_read_from_buffer replace C's raw copy_from_user/copy_to_user. These safe wrappers handle the user/kernel memory boundary without exposing raw pointers. Bounds, partial reads, and the user-space/kernel-space split are all enforced by the type system.
Ownership handles cleanup. The MiscDeviceRegistration is a field of RustEcho. When the module struct is dropped, the registration's own Drop runs and tears down the device node. The BUFFER static is freed when the module's memory is reclaimed. There is no kfree call to forget. There is exactly one owner and exactly one Drop per resource. Use-after-free and double-free are structurally impossible.
๐ Note: Building Rust kernel modules requires a kernel configured with CONFIG_RUST=y and the appropriate Rust toolchain. This is more setup than C modules require โ the Addendum at the bottom of this page walks through the full toolchain provisioning. That extra setup is the current cost of being early to Rust in the kernel. The safety guarantees you get in return are the reason the Linux community accepted that cost.
๐ Aside โ this very lecture has already been wrong about the API once. Earlier drafts of this page used kernel::miscdev::Registration and file::Operations, which is what most pre-2024 Rust-for-Linux tutorials still document. Those names come from the out-of-tree fork that was used to prototype Rust kernel support before merging โ and the merged upstream API is kernel::miscdevice::MiscDeviceRegistration plus the MiscDevice trait, with read_iter/write_iter instead of read/write. I discovered the mismatch the first time I tried to demo rustecho in class and the module refused to load. The fix was a 30-line code-shape change; the lesson is bigger. When in doubt, do not trust a blog post โ open samples/rust/ in the kernel tree on your target version. That directory is the source of truth for whatever Rust-for-Linux dialect your kernel actually ships, and it is what every example in the rest of this lecture and the homework was rebuilt against. This is what "early to a language in the kernel" actually feels like in practice: the safety story is real, and the API surface is still moving fast enough that documentation goes stale before the ink dries. Both things are true at once.
After building and loading the Rust module, the workflow is identical to what we would do with a C module:
sudo insmod rustecho.ko && sudo dmesg | tail -5
Load the Rust module and check for initialization messages. You should see rustecho: module loaded. dmesg needs sudo because modern Ubuntu sets kernel.dmesg_restrict=1, gating the kernel ring buffer behind root.
ls -la /dev/rustecho
Verify the device node exists. It should show as a character device with mode crw------- root:root โ the default for MiscDevice-registered nodes.
echo"hello from user space" | sudotee /dev/rustecho > /dev/null
Write a string to the device. The sudo tee idiom is required because /dev/rustecho is 0600 root:root, and a plain sudo echo "..." > /dev/rustecho would still fail โ the shell evaluates >beforesudo runs, so the redirect targets the device as your unprivileged user. This write() syscall crosses into kernel space, dispatches through VFS to the Rust write handler, and stores the data in the Mutex-protected buffer.
sudocat /dev/rustecho
Read from the device. The Rust read handler locks the buffer, copies the stored data back to user space, and releases the lock when the guard goes out of scope. You should see hello from user space.
Trace the syscalls to see the U-bend in action. The strace output is identical whether the module is written in C or Rust โ the kernel dispatches through the same VFS path. The difference is entirely in what happens on the kernel side of that boundary.
sudo rmmod rustecho && sudo dmesg | tail -5
Unload and verify cleanup. Drop runs, the buffer is freed, the device node disappears.
๐ก Connection to LN19 and LN23: You are now on the inside of the U-bend. In LN23, you watched strace reveal the syscall boundary from the process's perspective. Now you have written the code that runs on the kernel side of that boundary โ in the same language you have been using all semester. The same openat(), read(), write(), and close() syscalls are dispatching to your Rust handlers.
C vs. Rust: What the Rust Version Structurally Prevents
Now that we have seen both the C interface and the Rust implementation, we can make the comparison concrete. This is not a matter of style or preference. The two versions have structurally different failure modes.
Concern
C Version
Rust Version
Buffer lifetime
Developer must call kfree in the cleanup function. Forgetting it leaks memory. Calling it twice is a double free.
Buffer is owned by the module struct. Drop frees it exactly once. The compiler enforces single ownership.
Lock discipline
Developer must call mutex_lock before access and mutex_unlock after. Forgetting to unlock deadlocks. Forgetting to lock creates a data race.
global_lock! { static BUFFER: Mutex<...> } wraps the data. You cannot access it without .lock(). The guard auto-releases on scope exit. The compiler prevents unguarded access.
User/kernel copy
copy_to_user and copy_from_user take raw pointers and lengths. A wrong length is a buffer overrun.
IovIterSource/IovIterDest are safe abstractions. Bounds are checked at the API level; you never see a raw pointer or length.
Error propagation
Functions return negative integers for errors. The caller must check every return value manually. Missing a check silently continues with corrupt state.
Functions return Result<T>. The ? operator propagates errors automatically. The compiler warns about unused Result values.
Null pointers
Pointers can be NULL. Dereferencing a null pointer in kernel space can panic the kernel.
References are non-null by construction. Option<T> makes the absence of a value explicit and checked.
Look at the rustecho_exit function in C (the full version is in the Addendum):
Four cleanup calls, in a specific order that reverses the initialization order. If the developer forgets one, resources leak. If they get the order wrong, the kernel may access freed memory. If a future contributor adds a new resource to init but forgets to add the corresponding cleanup to exit, the module silently leaks every time it is unloaded.
In the Rust version, there is no exit function to write. When the module is unloaded, Drop runs automatically on every field of RustEcho. The MiscDeviceRegistration field has its own Drop implementation that calls the C misc_deregister for us โ we do not have to remember to do it, and we cannot do it in the wrong order, because there is no order to choose. The developer cannot forget a cleanup step because there are no cleanup steps to write โ ownership handles it.
This is the payoff of the entire semester's work with Rust. The strictness of ownership, borrowing, and lifetimes โ the rules that felt constraining in LN2 through LN4 โ is precisely what makes kernel code safer. The compiler does not trust you with memory, and in kernel space, that distrust is exactly what you want.
๐ค Key Framing: The C version is not wrong. It works. The Linux kernel has run on C modules like it for decades. But every line of that cleanup function is a line where a human can make a mistake that crashes the entire machine. The Rust version does not eliminate all possible bugs โ logic errors, deadlocks, and unsafe misuse remain โ but it eliminates the mechanical memory-safety failures that account for the majority of kernel vulnerabilities. That is not a theoretical improvement. It is an engineering decision backed by decades of vulnerability data.
Recovery: When Things Go Wrong
Kernel bugs manifest in a few characteristic ways:
Symptom
What Likely Happened
Recovery
Module loads but produces no dmesg output
Init function failed silently or never ran
Check return codes, add logging, rebuild
Module loads but device does not appear in /dev
Device registration failed
Check dmesg for error messages from the registration call
Reading/writing the device causes an error
The file_operations handler has a bug
Check dmesg, add more logging to the handler
The kernel panics (VM becomes unresponsive)
A null dereference, invalid memory access, or deadlock in module code
Restart the VM, examine the panic trace in dmesg (if it was logged before the halt), fix the bug
Module refuses to unload
Something still holds a reference (an open file descriptor, a dependent module)
Close all handles, check lsmod for "Used by" count
The VM is your safety net. A kernel panic in a VM is a 30-second restart. The same panic on bare metal could mean a corrupted filesystem and lost work.
๐ Key Point: Safe systems experimentation is not about never making mistakes. It is about making mistakes cheaply, observing their effects clearly, and recovering quickly. The combination of Rust's compile-time guarantees, a disposable VM environment, and systematic logging through dmesg creates a workflow where kernel development is demanding but not reckless.
Looking Beyond the Course
This course began with a process โ the operating system's fundamental unit of isolation, protection, and resource management. It moved through scheduling, synchronization, memory, deadlocks, and file systems. It crossed into I/O, drivers, and the privileged boundary between user space and kernel space. It traced communication from local IPC through the full network stack. It entered Linux and watched the abstractions become real. And now, in this final lecture, it arrived at the question of participation: how you, equipped with these abstractions and a memory-safe systems language, can approach real systems work responsibly.
The honest answer is that this course does not make you a kernel developer. It makes you a person who can understand what kernel developers do, why it is hard, why the tools and languages matter, and where the dangers live. That understanding is the foundation everything else builds on.
What You Should Now Believe
After this course, these claims should feel earned rather than aspirational:
Operating systems are engineered, not magical. Every behavior you see โ process isolation, device communication, network transport, file access โ is the result of specific design decisions that you can now name and critique.
Abstractions are real. The concepts from this course are not simplified teaching models. They are the actual vocabulary of real systems โ the same syscalls, the same driver dispatch, the same socket lifecycle, the same module system.
Rust is a meaningful tool for systems work. Not because it solves every problem, but because it eliminates the specific class of failure that has historically made systems work so hazardous. Your semester of ownership, borrowing, and lifetimes was not busywork. It was training for exactly this domain.
Safe contribution is about scope and discipline, not genius. You do not need to understand the entire kernel to write a module that runs inside it. You need to understand the relevant API, the privilege constraints, the observation tools, and the recovery workflow. That is a learnable, bounded skill set.
You can enter this space. Not tomorrow, and not alone. But with the foundation this course has built โ the abstractions, the language, the understanding of why things are hard โ you have a credible path into systems work. The kernel is just code. Complicated, privileged, consequential code โ but code that follows the same principles you have studied all semester.
๐ค Final Thought: The best systems engineers are not the ones who write the most code. They are the ones who understand the system well enough to write the least code that solves the right problem safely. That understanding โ of abstraction, of privilege, of consequence, of discipline โ is what this course has been about.
Summary
Kernel and driver code is qualitatively more dangerous than user-space code because there is no external authority to limit the blast radius of failures
The bug classes that dominate systems software vulnerabilities โ use-after-free, double free, buffer overrun, data races, invalid dereference โ are all memory-safety failures, and they are being found and fixed right now in projects like the Rust coreutils rewrite
Rust eliminates or sharply reduces these bug classes at compile time through ownership, borrowing, lifetimes, and the type system
Rust does not prevent logic bugs, deadlocks, unsafe misuse, or bad architecture โ it is a tool, not a guarantee
Rust was accepted into the Linux kernel as a second language because it addresses the exact failure class that has historically made driver and module development so risky โ recent contributions include the register! macro, C linked-list interop, and the Rust scull sample
Rust is rewriting the Unix toolbox: ripgrep, fd, bat, eza, and the full GNU coreutils (adopted by Ubuntu) demonstrate that the same language works from kernel modules to everyday CLI tools
The Rust/C boundary in the kernel uses unsafe wrappers to mediate the crossing โ the same U-bend principle applied to the language boundary
Realistic kernel contributions are small, well-scoped modules โ the dd stdout fix (27 lines, by a beginner) and the kernel scull sample show what this looks like in practice
We built rustecho โ a character device module โ in Rust, then compared it side-by-side with the equivalent C implementation to show what the Rust version structurally prevents โ all following a disciplined workflow inside a disposable VM (with an optional C skeleton in the addendum for validating the toolchain first)
The course's abstractions โ processes, syscalls, drivers, IPC, networking, transport, modules โ are the actual vocabulary of real systems
You now have the foundation to understand, observe, and begin contributing to systems at the kernel level
๐ Lecture Notes
Key Definitions:
Term
Definition
Kernel Panic
An unrecoverable error in kernel space that halts the entire system
Use-After-Free
Accessing memory through a pointer after that memory has been deallocated
Double Free
Deallocating the same memory region twice, corrupting the allocator's state
Buffer Overrun
Writing past the boundaries of an allocated buffer into adjacent memory
Data Race
Concurrent unsynchronized access to shared memory where at least one access is a write
unsafe Rust
A Rust block where the compiler's memory-safety checks are partially relaxed and the programmer assumes responsibility
FFI
Foreign Function Interface โ the mechanism for calling functions across language boundaries (e.g., Rust calling C kernel APIs)
Memory Safety
The property that a program cannot access memory it does not own, has not initialized, or has already freed
Rust Safety Boundary:
Category
Compiler-enforced?
Mechanism
Use-after-free
Yes
Ownership + lifetimes
Double free
Yes
Single ownership + Drop
Buffer overrun
Mostly (runtime bounds checks)
Slice/array access, iterators
Data races
Yes
Send/Sync + exclusive &mut
Invalid dereference
Yes
No null references, Option<T>
Logic bugs
No
โ
Deadlocks
No
โ
unsafe misuse
No
Programmer's responsibility
rustecho Demo Pieces:
Piece
Language
Where It Lives
What It Does
Key Insight
Skeleton
C
Addendum (optional pre-flight)
Loads and prints to dmesg
Validates the build/load/unload workflow before adding functionality
Character Device
Rust
Lecture body (primary demo)
Creates /dev/rustecho via MiscDevice; stores and echoes data through a global_lock!-protected buffer
Ownership handles cleanup; the global mutex enforces locking; Result handles errors
Comparison
C vs Rust
Lecture body
Side-by-side analysis of the same module
Identifies which bug classes become structurally impossible in the Rust version
Safe Experimentation Workflow:
Step
Tool
What to Check
Build
make
Compilation errors and warnings
Load
insmod / modprobe
dmesg for init messages
Observe
dmesg, lsmod, ls /dev/
Module presence, device registration
Interact
Application-specific
Expected behavior, strace if needed
Unload
rmmod
dmesg for cleanup messages, lsmod for removal confirmation
Recover
VM restart
Kernel panic trace in dmesg (if captured)
Rust Tool Ecosystem:
Classic Tool
Rust Alternative
Status
GNU coreutils
uutils/coreutils
Adopted by Ubuntu 25.10+
grep
ripgrep (rg)
Widely adopted
find
fd
Widely adopted
cat
bat
Widely adopted
ls
eza
Widely adopted
util-linux
uutils/util-linux
Active development
Course Abstraction โ Systems Reality:
Course Concept
Where It Lives in Practice
Ownership and borrowing (LN2โ4)
Compile-time memory safety for kernel modules
Concurrency and synchronization (LN6, LN12)
Lock discipline in kernel-space shared state
Processes and scheduling (LN9, LN10)
The units the kernel manages and multiplexes
Syscall boundary (LN19)
The privilege crossing every user program makes
Drivers and U-bend (LN19)
The code path kernel modules implement
IPC (LN20)
Local communication through kernel-mediated channels
Networking and transport (LN21, LN22)
The protocol stack modules can extend
Kernel modules (LN23)
The contribution mechanism for kernel functionality
Observability tools (LN23)
The verification layer for safe experimentation
๐ Additional Resources
Recommended Reading
OSTEP โ Security โ Chapters on protection, isolation, and the consequences of privileged-code failure
This section contains everything you need to reproduce the rustecho demo on your own machine. Both the C and Rust versions are provided โ the C version serves as a reference for the kernel's native interface, and the Rust version is what the lecture demo uses.
VM Setup
If you already have the ln23 VM running, you can reuse it. Otherwise, create a new one:
build-essential provides gcc and make. linux-headers-$(uname -r) installs the header files for the currently running kernel โ these are needed to compile modules that link against this specific kernel version. kmod provides insmod, rmmod, lsmod, and modprobe.
Rust Kernel Toolchain Setup
Building Rust kernel modules requires additional toolchain configuration beyond what the C modules need. The kernel must be configured with CONFIG_RUST=y (Ubuntu 26.04 LTS already does this), and you need the rustc version that matches the kernel's expectations. For the Ubuntu 26.04 kernel 7.0.0-x-generic, that is rustc 1.93.
rustc-1.93 is the kernel-blessed Rust compiler for this Ubuntu series. rust-1.93-src provides the Rust standard library source, which the kernel build system needs to compile core and alloc for the kernel's no_std environment. bindgen is what generates Rust bindings for the kernel's C headers. update-alternatives makes the versioned binary the default rustc so the Makefile picks it up automatically.
๐ Note: This is more setup than C modules require. That is the current cost of being early to Rust in the kernel โ the toolchain is newer and less integrated into distribution packages. The safety guarantees you get in return are the reason the Linux community accepted that cost. As Rust support matures in the kernel, distribution kernels will increasingly ship the right rustc and bindings as part of linux-headers automatically.
Create the project directory:
mkdir -p ~/rustecho && cd ~/rustecho
About the Makefile
What is a Makefile? A Makefile is a build automation file used by the make tool. It defines rules that specify how to compile source files into executables or, in this case, kernel modules. The Linux kernel relies heavily on Makefiles โ the kernel source tree contains thousands of them, organized hierarchically. When you build a kernel module, your Makefile delegates to the kernel's own build system by pointing make -C at the kernel's build directory (/lib/modules/.../build). The obj-m variable tells the kernel build system which source files to compile as loadable modules. This pattern โ obj-m += yourmodule.o with make -C $(KDIR) M=$(PWD) modules โ is the standard way to build out-of-tree kernel modules, whether in C or Rust.
You should see rustecho: module loaded and rustecho: module unloaded in dmesg, and the module should appear and disappear from lsmod.
Stage 2: Rust Character Device (Primary)
This is the version demonstrated in the lecture. It implements the full character device in Rust using the kernel's Rust abstractions.
rustecho.rs
cat > ~/rustecho/rustecho.rs << 'SRC'
// SPDX-License-Identifier: GPL-2.0
//! rustecho: a Rust character device that echoes data back via /dev/rustecho.
//!
//! Writes replace the shared kernel buffer; reads stream it back to user space.
//! All opens share the same buffer through a global mutex.
use kernel::{
fs::{File, Kiocb},
iov::{IovIterDest, IovIterSource},
miscdevice::{MiscDevice, MiscDeviceOptions, MiscDeviceRegistration},
prelude::*,
};
module! {
type: RustEcho,
name: "rustecho",
description: "rustecho โ a character device that echoes data back",
license: "GPL",
}
kernel::sync::global_lock! {
// SAFETY: Initialized in module initializer before first use.
unsafe(uninit) static BUFFER: Mutex<KVec<u8>> = KVec::new();
}
#[pin_data]
struct RustEcho {
#[pin]
_miscdev: MiscDeviceRegistration<RustEchoDevice>,
}
impl kernel::InPlaceModule for RustEcho {
fn init(_module: &'static ThisModule) -> impl PinInit<Self, Error> {
pr_info!("module loaded\n");
// SAFETY: Called exactly once during module init.
unsafe { BUFFER.init() };
let opts = MiscDeviceOptions { name: c"rustecho" };
try_pin_init!(Self {
_miscdev <- MiscDeviceRegistration::register(opts),
})
}
}
struct RustEchoDevice;
#[vtable]
impl MiscDevice for RustEchoDevice {
type Ptr = Pin<KBox<Self>>;
fn open(_file: &File, _misc: &MiscDeviceRegistration<Self>) -> Result<Pin<KBox<Self>>> {
Ok(KBox::new(RustEchoDevice, GFP_KERNEL).map(KBox::into_pin)?)
}
fn write_iter(mut kiocb: Kiocb<'_, Self::Ptr>, iov: &mut IovIterSource<'_>) -> Result<usize> {
let mut buf = BUFFER.lock();
buf.clear();
let n = iov.copy_from_iter_vec(&mut buf, GFP_KERNEL)?;
*kiocb.ki_pos_mut() = 0;
pr_info!("wrote {n} bytes\n");
Ok(n)
}
fn read_iter(mut kiocb: Kiocb<'_, Self::Ptr>, iov: &mut IovIterDest<'_>) -> Result<usize> {
let buf = BUFFER.lock();
let n = iov.simple_read_from_buffer(kiocb.ki_pos_mut(), &buf)?;
pr_info!("read {n} bytes\n");
Ok(n)
}
}
SRC
Build and Test Stage 2 (Rust)
๐ Important: Stage 1 left a rustecho.c in the directory. KBuild's obj-m += rustecho.o rule resolves to that .c file when both a .c and a .rs of the same basename are present, which would silently rebuild the C skeleton instead of the Rust module. Remove rustecho.c first so the build picks up rustecho.rs. Look for RUSTC [M] rustecho.o in the build output to confirm โ if you see CC [M] rustecho.o, you are still building the C version.
rm -f rustecho.c
make clean && make
sudo insmod rustecho.ko
sudo dmesg | tail -5
ls -la /dev/rustecho
echo"hello from user space" | sudotee /dev/rustecho > /dev/null
sudocat /dev/rustecho
sudo strace -e openat,read,write,close cat /dev/rustecho
sudo dmesg | tail -10
sudo rmmod rustecho
sudo dmesg | tail -3
ls /dev/rustecho 2>&1 || echo"/dev/rustecho removed โ clean."
You should see:
RUSTC [M] rustecho.o in the build output (proof you compiled the Rust source)
rustecho: module loaded in dmesg
The device node appear in /dev after loading
"hello from user space" echoed back when reading
strace showing the syscall dispatch to your module's Rust handlers
Clean removal with Drop running automatically
๐ Why sudo everywhere. The MiscDevice node defaults to mode 0600 root:root, so an unprivileged shell redirect (echo "..." > /dev/rustecho) cannot open it. And modern Ubuntu sets kernel.dmesg_restrict=1, so non-root users cannot read the kernel ring buffer. Every line above that touches /dev/rustecho or dmesg therefore needs sudo. The sudo tee ... > /dev/null idiom is what lets a redirect target a root-owned file โ > is interpreted by your shell, beforesudo runs, so sudo echo ... > /dev/rustecho would still fail.
C Reference: Character Device with Read/Write
This is the equivalent C version for reference and comparison. It implements the same functionality as the Rust version but requires manual memory management, manual locking, and manual cleanup.
The C version's device_create() also produces a 0600 root:root node, so the same sudo tee / sudo cat rules apply. Removing rustecho.c afterwards keeps the directory in the Rust-only state the rest of the addendum assumes.
The behavior from user space is identical. The differences are entirely in the kernel-side safety properties.
Testing Script
A convenience script to test the Rust module through the full workflow:
cat > ~/rustecho/test_rustecho.sh << 'SCRIPT'#!/bin/bash# Best-effort cleanup so a partial run never leaves the module loaded.cleanup() {
if lsmod | grep -q '^rustecho '; thensudo rmmod rustecho 2>/dev/null || truefi
}
trap cleanup EXIT
set -e
echo"=== Building Rust module ==="# Stage 1 leaves rustecho.c in place; remove it so KBuild picks up rustecho.rs.rm -f rustecho.c
make clean && make
# Sanity-check that the build was actually Rust, not the leftover C skeleton.if [ ! -f rustecho.rs ]; thenecho"ERROR: rustecho.rs is missing. Re-create it from the addendum." >&2
exit 1
fiecho""echo"=== Loading module ==="sudo insmod rustecho.ko
sudo dmesg | tail -3
echo""echo"=== Checking device node ==="ls -la /dev/rustecho
echo""echo"=== Writing to device ==="echo"hello from user space" | sudotee /dev/rustecho > /dev/null
echo""echo"=== Reading from device ==="sudocat /dev/rustecho
echo""echo"=== Tracing syscalls ==="sudo strace -e openat,read,write,close cat /dev/rustecho 2>&1 | head -15
echo""echo"=== Kernel log ==="sudo dmesg | tail -10
echo""echo"=== Unloading module ==="sudo rmmod rustecho
sudo dmesg | tail -3
echo""echo"=== Verifying cleanup ==="ls /dev/rustecho 2>&1 || echo"/dev/rustecho removed โ clean."
lsmod | grep rustecho || echo"Module not in lsmod โ clean."echo""echo"=== Done ==="
SCRIPT
chmod +x ~/rustecho/test_rustecho.sh
The script needs sudo for dmesg (because of kernel.dmesg_restrict=1 on modern Ubuntu) and for every operation on /dev/rustecho (because the device node defaults to 0600 root:root). The trap cleanup EXIT guarantees that an interrupted run never leaves the module loaded โ without it, set -e would abort before the rmmod line and the next run would fail with "module already loaded."
Verifying the Project
cd ~/rustecho && make clean && tree ~/rustecho/
make clean removes the .ko, .o, .mod*, and dot-.cmd build artifacts so tree shows only the source files you wrote.
The base rustecho module is fully functional but intentionally minimal. The following extensions build naturally on the foundation and are suitable as self-directed projects. Each one exercises additional course concepts and would make a strong addition to a systems programming portfolio.
1. Capacity Management โ Add a configurable maximum buffer size. Reject writes that exceed it with an appropriate error code (-ENOSPC). This exercises error handling at the kernel API boundary. (Connects to: LN18 device contracts, LN19 privilege-boundary error propagation)
2. Read Statistics via /proc โ Create a /proc/rustecho entry that reports the total number of reads, writes, and bytes transferred. This exercises the procfs interface from LN23 and gives you a second kernel surface for the same module. (Connects to: LN23 /proc as a kernel window)
3. Multiple Instances โ Support multiple minor numbers, each with an independent buffer. Users can echo "a" > /dev/rustecho0 and echo "b" > /dev/rustecho1 with separate storage. This exercises device registration and the major/minor number system from LN18. (Connects to: LN18 major/minor dispatch, LN19 driver multiplexing)
4. Persistent Buffer โ Save the buffer contents to a file on module unload and restore them on load. This exercises the boundary between volatile kernel state and persistent storage. (Connects to: filesystem concepts, LN19 module lifecycle)
5. Concurrent Access Control โ Add a reader/writer lock (rwlock) instead of a simple mutex, allowing multiple simultaneous readers but exclusive writers. Log contention events to dmesg. This exercises the concurrency and synchronization material from LN6 and LN12 in a real kernel context. (Connects to: LN6 concurrency, LN12 safety vs. liveness)
6. Access Permissions โ Implement permission checking in the open() handler. Only allow writes from root. Log denied access attempts. This exercises the privilege and security material from the course. (Connects to: LN19 privilege boundary, kernel security model)
Each extension is independent โ you can pick any combination. The module remains loadable and testable through the same insmod/rmmod/dmesg workflow regardless of which features you add.
Cleanup
cd ~/rustecho && make clean
cd ~ && rm -rf ~/rustecho