This week at Miggo, I was tasked with detecting two recent Linux local privilege escalation vulnerabilities: CopyFail and DirtyFrag.

Our research team provided me with detailed analysis of how both bugs work, and the exploitation primitive reminded me immediately of Dirty Pipe: splice() combined with a controlled write to a page that is already resident in the page cache.

The detection requirement was specific: high confidence, minimal false positives, and no broad mitigations that would break legitimate workloads or require maintaining binary allowlists (a bad idea).

There is a lot of material to cover. We’ll go through:

  • Which socket subsystems were vulnerable
  • A quick look at eBPF LSM
  • What is CopyFail
  • What is DirtyFrag
  • Where most detections fail
  • How we approached detections outside the box
  • Conclusion

Also see the sister post on our main website.

The socket subsystems involved

Before getting to the exploits and detection, it helps to understand why these socket subsystems exist and what normal use looks like. Both bugs live in parts of the kernel that are genuinely useful and actively used.

AF_ALG — the kernel crypto API socket interface

AF_ALG sockets were introduced to give userspace a way to reach the kernel’s crypto subsystem without routing data through a separate daemon or copying it into a library.

An application creates a socket in the AF_ALG family, binds it to a named algorithm (e.g., "gcm(aes)"), and then uses standard socket operations - setsockopt, sendmsg, recvmsg - to drive encryption, decryption, and authentication. The kernel does the work; the data never has to leave kernel space for the crypto engine.

Tools that deal with disk encryption, VPNs, or kernel-assisted TLS offload use this path. It keeps keys manageable from userspace while offloading computation. In normal usage, the sequence is straightforward: create, bind, set a valid key once, then operate on data.

The associated authentication data length (AEAD_ASSOCLEN) is also configured once, at session initialization, according to what the protocol requires.

UDP_ENCAP_ESPINUDP — IPsec NAT traversal

IPsec operates at the network layer and, in its default transport mode, is invisible to UDP IPSec sends. The problem is NAT: NAT devices generally cannot handle raw ESP packets (which IPSec is comprised of) because they have no port numbers to track state against.

UDP_ENCAP_ESPINUDP wraps ESP packets inside UDP, which allows NAT traversal to work. You configure this once on a socket that will carry IPsec traffic, set it, and leave it.

That means a legitimate VPN daemon or an IPsec stack will create a small number of long-lived sockets with this option set. It will not spray dozens of short-lived sockets and set the same option in rapid succession.

RxRPC — the AFS/Kerberos transport

RxRPC is a reliable UDP-based transport protocol used primarily by AFS (Andrew File System) and, in the Linux kernel, by the kernel’s Kerberos (rxkad) authentication path.

It has its own socket family (AF_RXRPC) and its own security options:

  • RXRPC_SECURITY_KEY installs a Kerberos ticket for authenticated sessions
  • RXRPC_MIN_SECURITY_LEVEL enforces the minimum level of protection required on the connection.
Real RxRPC clients — AFS clients, for instance — set these options at a measured pace, once per connection, before communicating with a fileserver. They do not create and configure dozens of authenticated sockets in rapid succession before dropping them.

eBPF LSM: the right attachment point

Why this hook exists

The Linux Security Module framework provides a set of hooks throughout the kernel at security-relevant decision points. These hooks predate eBPF by a significant margin; they were designed to give implementations like SELinux and AppArmor a stable, auditable place to enforce policy.

When the kernel is about to execute a sensitive operation — a setsockopt call, a mmap, a file open - it calls through the LSM hook. SELinux and AppArmor registered their enforcement callbacks there at boot time.

  • security_socket_setsockopt is one such hook. It fires on every setsockopt call that passes initial validation, receiving the socket, the level, the option name, and the caller’s credentials. SELinux uses it to enforce network socket policy; AppArmor uses it to check socket rules in profiles.

eBPF programs at LSM hooks

Since Linux 5.7, eBPF programs can attach to LSM hooks directly, through the BPF_PROG_TYPE_LSM program type and the BPF_LSM_MAC attach type.

This does not require that an LSM backend (SELinux, AppArmor) be active. The eBPF program is registered separately and runs regardless of whether a traditional MAC system is enabled.

This matters in practice because the majority of general-purpose Linux distributions do not ship with a strict MAC policy enforced by default. Ubuntu, for example, ships AppArmor with a limited set of active profiles.

Fedora ships SELinux in enforcing mode but with a policy that may or may not cover the operations relevant here. In many container and cloud environments, MAC is deliberately disabled for operational simplicity.

This matters in practice because the majority of general-purpose Linux distributions do not ship with a strict MAC policy enforced by default. Ubuntu, for example, ships AppArmor with a limited set of active profiles.
Fedora ships SELinux in enforcing mode but with a policy that may or may not cover the operations relevant here. In many container and cloud environments, MAC is deliberately disabled for operational simplicity.

An eBPF LSM program runs at the same hook regardless. It can observe the call, record state in a BPF map, and emit a signal — without any dependency on a userspace policy daemon or a kernel compiled with a particular CONFIG_DEFAULT_SECURITY.

BPF LSM programs can attach to these hooks on any kernel with CONFIG_BPF_LSM=y and bpf present in the active LSM list, which is a separate condition from BTF availability.

The distinction between detection and enforcement is a matter of program return value.

<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/Flip.min.js"></script>

<script>
  document.addEventListener("DOMContentLoaded", (event) => {
    gsap.registerPlugin(Flip);
    const state = Flip.getState("");
    const element = document.querySelector("");
    element.classList.toggle("");
    Flip.from(state, {
      duration: 0,
      ease: "none",
      absolute: true,
    });
  });
</script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/Flip.min.js"></script>

<script>
  document.addEventListener("DOMContentLoaded", (event) => {
    gsap.registerPlugin(Flip);
    const state = Flip.getState("");
    const element = document.querySelector("");
    element.classList.toggle("");
    Flip.from(state, {
      duration: 0,
      ease: "none",
      absolute: true,
    });
  });
</script>