{"uuid": "6fe519a1-4ace-4173-824a-c874745f1e6e", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-43503", "type": "seen", "source": "https://gist.github.com/Cvar1984/c32ea1598fdf727c699e47d7ff5c8e57", "content": "#!/usr/bin/env bash\n#\n# lpe2026-mitigate.sh \u2014 compensating controls for the 2026 kernel page-cache LPE chain\n#\n# Covers:\n#   CVE-2026-31431            \"Copy Fail\"   \u2014 algif_aead / af_alg (AF_ALG crypto API)\n#   CVE-2026-43284/CVE-2026-43500  \"Dirty Frag\"  \u2014 in-place decrypt in esp4/esp6/rxrpc\n#   CVE-2026-46331            \"pedit COW\"   \u2014 act_pedit (tc packet editor)\n#   (blacklisting esp4/esp6/rxrpc also covers CVE-2026-43503 \"DirtyClone\", which\n#    shares the same in-place-decrypt root cause)\n#\n# All of these let an unprivileged local user corrupt page-cache pages backing\n# read-only files (e.g. setuid binaries) and escalate to root. These are\n# workarounds pending the vendor kernel update for your distro \u2014 install and\n# reboot onto the patched kernel when it's available; this script only reduces\n# exposure until then.\n#\n# Sources consulted: access.redhat.com/security/vulnerabilities/RHSB-2026-002,\n# tuxcare.com/blog/dirty-frag-cve-2026-43284-cve-2026-43500-kernelcare-live-patches-released,\n# thecybersecguru.com/news/linux-lpe-pedit-cow-dirtyclone-cve-2026-46331-cve-2026-43503\n#\n# Tested distro families: RHEL / AlmaLinux / CentOS (dnf|yum + grubby),\n# Ubuntu (apt + update-grub).\n#\n# Usage:\n#   lpe2026-mitigate.sh                    # status + dry-run preview, no changes\n#   lpe2026-mitigate.sh --apply             # mitigate Copy Fail + pedit COW (low risk)\n#   lpe2026-mitigate.sh --apply --include-dirtyfrag   # also esp4/esp6/rxrpc (breaks IPsec/AFS)\n#   lpe2026-mitigate.sh --apply --disable-userns       # also lock down unprivileged userns\n#   lpe2026-mitigate.sh --apply --include-dirtyfrag --disable-userns --dry-run  # preview the full plan\n#   lpe2026-mitigate.sh --revert            # undo everything this script applied\n#   lpe2026-mitigate.sh --revert --dry-run  # preview what a revert would remove\n#\nset -euo pipefail\n\nMODPROBE_DIR=/etc/modprobe.d\nSTATE_DIR=/var/lib/lpe2026-mitigations\nLOG_FILE=/var/log/lpe2026-mitigations.log\nSYSCTL_CONF=/etc/sysctl.d/99-lpe2026-userns.conf\nBOOT_PARAM=\"initcall_blacklist=algif_aead_init\"\n\nAPPLY=0\nINCLUDE_DIRTYFRAG=0\nDISABLE_USERNS_FLAG=0\nASSUME_YES=0\nDO_REVERT=0\nDRY_RUN_FLAG=0\nDRY_RUN=0\nFAMILY=\"\"\n\n# log()/warn() skip the on-disk log during a dry run so a preview never\n# touches disk \u2014 dry() below is stdout-only for the same reason.\nlog() { local m; m=\"[$(date -Is)] $*\"; echo \"$m\"; [[ \"$DRY_RUN\" == 1 ]] || { echo \"$m\" &gt;&gt;\"$LOG_FILE\" 2&gt;/dev/null || true; }; }\nwarn() { local m; m=\"[$(date -Is)] WARNING: $*\"; echo \"$m\" &gt;&amp;2; [[ \"$DRY_RUN\" == 1 ]] || { echo \"$m\" &gt;&gt;\"$LOG_FILE\" 2&gt;/dev/null || true; }; }\ndry() { echo \"  [DRY-RUN] $*\"; }\n\nusage() {\n  cat &lt;&amp;2\n    exit 1\n  fi\n}\n\ndetect_family() {\n  local id id_like all\n  # shellcheck disable=SC1091\n  . /etc/os-release\n  id=\"${ID:-}\"; id_like=\"${ID_LIKE:-}\"\n  all=\" $id $id_like \"\n  if [[ \"$all\" =~ (rhel|centos|almalinux|fedora) ]]; then\n    echo rhel\n  elif [[ \"$all\" =~ (debian|ubuntu) ]]; then\n    echo debian\n  else\n    echo unknown\n  fi\n}\n\nconfirm() {\n  local prompt=\"$1\"\n  [[ \"$ASSUME_YES\" == 1 ]] &amp;&amp; return 0\n  if [[ ! -t 0 &amp;&amp; ! -e /dev/tty ]]; then\n    warn \"Non-interactive and --yes not given; refusing: $prompt\"\n    return 1\n  fi\n  local reply\n  read -r -p \"$prompt [y/N] \" reply /dev/null &gt;\"$statefile\" || true\n}\n\nrestore_prior_sysctl() {\n  local key=\"$1\" statefile=\"$STATE_DIR/sysctl.$key\"\n  if [[ -f \"$statefile\" ]]; then\n    local val; val=\"$(cat \"$statefile\")\"\n    if [[ -n \"$val\" ]]; then\n      sysctl -w \"$key=$val\" &gt;/dev/null &amp;&amp; log \"Restored $key=$val (prior value).\"\n    fi\n    rm -f \"$statefile\"\n  fi\n}\n\nwrite_blacklist() {\n  local file=\"$1\"; shift\n  local path=\"$MODPROBE_DIR/$file\"\n  if [[ \"$DRY_RUN\" == 1 ]]; then\n    dry \"Would write $path, blocking future loads of: $*\"\n    return\n  fi\n  : &gt;\"$path\"\n  local mod\n  for mod in \"$@\"; do\n    echo \"install $mod /bin/false\" &gt;&gt;\"$path\"\n  done\n  log \"Wrote $path blocking future loads of: $*\"\n}\n\nunload_modules() {\n  local mod\n  for mod in \"$@\"; do\n    if lsmod | grep -qw \"^$mod\"; then\n      if [[ \"$DRY_RUN\" == 1 ]]; then\n        dry \"Would unload module $mod (currently loaded).\"\n      elif modprobe -r \"$mod\" 2&gt;/dev/null; then\n        log \"Unloaded module $mod.\"\n      else\n        warn \"$mod is loaded and busy \u2014 could not unload now. Blacklist stops reload, but the in-memory module stays until released or you reboot.\"\n      fi\n    else\n      if [[ \"$DRY_RUN\" == 1 ]]; then\n        dry \"Module $mod not currently loaded \u2014 nothing to unload.\"\n      else\n        log \"Module $mod not currently loaded.\"\n      fi\n    fi\n  done\n}\n\ncheck_module_status() {\n  local mod=\"$1\" blacklisted=\"no\" loaded=\"no\"\n  if grep -rlqF \"install $mod /bin/false\" \"$MODPROBE_DIR\" 2&gt;/dev/null; then\n    blacklisted=\"yes\"\n  fi\n  if lsmod | grep -qw \"^$mod\"; then\n    loaded=\"yes\"\n  fi\n  printf \"  %-14s blacklisted=%-4s currently_loaded=%s\\n\" \"$mod\" \"$blacklisted\" \"$loaded\"\n}\n\nadd_boot_param() {\n  local param=\"$1\"\n  case \"$FAMILY\" in\n    rhel)\n      if ! command -v grubby &gt;/dev/null 2&gt;&amp;1; then\n        warn \"grubby not found; add '$param' to the kernel cmdline manually.\"\n        return\n      fi\n      if grubby --info=ALL 2&gt;/dev/null | grep -q \"$param\"; then\n        log \"Boot parameter '$param' already configured.\"\n      elif [[ \"$DRY_RUN\" == 1 ]]; then\n        dry \"Would run: grubby --update-kernel=ALL --args=\\\"$param\\\" (reboot required to take effect).\"\n      else\n        grubby --update-kernel=ALL --args=\"$param\"\n        log \"Added boot parameter '$param' via grubby. REBOOT REQUIRED to take effect.\"\n      fi\n      ;;\n    debian)\n      local grub_file=/etc/default/grub\n      if [[ ! -f \"$grub_file\" ]]; then\n        warn \"$grub_file not found; add '$param' to the kernel cmdline manually.\"\n        return\n      fi\n      if grep -q \"$param\" \"$grub_file\"; then\n        log \"Boot parameter '$param' already present in $grub_file.\"\n        return\n      fi\n      if [[ \"$DRY_RUN\" == 1 ]]; then\n        dry \"Would back up $grub_file to ${grub_file}.bak., append '$param' to its GRUB_CMDLINE_LINUX_DEFAULT, and run update-grub (reboot required).\"\n        return\n      fi\n      cp \"$grub_file\" \"${grub_file}.bak.$(date +%s)\"\n      if sed -i -E \"s/^(GRUB_CMDLINE_LINUX_DEFAULT=\\\")([^\\\"]*)\\\"/\\1\\2 $param\\\"/\" \"$grub_file\" \\\n        &amp;&amp; grep -q \"$param\" \"$grub_file\"; then\n        update-grub\n        log \"Added boot parameter '$param' to $grub_file and ran update-grub. REBOOT REQUIRED.\"\n      else\n        warn \"Could not auto-edit $grub_file \u2014 add '$param' to GRUB_CMDLINE_LINUX_DEFAULT manually, then run update-grub.\"\n      fi\n      ;;\n  esac\n}\n\nremove_boot_param() {\n  local param=\"$1\"\n  case \"$FAMILY\" in\n    rhel)\n      if ! command -v grubby &gt;/dev/null 2&gt;&amp;1; then\n        return\n      fi\n      if [[ \"$DRY_RUN\" == 1 ]]; then\n        grubby --info=ALL 2&gt;/dev/null | grep -q \"$param\" \\\n          &amp;&amp; dry \"Would run: grubby --update-kernel=ALL --remove-args=\\\"$param\\\" (reboot required).\"\n      else\n        grubby --update-kernel=ALL --remove-args=\"$param\" \\\n          &amp;&amp; log \"Removed boot parameter '$param' via grubby. REBOOT REQUIRED.\"\n      fi\n      ;;\n    debian)\n      local grub_file=/etc/default/grub\n      if [[ -f \"$grub_file\" ]] &amp;&amp; grep -q \"$param\" \"$grub_file\"; then\n        if [[ \"$DRY_RUN\" == 1 ]]; then\n          dry \"Would back up $grub_file to ${grub_file}.bak., remove '$param' from it, and run update-grub (reboot required).\"\n        else\n          cp \"$grub_file\" \"${grub_file}.bak.$(date +%s)\"\n          sed -i -E \"s/ ?${param}//g\" \"$grub_file\"\n          update-grub\n          log \"Removed boot parameter '$param' from $grub_file. REBOOT REQUIRED.\"\n        fi\n      fi\n      ;;\n  esac\n}\n\nmitigate_copyfail() {\n  echo \"-- CVE-2026-31431 Copy Fail (algif_aead / af_alg) --\"\n  write_blacklist \"lpe2026-copyfail.conf\" algif_aead af_alg\n  unload_modules algif_aead af_alg\n  if modinfo algif_aead &gt;/dev/null 2&gt;&amp;1; then\n    log \"algif_aead is a loadable module \u2014 modprobe blacklist is sufficient, effective immediately.\"\n  else\n    log \"algif_aead has no separate .ko on this kernel (likely built-in) \u2014 adding boot-param fallback.\"\n    add_boot_param \"$BOOT_PARAM\"\n  fi\n}\n\nmitigate_pedit() {\n  echo \"-- CVE-2026-46331 pedit COW (act_pedit) --\"\n  write_blacklist \"lpe2026-pedit.conf\" act_pedit\n  unload_modules act_pedit\n}\n\nmitigate_dirtyfrag() {\n  echo \"-- CVE-2026-43284 / CVE-2026-43500 / CVE-2026-43503 Dirty Frag / DirtyClone (esp4, esp6, rxrpc) --\"\n  write_blacklist \"lpe2026-dirtyfrag.conf\" esp4 esp6 rxrpc\n  unload_modules rxrpc esp6 esp4\n  if [[ \"$DRY_RUN\" == 1 ]]; then\n    dry \"Would flush dirty pages (sync) and drop reclaimable page cache (echo 3 &gt; /proc/sys/vm/drop_caches) to clear any already-corrupted clean pages.\"\n    return\n  fi\n  log \"Flushing dirty pages and dropping reclaimable page cache to clear any already-corrupted clean pages.\"\n  sync\n  echo 3 &gt;/proc/sys/vm/drop_caches 2&gt;/dev/null || warn \"Could not write /proc/sys/vm/drop_caches.\"\n}\n\ndisable_userns() {\n  echo \"-- Disabling unprivileged user namespaces (pedit COW / DirtyClone hardening) --\"\n  local key\n  case \"$FAMILY\" in\n    rhel) key=\"user.max_user_namespaces\" ;;\n    debian) key=\"kernel.unprivileged_userns_clone\" ;;\n  esac\n  if [[ \"$DRY_RUN\" == 1 ]]; then\n    dry \"Would save current $key to $STATE_DIR/sysctl.$key, write '$key = 0' to $SYSCTL_CONF, and run: sysctl -w $key=0\"\n    return\n  fi\n  save_prior_sysctl \"$key\"\n  echo \"$key = 0\" &gt;\"$SYSCTL_CONF\"\n  sysctl -w \"$key=0\" &gt;/dev/null\n  log \"Set $key=0 (persisted in $SYSCTL_CONF).\"\n}\n\nrevert_all() {\n  echo \"-- Reverting lpe2026 mitigations --\"\n  local f\n  local blacklist_files=(\n    \"$MODPROBE_DIR/lpe2026-copyfail.conf\"\n    \"$MODPROBE_DIR/lpe2026-pedit.conf\"\n    \"$MODPROBE_DIR/lpe2026-dirtyfrag.conf\"\n  )\n  if [[ \"$DRY_RUN\" == 1 ]]; then\n    for f in \"${blacklist_files[@]}\"; do\n      [[ -f \"$f\" ]] &amp;&amp; dry \"Would remove $f\"\n    done\n  else\n    rm -f \"${blacklist_files[@]}\"\n    log \"Removed modprobe.d blacklist files \u2014 modules can load again on next request.\"\n  fi\n\n  remove_boot_param \"$BOOT_PARAM\"\n\n  local key=\"\"\n  case \"$FAMILY\" in\n    rhel) key=\"user.max_user_namespaces\" ;;\n    debian) key=\"kernel.unprivileged_userns_clone\" ;;\n  esac\n  if [[ -n \"$key\" &amp;&amp; -f \"$SYSCTL_CONF\" ]]; then\n    if [[ \"$DRY_RUN\" == 1 ]]; then\n      dry \"Would remove $SYSCTL_CONF and restore prior $key from $STATE_DIR/sysctl.$key (if a prior value was saved).\"\n    else\n      rm -f \"$SYSCTL_CONF\"\n      restore_prior_sysctl \"$key\"\n    fi\n  fi\n\n  if [[ \"$DRY_RUN\" == 1 ]]; then\n    dry \"esp4/esp6/rxrpc/algif_aead/af_alg/act_pedit would remain unloaded until next use or reboot; revert only allows future loads again.\"\n  else\n    warn \"esp4/esp6/rxrpc/algif_aead/af_alg/act_pedit stay unloaded until next use or reboot; this only allows future loads again.\"\n    echo \"Revert complete. A reboot is recommended to fully restore prior module state.\"\n  fi\n}\n\nreport_status() {\n  echo \"== $(uname -r) \u2014 detected family: $FAMILY ==\"\n  echo\n  echo \"CVE-2026-31431 Copy Fail:\"\n  check_module_status algif_aead\n  check_module_status af_alg\n  echo\n  echo \"CVE-2026-46331 pedit COW:\"\n  check_module_status act_pedit\n  echo\n  echo \"CVE-2026-43284/43500/43503 Dirty Frag / DirtyClone:\"\n  check_module_status esp4\n  check_module_status esp6\n  check_module_status rxrpc\n  echo\n  echo \"Unprivileged user namespaces:\"\n  case \"$FAMILY\" in\n    rhel) sysctl user.max_user_namespaces 2&gt;/dev/null | sed 's/^/  /' ;;\n    debian) sysctl kernel.unprivileged_userns_clone 2&gt;/dev/null | sed 's/^/  /' ;;\n  esac\n}\n\nprint_summary() {\n  echo\n  echo \"== Summary ==\"\n  echo \"These are compensating controls, not a fix. Install the vendor kernel update and reboot when possible:\"\n  case \"$FAMILY\" in\n    rhel) echo \"  dnf update kernel   (or: yum update kernel)\" ;;\n    debian) echo \"  apt update &amp;&amp; apt install --only-upgrade linux-image-generic\" ;;\n  esac\n  echo \"Log: $LOG_FILE\"\n}\n\nparse_args() {\n  while [[ $# -gt 0 ]]; do\n    case \"$1\" in\n      --apply) APPLY=1 ;;\n      --include-dirtyfrag) INCLUDE_DIRTYFRAG=1 ;;\n      --disable-userns) DISABLE_USERNS_FLAG=1 ;;\n      --yes) ASSUME_YES=1 ;;\n      --revert) DO_REVERT=1 ;;\n      --dry-run) DRY_RUN_FLAG=1 ;;\n      --status) APPLY=0 ;;\n      -h|--help) usage; exit 0 ;;\n      *) echo \"Unknown option: $1\" &gt;&amp;2; usage; exit 1 ;;\n    esac\n    shift\n  done\n  if [[ ( \"$INCLUDE_DIRTYFRAG\" == 1 || \"$DISABLE_USERNS_FLAG\" == 1 ) &amp;&amp; \"$APPLY\" != 1 &amp;&amp; \"$DRY_RUN_FLAG\" != 1 ]]; then\n    echo \"--include-dirtyfrag/--disable-userns require --apply or --dry-run\" &gt;&amp;2\n    exit 1\n  fi\n}\n\nmain() {\n  parse_args \"$@\"\n  require_root\n  FAMILY=\"$(detect_family)\"\n  if [[ \"$FAMILY\" == unknown ]]; then\n    echo \"Unsupported/undetected distro (expected RHEL, AlmaLinux, CentOS, or Ubuntu). Aborting.\" &gt;&amp;2\n    exit 1\n  fi\n\n  if [[ \"$DRY_RUN_FLAG\" == 1 ]]; then\n    DRY_RUN=1\n  elif [[ \"$APPLY\" != 1 &amp;&amp; \"$DO_REVERT\" != 1 ]]; then\n    DRY_RUN=1\n  fi\n  [[ \"$DRY_RUN\" == 1 ]] || mkdir -p \"$STATE_DIR\"\n\n  if [[ \"$DRY_RUN\" == 1 ]]; then\n    echo \"== DRY RUN \u2014 previewing actions, no changes will be made ==\"\n    echo\n  fi\n\n  if [[ \"$DO_REVERT\" == 1 ]]; then\n    revert_all\n    if [[ \"$DRY_RUN\" == 1 ]]; then\n      echo\n      echo \"Dry run only \u2014 no changes made. Re-run without --dry-run to actually revert.\"\n    fi\n    exit 0\n  fi\n\n  report_status\n  echo\n\n  mitigate_copyfail\n  echo\n  mitigate_pedit\n\n  if [[ \"$INCLUDE_DIRTYFRAG\" == 1 ]]; then\n    echo\n    if [[ \"$DRY_RUN\" == 1 ]]; then\n      dry \"Would ask to confirm: 'This disables the IPsec (ESP) kernel dataplane and AF_RXRPC (breaks AFS mounts). Continue?' \u2014 previewing as if confirmed:\"\n      mitigate_dirtyfrag\n    elif confirm \"This disables the IPsec (ESP) kernel dataplane and AF_RXRPC (breaks AFS mounts). Continue?\"; then\n      mitigate_dirtyfrag\n    else\n      warn \"Skipped Dirty Frag / DirtyClone mitigation \u2014 CVE-2026-43284/43500/43503 remain unmitigated.\"\n    fi\n  fi\n\n  if [[ \"$DISABLE_USERNS_FLAG\" == 1 ]]; then\n    echo\n    if [[ \"$DRY_RUN\" == 1 ]]; then\n      dry \"Would ask to confirm: 'This disables unprivileged user namespaces (breaks rootless containers/sandboxes). Continue?' \u2014 previewing as if confirmed:\"\n      disable_userns\n    elif confirm \"This disables unprivileged user namespaces (breaks rootless containers/sandboxes). Continue?\"; then\n      disable_userns\n    else\n      warn \"Skipped userns lockdown.\"\n    fi\n  fi\n\n  print_summary\n\n  if [[ \"$DRY_RUN\" == 1 ]]; then\n    echo\n    if [[ \"$APPLY\" == 1 ]]; then\n      echo \"Dry run only \u2014 no changes made. Re-run without --dry-run to apply the changes shown above.\"\n    else\n      echo \"Dry run only \u2014 no changes made. Re-run with --apply to mitigate (add --yes to skip prompts).\"\n    fi\n  fi\n}\n\nmain \"$@\"\n", "creation_timestamp": "2026-09-04T10:19:03.285809Z"}