183 lines
6.8 KiB
C
183 lines
6.8 KiB
C
/*
|
|
* DIRTYFAIL — mitigate.c — defensive deployment
|
|
*
|
|
* See mitigate.h for the design.
|
|
*/
|
|
|
|
#include "mitigate.h"
|
|
|
|
#include <fcntl.h>
|
|
#include <sys/stat.h>
|
|
#include <stdlib.h>
|
|
|
|
#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 <name>`");
|
|
return DF_OK;
|
|
}
|