Added dirtyfrag module#143
Conversation
4565be0 to
4bde0b0
Compare
Detects Dirty Frag kernel page-cache write vulnerabilities (xfrm-ESP and RxRPC) and optionally applies mitigation via modprobe.d module blacklisting.
4bde0b0 to
98fde78
Compare
oldgiova
left a comment
There was a problem hiding this comment.
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)", |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Same here: maybe you have to drop the 4?
| or => { | ||
| "_esp_conf_exists", | ||
| "_userns_conf_exists", | ||
| "!_userns_enabled", |
There was a problem hiding this comment.
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}"), |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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", |
| - `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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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"`. |
There was a problem hiding this comment.
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}'", |
There was a problem hiding this comment.
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}'", |
There was a problem hiding this comment.
remove full paths to executables, useshell is in effect.
| "kernel_patch_check" usebundle => "dirtyfrag:kernel_patch_check"; | ||
|
|
||
| vars: | ||
| # --- Constants --- |
There was a problem hiding this comment.
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.
| 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" | ||
| ); |
There was a problem hiding this comment.
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.
|
|
||
| classes: | ||
| # --- CMDB toggles --- | ||
| "_mitigate_esp" expression => strcmp("true", "$(mitigate_esp)"); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
I suppose you could have iterated on these bits, supplying an slist of suffixes of [ .ko, .ko.zst, .ko.xz ]
|
|
||
| commands: | ||
| # --- Unload ESP/IPComp modules after conf written --- | ||
| _mitigate_esp.dirtyfrag_esp_conf_repaired:: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
Thanks for the reviews, great stuff. I am working on an overhaul |
No description provided.