How Does set_memory_ro() Work in Linux? – Free Linux Device Drivers Training Online


← Previous Lecture
|
Next Lecture →

Linux Kernel Memory Protection: set_memory_ro() & vmalloc() Explained
A free Linux kernel programming lecture, updated for modern kernels
Level: Intermediate
Reading time: 14 min
Course: Free Linux Kernel Programming

Linux kernel memory protection is the set of mechanisms the kernel uses to control which regions of its own address space can be read, written, or executed at any given moment. Getting this right matters for driver writers, security researchers, and anyone building kernel modules, because a single stray write into the wrong region can crash a running system in production. In this lecture from our free Linux kernel programming course, we look at how Linux kernel memory protection actually works today, why the old __vmalloc() approach you may have read about in older books no longer applies, and how to correctly mark kernel memory read-only using the modern set_memory_ro() family of APIs.

What You Will Learn
kmalloc vs vmalloc internals
Why __vmalloc() protection flags were removed
set_memory_ro() / set_memory_rw()
Guard pages and hardening
Debugging permission-fault oopses
Best practices & pitfalls

Prerequisites

Before this lecture, you should be comfortable with:

Writing and loading a basic kernel module
C pointers and memory addresses
Reading dmesg output
A recent kernel source tree (6.x recommended)

Why Linux Kernel Memory Protection Matters

Every allocation your module makes lives inside the same address space as the rest of the running kernel. There is no hardware wall between your driver’s buffer and, say, a page table or a function pointer table sitting nearby in memory. Linux kernel memory protection exists precisely to add that wall back in software: by marking selected pages read-only, non-executable, or otherwise restricted, the kernel can catch accidental corruption immediately, as a fault, instead of letting it silently overwrite something critical and surface as an unrelated crash minutes later. This is also the same category of protection that underpins kernel hardening features you may have heard of, such as marking module text sections non-writable after load.

kmalloc() vs vmalloc(): A Quick Refresher

Before touching kernel memory protection directly, it helps to be clear on the two allocators you will most often be protecting. They behave very differently underneath.

Aspect kmalloc() vmalloc()
Physical layout Physically contiguous Only virtually contiguous
Typical use Small buffers, DMA-friendly paths Large buffers where physical contiguity is not required
Allocation cost Fast, direct from slab Slower; builds page tables on demand
Interrupt context Safe with GFP_ATOMIC Not safe to call
Custom page protection today Via set_memory_*() after allocation Via set_memory_*() after allocation
Inline Diagram — Where Protection Fits In
Allocate
kmalloc / vmalloc
→
Initialize data
while still writable
→
Lock it down
set_memory_ro()
→
Use safely
reads only
→
Unlock
set_memory_rw()
→
Free
kfree / vfree

The Old Way: Why __vmalloc()’s Protection Argument Is Gone

Older Linux kernel programming material shows __vmalloc() taking a third argument — a page protection bitmask — so you could request read-only pages directly at allocation time. If you try that same call on a current kernel, it will not even compile. Somewhere around the 5.8–5.9 development cycle, that protection parameter was dropped from the public API. The kernel maintainers found that essentially every in-tree caller passed the default protection anyway, so the extra flexibility was removed to simplify the allocator internals. On a modern kernel, __vmalloc() and vmalloc() always hand you standard read/write kernel memory — nothing more.

This is a good example of why Linux kernel memory protection techniques need to be re-learned periodically: the kernel’s internal APIs evolve release after release, and code copied from an older textbook can silently stop working, or worse, compile against a similar-looking but different function.

H3: What Replaced It

The supported way to change memory protection today is to allocate normally, then adjust the protection bits of already-mapped pages using a small, architecture-independent API family declared in <linux/set_memory.h>:

set_memory_ro(addr, numpages)
set_memory_rw(addr, numpages)
set_memory_x(addr, numpages)
set_memory_nx(addr, numpages)

Each call takes a page-aligned virtual address and a page count, not a byte count, which trips up a lot of newcomers — more on that in the troubleshooting section below.

Hands-On: Applying Linux Kernel Memory Protection in a Module

Let’s build a small, original example that allocates a buffer, fills it with data, and then hardens it to read-only using the modern API. This is written fresh for this lecture and targets current kernel headers.

Step 1 — Allocate and Prepare the Buffer

#include <linux/module.h>
#include <linux/vmalloc.h>
#include <linux/set_memory.h>

#define EP_PAGES  4   /* number of pages we intend to protect */

static void *ep_buf;

static int __init ep_memprotect_init(void)
{
    ep_buf = vmalloc(EP_PAGES * PAGE_SIZE);
    if (!ep_buf)
        return -ENOMEM;

    /* Fill the buffer with data while it is still writable */
    memset(ep_buf, 0xA5, EP_PAGES * PAGE_SIZE);

    pr_info("ep_memprotect: buffer ready at %px\n", ep_buf);
    return 0;
}

Step 2 — Lock the Buffer Read-Only

    /* vmalloc() returns a page-aligned address, so we can protect it directly */
    if (set_memory_ro((unsigned long)ep_buf, EP_PAGES)) {
        pr_err("ep_memprotect: failed to set memory read-only\n");
        vfree(ep_buf);
        return -EFAULT;
    }

    pr_info("ep_memprotect: buffer is now read-only\n");

Step 3 — Restore Protection Before Freeing

