Skip to content

Repository files navigation

linux-pagecache-cve-audit

CVE-2016-5195 CVE-2022-0847 CVE-2026-31431 CVE-2026-43284 CVE-2026-43500 CVE-2026-46300 CVE-2026-43503 CVE-2026-46331

Shell License Version CI

Read-only audit script for the Linux page-cache local privilege escalation family — a class of kernel bugs where zero-copy / shared-page invariants fail, allowing an unprivileged local user to corrupt page-cache memory and escalate to root by poisoning cached setuid binaries (/bin/su, sudo, ...).

  ____   _    ____ _____ ____    _    ____ _   _ _____
 |  _ \ / \  / ___| ____/ ___|  / \  / ___| | | | ____|
 | |_) / _ \| |  _|  _|| |     / _ \| |   | |_| |  _|
 |  __/ ___ \ |_| | |__| |___ / ___ \ |___|  _  | |___
 |_| /_/   \_\____|_____\____/_/   \_\____|_| |_|_____|

      C V E - A U D I T   |  Linux page-cache LPE family
      v1.1                |  by Quaerendir

What this audits

CVE Name Subsystem Disclosed Mitigation surface
CVE-2016-5195 Dirty COW mm/gup (COW race) 2016-10-19 none (kernel patch only)
CVE-2022-0847 Dirty Pipe fs/splice + pipe_buffer 2022-03-07 none (kernel patch; seccomp-block splice())
CVE-2026-31431 Copy Fail crypto AF_ALG / AEAD 2026-04-29 install algif_aead /bin/false (a blacklist line does not stop the autoload)
CVE-2026-43284 Dirty Frag net/xfrm + net/rxrpc 2026-05-07 blacklist esp4, esp6, rxrpc
CVE-2026-43500 Dirty Frag (rxrpc half) net/rxrpc 2026-05-07 same as above
CVE-2026-46300 Fragnesia net/xfrm ESP-in-TCP + skb_try_coalesce 2026-05-13 blacklist esp4, esp6
CVE-2026-43503 DirtyClone net/core __pskb_copy_fclone + skb_shift 2026-05-23 blacklist esp4, esp6 (same as Fragnesia)
CVE-2026-46331 pedit COW net/sched act_pedit 2026-06-16 install act_pedit /bin/false (blacklist works only on 6.9+)

