Skip to content

Added dirtyfrag module#143

Draft
nickanderson wants to merge 1 commit into
cfengine:masterfrom
nickanderson:security/dirtyfrag
Draft

Added dirtyfrag module#143
nickanderson wants to merge 1 commit into
cfengine:masterfrom
nickanderson:security/dirtyfrag

Conversation

@nickanderson
Copy link
Copy Markdown
Member

No description provided.

@nickanderson nickanderson requested a review from olehermanse May 13, 2026 19:16
@nickanderson nickanderson force-pushed the security/dirtyfrag branch 2 times, most recently from 4565be0 to 4bde0b0 Compare May 13, 2026 19:22
Detects Dirty Frag kernel page-cache write vulnerabilities (xfrm-ESP and
RxRPC) and optionally applies mitigation via modprobe.d module blacklisting.
Copy link
Copy Markdown

@oldgiova oldgiova left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've asked Claude Code to helping me review this and here's the finding (the ones that I understood and confirmed with human double-checks)

"# Dirty Frag CVE-2026-43284 mitigation: block xfrm-ESP and IPComp$(const.n)",
"install esp4 /bin/false$(const.n)",
"install esp6 /bin/false$(const.n)",
"install ipcomp4 /bin/false$(const.n)",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you have to drop the 4?

"install ipcomp /bin/false$(const.n)",

# Dirty Frag CVE-2026-43284 mitigation: block xfrm-ESP and IPComp
install esp4 /bin/false
install esp6 /bin/false
install ipcomp4 /bin/false
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: maybe you have to drop the 4?

