/* * DIRTYFAIL — mitigate.c — defensive deployment * * See mitigate.h for the design. */ #include "mitigate.h" #include #include #include #define MODPROBE_CONF "/etc/modprobe.d/dirtyfail-mitigations.conf" #define SYSCTL_CONF "/etc/sysctl.d/99-dirtyfail-mitigations.conf" /* Modules to blacklist. Each is the kernel module name + reason. */ static const struct { const char *name; const char *reason; } BLACKLIST[] = { {"algif_aead", "Copy Fail (CVE-2026-31431) — authencesn page-cache STORE primitive"}, {"esp4", "Dirty Frag (CVE-2026-43284) — xfrm-ESP IPv4 path"}, {"esp6", "Dirty Frag (CVE-2026-43284) — xfrm-ESP IPv6 path"}, {"rxrpc", "Dirty Frag (CVE-2026-43500) — RxRPC pcbc(fcrypt) path"}, {NULL, NULL}, }; static bool write_file(const char *path, const char *content) { int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) return false; size_t n = strlen(content); ssize_t got = write(fd, content, n); close(fd); return got == (ssize_t)n; } static bool require_root(void) { if (geteuid() != 0) { log_bad("mitigate requires root — re-run as `sudo dirtyfail --mitigate`"); return false; } return true; } static int rmmod_if_loaded(const char *name) { /* Try via /sbin/rmmod (system shell). Returns 0 if module wasn't * loaded or unload succeeded; 1 if unload failed. */ char cmd[256]; snprintf(cmd, sizeof(cmd), "if lsmod | grep -q '^%s '; then " " rmmod %s 2>/dev/null && echo unloaded || echo \"unload failed (in use?)\"; " "else " " echo \"not loaded\"; " "fi", name, name); return system(cmd) == 0 ? 0 : 1; } df_result_t mitigate_apply(void) { log_step("DIRTYFAIL — defensive mitigation deployment"); if (!require_root()) return DF_TEST_ERROR; log_warn("about to apply system-wide mitigations:"); log_warn(" 1. blacklist algif_aead, esp4, esp6, rxrpc via modprobe"); log_warn(" 2. unload those modules if loaded"); log_warn(" 3. set kernel.apparmor_restrict_unprivileged_userns=1 (where AA loaded)"); log_warn(" 4. drop page cache"); fputc('\n', stderr); log_warn("SIDE EFFECTS:"); log_warn(" - blacklisting esp4/esp6 BREAKS IPsec / strongSwan / libreswan VPNs"); log_warn(" - blacklisting rxrpc BREAKS AFS distributed file system clients"); log_warn(" - blacklisting algif_aead BREAKS userspace AEAD via AF_ALG (rare)"); fputc('\n', stderr); log_warn("undo with `dirtyfail --cleanup-mitigate` (removes config files, leaves modules unloaded)"); if (!typed_confirm("DIRTYFAIL")) { log_bad("confirmation declined — aborting"); return DF_OK; } /* 1. Write modprobe blacklist */ char buf[2048]; char *p = buf; p += snprintf(p, sizeof(buf) - (p - buf), "# DIRTYFAIL mitigations — blacklist modules that expose the\n" "# Copy Fail (CVE-2026-31431) and Dirty Frag (CVE-2026-43284,\n" "# CVE-2026-43500) page-cache write primitives.\n" "#\n" "# Generated by `dirtyfail --mitigate`. Remove with\n" "# `dirtyfail --cleanup-mitigate` or by deleting this file.\n" "\n"); for (int i = 0; BLACKLIST[i].name; i++) { p += snprintf(p, sizeof(buf) - (p - buf), "# %s\n" "install %s /bin/false\n", BLACKLIST[i].reason, BLACKLIST[i].name); } if (!write_file(MODPROBE_CONF, buf)) { log_bad("failed to write %s: %s", MODPROBE_CONF, strerror(errno)); return DF_TEST_ERROR; } log_ok("wrote %s", MODPROBE_CONF); /* 2. Unload currently loaded modules */ log_step("unloading currently-loaded modules:"); for (int i = 0; BLACKLIST[i].name; i++) { printf(" %s: ", BLACKLIST[i].name); fflush(stdout); rmmod_if_loaded(BLACKLIST[i].name); } /* 3. Set AppArmor sysctl (only if AA is loaded) */ int sysctl_fd = open("/proc/sys/kernel/apparmor_restrict_unprivileged_userns", O_WRONLY); if (sysctl_fd >= 0) { if (write(sysctl_fd, "1\n", 2) == 2) log_ok("set apparmor_restrict_unprivileged_userns=1 (runtime)"); else log_warn("could not set apparmor_restrict_unprivileged_userns: %s", strerror(errno)); close(sysctl_fd); /* Persist via sysctl.d */ const char *sysctl_content = "# DIRTYFAIL mitigations — block unprivileged userns capability acquisition.\n" "# This prevents the xfrm-ESP / RxRPC / GCM exploit infrastructure from\n" "# obtaining CAP_NET_ADMIN inside a fresh user namespace.\n" "kernel.apparmor_restrict_unprivileged_userns = 1\n"; if (write_file(SYSCTL_CONF, sysctl_content)) log_ok("wrote %s (persists across reboot)", SYSCTL_CONF); else log_warn("could not write %s: %s", SYSCTL_CONF, strerror(errno)); } else { log_hint("AppArmor sysctl not present (kernel without AA, or AA not loaded) — skipping"); } /* 4. Drop page cache */ int dc = open("/proc/sys/vm/drop_caches", O_WRONLY); if (dc >= 0) { ssize_t n = write(dc, "3\n", 2); close(dc); if (n == 2) log_ok("dropped page cache"); } fputc('\n', stdout); log_ok("=== mitigation summary ==="); log_ok(" modprobe blacklist: %s", MODPROBE_CONF); log_ok(" sysctl persistence: %s", SYSCTL_CONF); log_ok(" modules unloaded: algif_aead, esp4, esp6, rxrpc (where loaded)"); fputc('\n', stdout); log_hint("Re-verify with `dirtyfail --scan` — should now report most modes as"); log_hint("preconditions missing or mitigated."); fputc('\n', stdout); log_hint("Ultimate fix: install kernel update with f4c50a4034e6 backport."); return DF_OK; } df_result_t mitigate_revert(void) { log_step("DIRTYFAIL — revert mitigations"); if (!require_root()) return DF_TEST_ERROR; log_warn("removing %s + %s", MODPROBE_CONF, SYSCTL_CONF); log_warn("modules will NOT be auto-loaded — operator decides if/when"); if (!typed_confirm("DIRTYFAIL")) { log_bad("confirmation declined"); return DF_OK; } if (unlink(MODPROBE_CONF) == 0) log_ok("removed %s", MODPROBE_CONF); else if (errno == ENOENT) log_hint("%s did not exist", MODPROBE_CONF); else log_bad("unlink %s: %s", MODPROBE_CONF, strerror(errno)); if (unlink(SYSCTL_CONF) == 0) log_ok("removed %s", SYSCTL_CONF); else if (errno == ENOENT) log_hint("%s did not exist", SYSCTL_CONF); else log_bad("unlink %s: %s", SYSCTL_CONF, strerror(errno)); log_hint("modules can be reloaded individually with `sudo modprobe `"); return DF_OK; }