static void __exit ep_memprotect_exit(void)
{
    /* Always restore RW before vfree(), see the pitfalls section below */
    set_memory_rw((unsigned long)ep_buf, EP_PAGES);
    vfree(ep_buf);
    pr_info("ep_memprotect: buffer released\n");
}

module_init(ep_memprotect_init);
module_exit(ep_memprotect_exit);
MODULE_LICENSE("GPL");

Load and unload it like any other module:

$ make
$ sudo insmod ep_memprotect.ko
$ dmesg | tail
$ sudo rmmod ep_memprotect

If you deliberately add a write to ep_buf after the set_memory_ro() call, dmesg will show a permission-violation page fault the moment that instruction executes — the exact same class of failure production kernel hardening relies on to catch bugs early instead of letting them corrupt nearby memory silently.

Real-World Use Cases for Kernel Memory Protection

You will run into this pattern outside of toy demos too:

Guard pages around sensitive buffers
Read-only module .rodata after init
JIT-compiled BPF code segments
Immutable configuration tables
Defense against use-after-free writes

In each case, the underlying idea of Linux kernel memory protection is the same: writable only while it needs to be, locked down everywhere else.

Common Mistakes and Troubleshooting

Most problems with kernel memory protection in real modules trace back to one of these:

Passing a byte count instead of a page count to set_memory_ro()
Calling set_memory_*() on a non-page-aligned address
Forgetting set_memory_rw() before vfree()/kfree()
Calling these APIs from interrupt or atomic context
Assuming kmalloc() memory behaves identically to vmalloc() memory here

Tip: if dmesg shows a supervisor write access permission fault at an address you allocated yourself, that is almost always working correctly, not a bug — it means kernel memory protection just did its job and caught an unintended write.

Warning: freeing memory while it is still marked read-only can trigger warnings on debug kernels, because the allocator may need to write into the freed pages during teardown. Always flip protection back with set_memory_rw() first.

Best Practices for Linux Kernel Memory Protection

Initialize data before locking memory
Always restore RW before freeing
Check the return value of set_memory_*()
Keep protected regions page-aligned and page-sized
Document why a region is protected, for the next maintainer

Performance Considerations

Changing page protection is not free. Under the hood it walks page tables and, on multi-core systems, typically needs a TLB shootdown so every CPU sees the new permissions. That is fine for a handful of calls at module load or teardown time, but calling set_memory_ro()/set_memory_rw() in a hot path — anything per-packet or per-request — will show up clearly in profiling. Treat kernel memory protection as a setup/teardown-time operation, not a steady-state one.

Security Considerations

Kernel memory protection is one layer of defense, not a complete answer on its own. It stops accidental or malicious writes to a specific region, but it does not stop an attacker who can redirect execution before the protection is applied, and it does not replace input validation. Pair it with other kernel hardening options in your build configuration for meaningful defense in depth.

Summary / Key Takeaways

kmalloc() is contiguous and fast; vmalloc() is flexible but slower
__vmalloc()’s old protection argument no longer exists
set_memory_ro()/set_memory_rw() are the current API
Protection changes cost time — avoid hot paths
Always unlock before freeing

Conclusion

Linux kernel memory protection has moved on from the days of passing a protection flag straight into an allocator call. The current model — allocate first, then explicitly lock pages down with set_memory_ro() and friends — is more explicit, easier to audit, and matches how the kernel itself protects module text and other sensitive regions today. Once you have written and tested the small example in this lecture, you already understand the mechanism used across much larger kernel hardening features. That is the real value of learning kernel memory protection properly, as part of a structured, free Linux kernel programming course, rather than picking up isolated snippets from scattered sources.

Frequently Asked Questions

What is Linux kernel memory protection?

It is the set of kernel APIs and mechanisms used to control read, write, and execute permissions on specific pages of kernel memory at runtime.

Why can’t I pass a protection flag to __vmalloc() anymore?

The protection parameter was removed from the public __vmalloc() API in recent kernel development cycles because nearly every caller used the default protection, so the interface was simplified.

What is the difference between kmalloc() and vmalloc()?

kmalloc() returns physically contiguous memory and is fast, while vmalloc() returns virtually contiguous memory that may be scattered physically, and is better suited to larger allocations.

How do I make kernel memory read-only today?

Allocate the memory normally, initialize it, then call set_memory_ro() on the page-aligned address with the correct page count.

Can I call set_memory_ro() from interrupt context?

No. These calls can sleep and modify page tables across CPUs, so they must be made from normal process context.

What happens if I write to memory I’ve marked read-only?

The CPU raises a page fault, and the kernel reports it as an oops with a permission-violation error code in dmesg.

Do I need to unlock memory before freeing it?

Yes. Call set_memory_rw() to restore normal permissions before kfree() or vfree(), to avoid warnings on debug-enabled kernels.

Does this work the same way on ARM64 as on x86_64?

The set_memory_*() API is architecture-independent at the call site; each architecture implements the underlying page table changes, so your module code stays portable.

Linux kernel memory protection
free Linux kernel programming course
free Linux device drivers course
free embedded systems course
vmalloc
set_memory_ro

Continue the Free Linux Kernel Programming Course

Keep going with the next lecture, or revisit the previous one.

← Previous Lecture
Next Lecture →

← Previous Lecture
|
Next Lecture →

Leave a Reply

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