The shared shape: each one is a kernel fast path that touches pages belonging to the page cache while failing an ownership invariant. Each one chains through unshare(CLONE_NEWUSER | CLONE_NEWNET) to obtain CAP_NET_ADMIN in a namespace (except Dirty COW and Dirty Pipe, which don't need it). Each one corrupts a cached binary in memory while leaving the on-disk file untouched — so file-integrity monitors see nothing.

Why one tool for all six

These bugs share more than a vibe. They share:

  • Operational triage flow: identify the running kernel, check vendor advisory status, check module reachability, check userns gate, hunt behavioural IoCs.
  • Mitigation primitives: modprobe install /bin/false for the relevant module, sysctl user.max_user_namespaces=0 to break the userns chain.
  • Detection signals: unexpected module loads, namespace creation followed by setuid execution, tc/ip xfrm/af_alg syscall activity in places it has no business being.

Running six separate scripts is sysadmin theater. One dispatcher with a check-per-CVE plugin layout lets you audit selectively, JSON-aggregate across a fleet, and add the next entry in this family with a single new file in checks/.

Architecture

linux-pagecache-cve-audit/
├── linux-pagecache-cve-audit.sh    # dispatcher
├── lib/
│   ├── common.sh                   # inventory, output, scoring, module probes
│   ├── ioc.sh                      # behavioural IoC hunt (auditd + journalctl)
│   └── prometheus.sh               # node_exporter textfile output
├── checks/
│   ├── cve-2016-5195.sh            # Dirty COW
│   ├── cve-2022-0847.sh            # Dirty Pipe
│   ├── cve-2026-31431.sh           # Copy Fail
│   ├── cve-2026-43284.sh           # Dirty Frag (+ CVE-2026-43500)
│   ├── cve-2026-46300.sh           # Fragnesia
│   ├── cve-2026-43503.sh           # DirtyClone
│   └── cve-2026-46331.sh           # pedit COW
├── examples/
│   ├── sample-output.txt
│   ├── sample-output.json
│   ├── sample-output-quiet.txt
│   ├── sample-output.prom
│   └── cve-list.txt
├── tests/
│   ├── run_tests.sh                # regression test driver
│   ├── mock_bin/                   # deterministic mock binaries
│   └── fixtures/                   # os-release and modprobe.d samples
└── .github/workflows/shellcheck.yml

Each checks/cve-XXXX-YYYY.sh exposes two functions: cve_XXXX_YYYY_check (runs the audit, calls record_result) and cve_XXXX_YYYY_mitigation (prints mitigation guidance). The dispatcher sources lib/common.sh, sources each selected check, and aggregates verdicts.

The common library handles:

  • Host inventory (kernel release, distro, kernel package version via rpm/dpkg).
  • Module reachability across four signals: loaded, modular-and-loadable, built-in, blocked by an install override, or by blacklist when the kernel autoloads the module through an alias (esp4/esp6, rxrpc, act_pedit on 6.9+; not algif_aead or older act_pedit, which are requested by name).
  • Kernel config detection (/boot/config-$(uname -r) or /proc/config.gz).
  • Version comparison via dpkg --compare-versions with sort -V fallback.
  • Userns probes including the Ubuntu 24.04+/26.04 AppArmor userns gates.
  • Netns probes (user.max_net_namespaces) used as an additional userns-gated chain break.
  • Test-mode fixture paths controlled by __UNIT_TEST=1 for the regression suite.
  • Scoring: compute_verdict <vendor_status> <surface_score> <userns_required> returns verdict|score.

lib/ioc.sh adds an opt-in behavioural pass over auditd (if rules were installed) and journalctl looking for module-load events, public-PoC names, and userns + setuid execution patterns. lib/prometheus.sh emits node_exporter textfile collector output.

Usage

# Full audit, all six CVEs.
sudo ./linux-pagecache-cve-audit.sh

# Single CVE.
sudo ./linux-pagecache-cve-audit.sh --cve 2026-46331

# Subset.
sudo ./linux-pagecache-cve-audit.sh --cve 2026-46331,2026-46300,2026-43284

# Only this year, or everything up to a given year.
sudo ./linux-pagecache-cve-audit.sh --since 2026
sudo ./linux-pagecache-cve-audit.sh --until 2022

# Add behavioural IoC pass over auditd + journalctl.
sudo ./linux-pagecache-cve-audit.sh --hunt
sudo ./linux-pagecache-cve-audit.sh --hunt --hunt-since 2026-06-01

# Machine-readable.
sudo ./linux-pagecache-cve-audit.sh --json

# One-line per CVE, for fleet aggregation.
sudo ./linux-pagecache-cve-audit.sh --quiet

# node_exporter textfile collector format.
sudo ./linux-pagecache-cve-audit.sh --prometheus

# Print mitigation guidance for a specific CVE.
./linux-pagecache-cve-audit.sh --mitigation CVE-2026-46331

# List known CVEs.
./linux-pagecache-cve-audit.sh --list

# Print version.
./linux-pagecache-cve-audit.sh --version

# Paste-ready auditd rules and Prometheus alerting rules.
./linux-pagecache-cve-audit.sh --print-audit-rules
./linux-pagecache-cve-audit.sh --print-alert-rules

Exit codes (aggregated across CVEs checked)

Code Meaning
0 All PATCHED or NOT_APPLICABLE
1 At least one MITIGATED
2 At least one VULNERABLE
3 At least one UNKNOWN and no VULNERABLE
4 ERROR (missing files, bad args)

The exit code is the worst verdict, so failed_when: rc >= 2 in Ansible is the conservative gate.

Sample output

See examples/. Abbreviated:

== Summary ==
  CVE                 Name          Verdict       Score  Detail
  ---                 ----          -------       -----  ------
  CVE-2016-5195       Dirty COW     PATCHED          20  6.18 >= 4.9 (well-backported)
  CVE-2022-0847       Dirty Pipe    PATCHED          25  6.18: above all 5.x stable fix lines
  CVE-2026-31431      Copy Fail     UNKNOWN          30  Ubuntu: check USN
  CVE-2026-43284      Dirty Frag    UNKNOWN          45  Ubuntu: check USN per release stream
  CVE-2026-46300      Fragnesia     UNKNOWN          45  Ubuntu: check USN
  CVE-2026-46331      pedit COW     UNKNOWN          45  Ubuntu 24.04: check USN per variant

  Overall verdict: UNKNOWN  (worst case across all CVEs)

Testing

A regression suite lives under tests/. It runs the dispatcher against mock binaries (tests/mock_bin/) and fixture files (tests/fixtures/) so the results are deterministic and do not depend on the host kernel:

./tests/run_tests.sh

The suite also serves as documentation for how the script behaves on different distros and kernel versions. shellcheck is run over linux-pagecache-cve-audit.sh, lib/*.sh, checks/*.sh, tests/run_tests.sh, and tests/mock_bin/*.

Mass deployment

Ansible

- name: audit page-cache LPE family
  hosts: all
  become: true
  tasks:
    - name: run audit (JSON)
      ansible.builtin.script: linux-pagecache-cve-audit.sh --json
      register: audit
      changed_when: false
      failed_when: false

    - name: parse result
      ansible.builtin.set_fact:
        pagecache_audit: "{{ audit.stdout | from_json }}"

    - name: fail on VULNERABLE hosts
      ansible.builtin.fail:
        msg: "Host has VULNERABLE CVEs: {{ pagecache_audit.results | selectattr('verdict','equalto','VULNERABLE') | map(attribute='cve') | list }}"
      when: pagecache_audit.overall_verdict == 'VULNERABLE'

Fleet aggregation with jq

# Collect from many hosts.
for h in $(cat hosts.txt); do
  ssh "$h" "sudo /usr/local/bin/linux-pagecache-cve-audit.sh --json" 2>/dev/null \
    | jq --arg h "$h" '. + {host: $h}'
done > audit.jsonl

# Hosts vulnerable to pedit COW.
jq -r 'select(.results[] | select(.cve == "CVE-2026-46331" and .verdict == "VULNERABLE")) | .host' audit.jsonl

# Verdict matrix.
jq -r '[.host, (.results[] | .cve + "=" + .verdict)] | @tsv' audit.jsonl

Prometheus textfile collector

Native output via --prometheus. Drop into a cron and node_exporter picks it up:

# /etc/cron.daily/pagecache-cve-audit
#!/bin/sh
/usr/local/bin/linux-pagecache-cve-audit.sh --prometheus --hunt \
  > /var/lib/node_exporter/textfile_collector/pagecache_cve.prom.tmp
mv /var/lib/node_exporter/textfile_collector/pagecache_cve.prom{.tmp,}

Metrics emitted:

Metric Labels Meaning
linux_pagecache_cve_risk_score cve, name, verdict 0-100 score
linux_pagecache_cve_status cve, name, verdict 1 = this CVE has this verdict
linux_pagecache_cve_audit_overall verdict 1 for the worst-case verdict
linux_pagecache_cve_audit_timestamp (none) unix epoch of last run
linux_pagecache_cve_audit_ioc_hits (none) IoC count from --hunt

Get paste-ready alerting rules:

./linux-pagecache-cve-audit.sh --print-alert-rules > /etc/prometheus/rules/pagecache_cve.yml

Behavioural IoC hunt

--hunt runs an opt-in pass over auditd (if rules are installed) and journalctl looking for:

  • Public-PoC names: packet_edit_meme, dirtyfrag, fragnesia, copyfail, dirtypipe, dirtycow.
  • Module-load events for act_pedit, algif_aead, esp4, esp6, rxrpc.
  • unshare(CLONE_NEWUSER|CLONE_NEWNET) followed by setuid execution.

For auditd-based detection you need rules installed beforehand. Get paste-ready rules:

./linux-pagecache-cve-audit.sh --print-audit-rules > /etc/audit/rules.d/99-pagecache-cve.rules
sudo augenrules --load

The hunt is triage, not proof. False positives are expected on hosts where tc, IPsec, or AF_ALG are legitimately used.

Risk scoring model

A weighted sum, capped at 100:

Signal Weight
Vendor verdict VULNERABLE +50
Vendor verdict UNKNOWN +30
Vendor verdict NOT_APPLICABLE 0 (kernel predates the bug)
Module built into kernel +25
Module loaded right now +25
Module loadable, no override +20-22
Module loadable, override active +5
Unprivileged userns reachable, no AppArmor gate +15
Unprivileged userns reachable, AppArmor gate active +8
Unprivileged netns blocked (user.max_net_namespaces=0) can downgrade VULNERABLE to MITIGATED for netns-gated CVEs

For multi-module CVEs (Dirty Frag, Fragnesia), surface from each independent path stacks up to a cap of 50.

For Dirty COW and Dirty Pipe, the "module surface" is conceptually replaced with "primitive is always reachable" (fixed +20-25), because the bug lives in core mm or splice paths with no module to disable.

The architectural failure

This bug family is not "Linux is broken". It's a structural debt from a 1990s design choice: zero-copy plus the page cache as universal intermediary between filesystems and everything that reads files. Every kernel fast path that touches skb, pipe_buffer, iov_iter, or bvec operates on pages that might be page-cache-backed. The invariant "before you write, prove the page is yours" has to be maintained at hundreds of call sites. Each one missed gives you one of these CVEs.

CVE Year Primitive Page-ownership invariant violated
CVE-2016-5195 2016 mm/gup COW race private/shared distinction during fault retry
CVE-2022-0847 2022 pipe buffer flag leak PIPE_BUF_FLAG_CAN_MERGE survives across pipe contexts
CVE-2026-31431 2026 AF_ALG in-place AEAD source vs destination mapping conflated
CVE-2026-43284 2026 xfrm ESP + rxrpc shared-frag marker dropped during coalesce
CVE-2026-46300 2026 skb_try_coalesce same marker, different call site, the Dirty Frag fix didn't cover it
CVE-2026-43503 2026 __pskb_copy_fclone + skb_shift SKBFL_SHARED_FRAG lost during clone; Fragnesia fix didn't cover these paths
CVE-2026-46331 2026 act_pedit typed-key offset COW range computed before final offset is known

Each fix addresses the proximate cause. None addresses the root cause. Rust-in-kernel targets exactly this class of bug via ownership types in the type system. It will not show up in net/sched or crypto or mm tomorrow, but the trajectory matters.

Safety properties

This script:

  • Never loads kernel modules.
  • Never modifies sysctl, modprobe.d, or any other persistent state.
  • Never executes a public PoC, ever.
  • Is read-only by design; running it on production has no side effects beyond CPU cycles and a few lsmod/modinfo/sysctl -n calls.

For real authorisation-bounded exploitation testing, use the published PoCs in an isolated lab and not via this script.

References

License

MIT. See LICENSE.

About

Multi-CVE audit for the Linux page-cache LPE family with Prometheus output and behavioural IoC hunt

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages