What are Debugfs Cleanup Mistakes That Crash the Linux Kernel-Linux Device Driver Training in Hyderabad

Debugfs Cleanup Mistakes That Crash the Linux Kernel
A free Linux kernel programming course lesson on writing safe, production-ready debugfs interfaces (updated for kernel 6.x)
🧩 Skill Level: Intermediate
⏱️ Read Time: 12 min
🐧 Kernel: 6.x

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.

📘 What You Will Learn
Why debugfs cleanup is not automatic The dangling-dentry crash pattern Correct debugfs_remove_recursive() usage Reading a kernel Oops report Production-safe debugfs design
✅ Prerequisites

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.

Debugfs Directory Tree (Healthy State)
/sys/kernel/debug/
└── mydriver/
├── status
└── log_level

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.

Correct Flow vs. Broken Flow
✔ Correct Path
rmmod called → module_exit runs → debugfs_remove_recursive() → dentries freed → /sys/kernel/debug/mydriver/ gone → safe
✘ Broken Path
rmmod called → module_exit runs → cleanup skipped → driver memory freed → dentry still live → user reads file → 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 RIP and 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

Q1. Does rmmod automatically remove my debugfs files?

No. The kernel does not track debugfs entries per module automatically; your module’s exit function must remove them explicitly.

Q2. What’s the difference between debugfs_remove() and debugfs_remove_recursive()?

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.

Q3. Is it safe to call debugfs_remove_recursive() on a NULL or error pointer?

Yes, modern debugfs removal functions are written to tolerate NULL/error dentries gracefully, which simplifies error-path cleanup.

Q4. Why did my system not panic when the Oops happened?

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.

Q5. Can I use debugfs for data that user-space applications rely on?

No — debugfs has no stable ABI, can be absent entirely on hardened kernels, and is intended purely for debugging, not for production data paths.

Q6. What tool helps decode an Oops’s call trace into readable source lines?

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.

Continue the Free Linux Kernel Programming Course

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

Leave a Reply

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