If you are following along with our free Linux kernel programming course, you already know that debugfs is one of the easiest ways to expose driver internals to user space. But “easy” does not mean “safe.” A single missing cleanup call inside your module’s exit path can turn a harmless debugfs file into a kernel crash waiting to happen.
In this lesson of our free Linux device drivers course, we deliberately construct a broken debugfs cleanup path, watch the kernel crash, and then rebuild the same driver the right way — using APIs and conventions that hold up on a modern 6.x kernel.
Before starting this lesson, you should be comfortable with basic loadable kernel module structure (module_init / module_exit), have a 6.x Linux kernel build environment ready, and have gone through the earlier lecture in this free embedded systems course on creating your first debugfs entry.
Why Debugfs Does Not Clean Up After You
A common misunderstanding among newcomers to kernel driver development is assuming that debugfs entries disappear automatically once a module is removed. They do not. Debugfs is a plain, in-memory virtual filesystem — every directory and file your driver creates stays registered in the kernel’s dentry cache until you explicitly remove it. If your module’s cleanup function forgets this step, or skips it under a certain code path, the debugfs files keep pointing to memory that has already been freed.
The result is what kernel developers call a dangling reference: a live filesystem entry pointing at freed memory. Nothing bad happens immediately — the crash only occurs the moment someone tries to read or write that file after the module is gone.
Reproducing the Crash: A Conditional Cleanup Bug
To really understand why cleanup discipline matters, it helps to build the bug on purpose. Imagine a driver that exposes a debug directory and, for whatever reason — a leftover debug flag, an early-return in an error path, a refactor that missed one branch — sometimes skips the removal call. On a 6.x kernel, the pattern looks like this:
#include <linux/module.h>
#include <linux/debugfs.h>
#include <linux/init.h>
static struct dentry *ep_dbg_root;
static int skip_cleanup; /* module parameter, defaults to 0 */
module_param(skip_cleanup, int, 0644);
static int __init ep_dbg_init(void)
{
ep_dbg_root = debugfs_create_dir("ep_dbg_demo", NULL);
debugfs_create_u32("counter", 0644, ep_dbg_root, NULL);
pr_info("ep_dbg_demo: loaded\n");
return 0;
}
static void __exit ep_dbg_exit(void)
{
if (!skip_cleanup)
debugfs_remove_recursive(ep_dbg_root);
pr_info("ep_dbg_demo: unloaded\n");
}
module_init(ep_dbg_init);
module_exit(ep_dbg_exit);
MODULE_LICENSE("GPL");
Load the module normally with skip_cleanup=1, unload it, then try to read the leftover file. Because the underlying driver memory is already gone but the dentry is still registered, the VFS layer walks into freed memory the instant a read is attempted — producing a classic NULL/invalid pointer dereference Oops.
Reading the Kernel Oops Without Fear
New driver authors often panic (pun intended) the first time dmesg shows a wall of hexadecimal after an Oops. In reality, three fields are usually enough to get you 80% of the way to the bug:
| Field | What It Tells You |
|---|---|
RIP |
Exact function and byte offset where execution died |
Call Trace |
The chain of function calls leading to the crash, read bottom-up |
Tainted |
Whether out-of-tree or non-GPL modules are loaded, affecting how much support you’ll get from upstream |
On kernels 6.x, you’ll also want CONFIG_KALLSYMS enabled so that addresses in the call trace resolve to readable symbol names instead of raw hex, and scripts/decode_stacktrace.sh (bundled in the kernel source tree) to turn a raw Oops log into fully annotated source lines.
Building It the Right Way
The fix is almost always structural rather than clever: make debugfs teardown unconditional, and make it the very first thing your exit function does — before you free any of the memory that the debugfs callbacks might touch.
static void __exit ep_dbg_exit(void)
{
debugfs_remove_recursive(ep_dbg_root); /* always, no branches */
ep_dbg_root = NULL;
pr_info("ep_dbg_demo: unloaded cleanly\n");
}
A second, equally important habit: store the top-level dentry returned by debugfs_create_dir() and remove that one directory recursively, rather than tracking every individual file dentry. This is exactly why debugfs_remove_recursive() exists — one call tears down the whole subtree safely, in the correct order.
Common Mistakes and Best Practices
| Mistake | Best Practice Instead |
|---|---|
| Skipping cleanup in an error/debug branch | Call removal unconditionally in every exit path |
| Freeing driver context before removing debugfs | Remove debugfs entries first, free memory second |
| Tracking dozens of individual dentries | Track only the root dentry, remove recursively |
| Ignoring return value checks on creation | Check for IS_ERR() if error reporting is enabled in your build |
Performance and Security Considerations
Debugfs is explicitly documented as unstable API with no ABI guarantees, and on hardened production kernels it may not even be mounted. Never route functionality that user-facing software depends on through debugfs — treat it strictly as a developer/debug channel. From a security standpoint, remember that anything you expose there is typically readable by root only by default, but a misconfigured mount can widen that; never expose raw kernel pointers or sensitive buffer contents through a debugfs file.
Key Takeaways
- Debugfs entries are not garbage-collected — you own their entire lifecycle.
- Always call
debugfs_remove_recursive()unconditionally in your exit path. - Remove debugfs entries before freeing the memory those entries reference.
- Use the Oops’s
RIPand call trace to locate the failing function quickly. - Debugfs has no ABI stability guarantees — never expose production-critical data through it.
Conclusion
A crash caused by broken debugfs cleanup is one of the most common “gotchas” new kernel driver developers run into, and it is also one of the easiest to prevent once you understand the underlying dentry lifecycle. By making removal unconditional and ordering your exit path correctly, you eliminate an entire category of use-after-free bugs in your driver. Keep following this free Linux kernel programming course as we continue building safer, production-grade driver interfaces.
FAQ
No. The kernel does not track debugfs entries per module automatically; your module’s exit function must remove them explicitly.
debugfs_remove() removes a single entry, while debugfs_remove_recursive() walks and removes an entire directory subtree, which is what you want when you created a directory with multiple files inside it.
Yes, modern debugfs removal functions are written to tolerate NULL/error dentries gracefully, which simplifies error-path cleanup.
By default, most distributions set /proc/sys/kernel/panic_on_oops to 0 on development systems, so the offending process is killed but the system keeps running. Production systems typically set this to 1.
No — debugfs has no stable ABI, can be absent entirely on hardened kernels, and is intended purely for debugging, not for production data paths.
The kernel source tree ships scripts/decode_stacktrace.sh, which combined with a build that has debug symbols, converts raw addresses into file and line numbers.
More lessons on kernel module development, device drivers, and embedded Linux are on the way at EmbeddedPathashala.
Browse Course Index Subscribe for Updates
2 Comments