# My W^X Hook Was Letting Things Be W+X

> Source: <https://naveensrinivasan.com/posts/2026-11-08-my-wx-hook-was-letting-things-be-w-plus-x/>
> Published: 2026-11-08 18:38:00+00:00

5 minutes

# [My W^X Hook Was Letting Things Be W+X](https://naveensrinivasan.com/posts/2026-11-08-my-wx-hook-was-letting-things-be-w-plus-x/)

I added a new eBPF LSM hook to prevent memory from being writable and executable. A bypass in my original code let a page become W+X.

## How Did I Find This?

I wish I had found it myself, but the truth is that AI code scanning helped me identify the gap. The existing policies all say which executables and paths may do what. But what if the code never has a path? Pulled down from the network, kept in memory, executed. None of the policies would apply.

This is what led me to this PR.

## What is In-Memory Execution?

Two types of memory are mapped into a program. One for code and one for data. The code section can be read but not changed. The data section can be changed but not run. Because of this rule, programs can’t easily be tricked into executing something that they pulled over the network.

Take a page of memory the program has already filled with data. Marking it executable allows the program to run that information as code. This is done with a syscall called mprotect. If the program passes the PROT_EXEC flag to that call, it can make the page executable, which lets it run code it just downloaded. It never touches the disk, so no file based policies can act against this.

This is called the W^X rule. You can have write or execute on a memory page, not both.

mprotect is a good place to catch this. If a process wants to write to its data pages, it doesn’t have to notify anyone. But if it wants to mark a page it’s been writing to as executable, it needs to use mprotect to do that.

## What I Built

Since the code uses one BPF LSM hook per action we wanted to protect against, it seemed reasonable to build another hook to catch mprotect.

- If mprotect isn’t setting PROT_EXEC, allow it.
- If we aren’t configured to deny in memory executables, allow it.
- If the process is on the allowed_in_memory_exec list, allow it. There are reasons a process would need this, and there isn’t a clean way to fix it right now.
- Else, we tell the kernel no and report a violation.

We’re also passed the VMA being modified, which is basically what the kernel knows about that memory region, including its current permissions. That will matter in a minute.

I coded this up and made some integration tests for it. They cover anonymous memory and file-backed memory, as well as processes on the allowlist. Sent my PR out.

## What the Bots Found

They caught four things. Three were fairly normal mistakes and were resolved that day. I want to dig deeper into the last one because it can happen to anybody.

My first implementation of this code was:

```
// If the page is already executable, no reason to block it
vm_flags_t vm_flags = BPF_CORE_READ(vma, vm_flags);
if (vm_flags & VM_EXEC)
    return 0;
```

This would check if the page is already marked executable and continue. My thought process was that if the page is already marked executable, there’s nothing to block against. It can’t get any more executable than it already is.

Here is how that code could be abused.

- Ask the kernel to map a page as read and execute. We aren’t blocking writes, so this would go through. Now the page is executable.
- Tell the kernel to change the page to read, write, and execute. This will hit our mprotect hook. We will see that it’s asking for PROT_EXEC and proceed. When we check the current page permissions, we’ll see it’s already marked executable. We will return 0, allowing the kernel to update the page to W+X.

To fix this, I added a check to make sure it isn’t asking for PROT_WRITE.

```
if ((vm_flags & VM_EXEC) && !(prot & PROT_WRITE))
    return 0;
```

If it’s marked executable and not asking for write, nothing will change by allowing it. If it is marked executable and asking to add write, that’s W+X, so we fall through to the allowlist check and deny.

## Why I Missed It

I think every company has some form of this logic implemented. A person who can modify vendors in your system shouldn’t be able to approve payments. Giving one person the ability to do both opens you up to fraud. Usually, the IT department enforces this when granting users privileges.

Say a user can approve payments. They come to you and want to edit vendors as well. Your thinking would probably go: “Ok, this user is already an approver, so nothing is going to change if I give them this privilege.” Now that user can modify vendors and approve payments.

By marking the page as write, I’m editing the vendor. Marking it executable is approving a payment. My mprotect hook is acting as the IT department. Because I only checked what the user already had, I fell for this trap. When I write it out like this, it seems pretty obvious. In code, it wasn’t.

## Lessons Learned

My tests covered only two scenarios: something writable becomes executable, and something executable stays executable. What I should have tested was a page becoming writable while it’s already executable. I didn’t even know that was an option when I was coding this. If you don’t know about something, you won’t test for it.

[https://github.com/bomfather/agent/pull/33](https://github.com/bomfather/agent/pull/33)

When writing rules about memory being executable or writable, don’t check what the page is now. Check what it will be after the call, and make sure it’s never both.