or => {
"_esp_conf_exists",
"_userns_conf_exists",
"!_userns_enabled",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RedHat/SUSE have /proc/sys/kernel/unprivileged_userns_clone, so !_userns_enabled is always true, so _esp_mitigated is true. The consequence is that every RHEL/SLES host with esp4.ko on disk are listed as mitigated always.

Maybe probe user.max_user_namespaces instead as an evidence of mititagion?


# --- Read the unprivileged user namespace setting ---
"_ns_val"
string => readfile("${_ns_proc}"),
Copy link
Copy Markdown

@oldgiova oldgiova May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readfile() does not strip trailing newlines, so the comparison is false even if the file exists. Maybe use sysctlvalue() ?

  vars:
    "_max_userns" string => sysctlvalue("user.max_user_namespaces");

  classes:
    "_userns_disabled" expression => strcmp("0", "${_max_userns}");

Then in _esp_mitigated, drop the userns leg entirely, and fold it into the vulnerability condition instead:

  "dirtyfrag_esp_needs_mitigation"
    and => { "dirtyfrag_esp_present", "!_userns_disabled", "!_esp_mitigated" };

# patched version comes first (or equal) in version-sorted order.
# printf '%s\n' "$patched" "$running" | sort -V | head -1
# If result == patched, then running >= patched.
"_esp_kernel_patched"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These classes are not passed by default in main, so you should widen the scope, right?

{
"label": "RHEL/CentOS/Alma/Rocky/OL 8",
"id_match": "^(rhel|centos|rocky|almalinux|ol)$",
"version_match": "^8",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't match 8.10 is it?

Copy link
Copy Markdown
Contributor

@craigcomstock craigcomstock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good. :)

- `NOT AFFECTED` -- vulnerable modules not present on this host
- **Dirty Frag CVE-2026-43500 (RxRPC) status**:
- `VULNERABLE (rxrpc loaded)` -- module currently in memory
- `VULNERABLE (module on disk, not loaded)` -- module present but not loaded; latent risk
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the same as the above, should have some differentiation to know which CVE is "module on disk, not loaded".

- **Dirty Frag CVE-2026-43500 (RxRPC) status**:
- `VULNERABLE (rxrpc loaded)` -- module currently in memory
- `VULNERABLE (module on disk, not loaded)` -- module present but not loaded; latent risk
- `PATCHED (kernel fix applied)` -- running kernel version includes the fix (auto-detected or admin-declared)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, maybe the inventory name is the name of the CVE and the values what you are enumerating here... in that case the values should NOT include any mention of the CVE and maybe even not include comments so that they are more easily queried for just the CAPS portion.


This blocks the ESP/IPComp exploit path without blacklisting the modules, preserving IPsec functionality. Use this instead of `mitigate_esp` on hosts that require IPsec. Note: this does **not** mitigate CVE-2026-43500 (RxRPC) and may break rootless containers (Podman, Docker rootless), Flatpak, and browser sandboxes. Applied via `sysctl --system` on first write.

All mitigations are **disabled by default** -- the module only reports status unless the corresponding CMDB variable is set to `"true"`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the CMDB variable name?

# If result == patched, then running >= patched.
"_esp_kernel_patched"
expression => returnszero(
"/usr/bin/test \"$(const.dollar)(/usr/bin/printf '%s\n' '${_esp_patched_ver}' '$(default:sys.release)' | /usr/bin/sort -V | /usr/bin/head -1)\" = '${_esp_patched_ver}'",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe use paths here to avoid trouble with OS different paths to these common commands? You are already specifying useshell so might as well remove the full paths to the commands for flexibility sake.


"_rxrpc_kernel_patched"
expression => returnszero(
"/usr/bin/test \"$(const.dollar)(/usr/bin/printf '%s\n' '${_rxrpc_patched_ver}' '$(default:sys.release)' | /usr/bin/sort -V | /usr/bin/head -1)\" = '${_rxrpc_patched_ver}'",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove full paths to executables, useshell is in effect.

"kernel_patch_check" usebundle => "dirtyfrag:kernel_patch_check";

vars:
# --- Constants ---
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably remove this comment, these are not constants in language terms though they may be in effect such things in usage. 99% of vars in cfengine policy language are vars right? even though you can redefine them at will.

Comment on lines +192 to +215
string => ifelse(
"dirtyfrag_esp_needs_mitigation._esp_any_loaded",
"VULNERABLE (${_esp_loaded_names} loaded)",
"dirtyfrag_esp_needs_mitigation",
"VULNERABLE (modules on disk, none loaded)",
"dirtyfrag:kernel_patch_check._esp_kernel_patched|_esp_admin_patched",
"PATCHED (kernel fix applied)",
"dirtyfrag_esp_present._esp_mitigated",
"MITIGATED (blacklist in place)",
"NOT AFFECTED"
);

"_rxrpc_status"
string => ifelse(
"dirtyfrag_rxrpc_needs_mitigation._rxrpc_loaded",
"VULNERABLE (rxrpc loaded)",
"dirtyfrag_rxrpc_needs_mitigation",
"VULNERABLE (module on disk, not loaded)",
"dirtyfrag:kernel_patch_check._rxrpc_kernel_patched|_rxrpc_admin_patched",
"PATCHED (kernel fix applied)",
"dirtyfrag_rxrpc_present._rxrpc_mitigated",
"MITIGATED (blacklist in place)",
"NOT AFFECTED"
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for all of these I mentioned in the README that maybe simpler values would be easier to see and query and the inventory NAME will convey which CVE is in play for that value so no need to repeat it in the value.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔


classes:
# --- CMDB toggles ---
"_mitigate_esp" expression => strcmp("true", "$(mitigate_esp)");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why vars with value "true" instead of classes in CMDB? It would seem that classes would make more sense. Maybe add a note here that you use variables instead of classes because you want to enable global settings like groups for all devices and then override individual hosts with CMDB values other than true. We can't specify CMDB data that REMOVES a class so this workaround is needed. Right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it was at least concern about shadowing and not overriding classes to false that drove me towards a variable here.

"_ipcomp6_on_disk" expression => fileexists("${_ipcomp6_path}");

# Compressed variants (.ko.zst, .ko.xz)
"_esp4_on_disk_z" expression => fileexists("${_esp4_path}.zst");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose you could have iterated on these bits, supplying an slist of suffixes of [ .ko, .ko.zst, .ko.xz ]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, could have.


commands:
# --- Unload ESP/IPComp modules after conf written ---
_mitigate_esp.dirtyfrag_esp_conf_repaired::
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you going to miss things sometimes if only when _repaired? Maybe a kernel module promise would be better... promise that these are not loaded.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love the overhead of custom promise types. semantically reading is way nicer, but it's a bit of yeuck for me every time i think about custom promises that would be backed with an interpreter.

but yes, keying on just repaired can miss stuff. Better to not key off indirect state and keyu off fact that module is loaded and mitigation is desired.

@nickanderson nickanderson marked this pull request as draft May 15, 2026 17:52
@nickanderson
Copy link
Copy Markdown
Member Author

Thanks for the reviews, great stuff. I am working on an overhaul

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants