1 // SPDX-License-Identifier: GPL-2.0-only 2 /* 3 * Basic VM_PFNMAP tests relying on mmap() of input file provided. 4 * Use '/dev/mem' as default. 5 * 6 * Copyright 2025, Red Hat, Inc. 7 * 8 * Author(s): David Hildenbrand <david@redhat.com> 9 */ 10 #define _GNU_SOURCE 11 #include <stdlib.h> 12 #include <string.h> 13 #include <stdint.h> 14 #include <unistd.h> 15 #include <errno.h> 16 #include <stdio.h> 17 #include <ctype.h> 18 #include <fcntl.h> 19 #include <signal.h> 20 #include <setjmp.h> 21 #include <linux/mman.h> 22 #include <sys/mman.h> 23 #include <sys/wait.h> 24 25 #include "kselftest_harness.h" 26 #include "vm_util.h" 27 28 #define DEV_MEM_NPAGES 2 29 30 static sigjmp_buf sigjmp_buf_env; 31 static char *file = "/dev/mem"; 32 static off_t file_offset; 33 static int fd; 34 35 static void signal_handler(int sig) 36 { 37 siglongjmp(sigjmp_buf_env, -EFAULT); 38 } 39 40 static int test_read_access(char *addr, size_t size, size_t pagesize) 41 { 42 int ret; 43 44 if (signal(SIGSEGV, signal_handler) == SIG_ERR) 45 return -EINVAL; 46 47 ret = sigsetjmp(sigjmp_buf_env, 1); 48 if (!ret) 49 force_read_pages(addr, size/pagesize, pagesize); 50 51 if (signal(SIGSEGV, SIG_DFL) == SIG_ERR) 52 return -EINVAL; 53 54 return ret; 55 } 56 57 static int find_ram_target(off_t *offset, 58 unsigned long long pagesize) 59 { 60 unsigned long long start, end; 61 char line[80], *end_ptr; 62 FILE *file; 63 64 /* Search /proc/iomem for the first suitable "System RAM" range. */ 65 file = fopen("/proc/iomem", "r"); 66 if (!file) 67 return -errno; 68 69 while (fgets(line, sizeof(line), file)) { 70 /* Ignore any child nodes. */ 71 if (!isalnum(line[0])) 72 continue; 73 74 if (!strstr(line, "System RAM\n")) 75 continue; 76 77 start = strtoull(line, &end_ptr, 16); 78 /* Skip over the "-" */ 79 end_ptr++; 80 /* Make end "exclusive". */ 81 end = strtoull(end_ptr, NULL, 16) + 1; 82 83 /* Actual addresses are not exported */ 84 if (!start && !end) 85 break; 86 87 /* We need full pages. */ 88 start = (start + pagesize - 1) & ~(pagesize - 1); 89 end &= ~(pagesize - 1); 90 91 if (start != (off_t)start) 92 break; 93 94 /* We need two pages. */ 95 if (end > start + DEV_MEM_NPAGES * pagesize) { 96 fclose(file); 97 *offset = start; 98 return 0; 99 } 100 } 101 return -ENOENT; 102 } 103 104 static void pfnmap_init(void) 105 { 106 size_t pagesize = getpagesize(); 107 size_t size = DEV_MEM_NPAGES * pagesize; 108 void *addr; 109 110 if (strncmp(file, "/dev/mem", strlen("/dev/mem")) == 0) { 111 int err = find_ram_target(&file_offset, pagesize); 112 113 if (err) 114 ksft_exit_skip("Cannot find ram target in '/proc/iomem': %s\n", 115 strerror(-err)); 116 } else { 117 file_offset = 0; 118 } 119 120 fd = open(file, O_RDONLY); 121 if (fd < 0) 122 ksft_exit_skip("Cannot open '%s': %s\n", file, strerror(errno)); 123 124 /* 125 * Make sure we can map the file, and perform some basic checks; skip 126 * the whole suite if anything goes wrong. 127 * A fresh mapping is then created for every test case by 128 * FIXTURE_SETUP(pfnmap). 129 */ 130 addr = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, file_offset); 131 if (addr == MAP_FAILED) 132 ksft_exit_skip("Cannot mmap '%s': %s\n", file, strerror(errno)); 133 134 if (!check_vmflag_pfnmap(addr)) 135 ksft_exit_skip("Invalid file: '%s'. Not pfnmap'ed\n", file); 136 137 if (test_read_access(addr, size, pagesize)) 138 ksft_exit_skip("Cannot read-access mmap'ed '%s'\n", file); 139 140 munmap(addr, size); 141 } 142 143 FIXTURE(pfnmap) 144 { 145 size_t pagesize; 146 char *addr1; 147 size_t size1; 148 char *addr2; 149 size_t size2; 150 }; 151 152 FIXTURE_SETUP(pfnmap) 153 { 154 self->pagesize = getpagesize(); 155 156 self->size1 = DEV_MEM_NPAGES * self->pagesize; 157 self->addr1 = mmap(NULL, self->size1, PROT_READ, MAP_SHARED, 158 fd, file_offset); 159 ASSERT_NE(self->addr1, MAP_FAILED); 160 161 self->size2 = 0; 162 self->addr2 = MAP_FAILED; 163 } 164 165 FIXTURE_TEARDOWN(pfnmap) 166 { 167 if (self->addr2 != MAP_FAILED) 168 munmap(self->addr2, self->size2); 169 if (self->addr1 != MAP_FAILED) 170 munmap(self->addr1, self->size1); 171 } 172 173 TEST_F(pfnmap, madvise_disallowed) 174 { 175 int advices[] = { 176 MADV_DONTNEED, 177 MADV_DONTNEED_LOCKED, 178 MADV_FREE, 179 MADV_WIPEONFORK, 180 MADV_COLD, 181 MADV_PAGEOUT, 182 MADV_POPULATE_READ, 183 MADV_POPULATE_WRITE, 184 }; 185 int i; 186 187 /* All these advices must be rejected. */ 188 for (i = 0; i < ARRAY_SIZE(advices); i++) { 189 EXPECT_LT(madvise(self->addr1, self->pagesize, advices[i]), 0); 190 EXPECT_EQ(errno, EINVAL); 191 } 192 } 193 194 TEST_F(pfnmap, munmap_split) 195 { 196 /* 197 * Unmap the first page. This munmap() call is not really expected to 198 * fail, but we might be able to trigger other internal issues. 199 */ 200 ASSERT_EQ(munmap(self->addr1, self->pagesize), 0); 201 202 /* 203 * Remap the first page while the second page is still mapped. This 204 * makes sure that any PAT tracking on x86 will allow for mmap()'ing 205 * a page again while some parts of the first mmap() are still 206 * around. 207 */ 208 self->size2 = self->pagesize; 209 self->addr2 = mmap(NULL, self->pagesize, PROT_READ, MAP_SHARED, 210 fd, file_offset); 211 ASSERT_NE(self->addr2, MAP_FAILED); 212 } 213 214 TEST_F(pfnmap, mremap_fixed) 215 { 216 char *ret; 217 218 /* Reserve a destination area. */ 219 self->size2 = self->size1; 220 self->addr2 = mmap(NULL, self->size2, PROT_READ, MAP_ANON | MAP_PRIVATE, 221 -1, 0); 222 ASSERT_NE(self->addr2, MAP_FAILED); 223 224 /* mremap() over our destination. */ 225 ret = mremap(self->addr1, self->size1, self->size2, 226 MREMAP_FIXED | MREMAP_MAYMOVE, self->addr2); 227 ASSERT_NE(ret, MAP_FAILED); 228 } 229 230 TEST_F(pfnmap, mremap_shrink) 231 { 232 char *ret; 233 234 /* Shrinking is expected to work. */ 235 ret = mremap(self->addr1, self->size1, self->size1 - self->pagesize, 0); 236 ASSERT_NE(ret, MAP_FAILED); 237 } 238 239 TEST_F(pfnmap, mremap_expand) 240 { 241 /* 242 * Growing is not expected to work, and getting it right would 243 * be challenging. So this test primarily serves as an early warning 244 * that something that probably should never work suddenly works. 245 */ 246 self->size2 = self->size1 + self->pagesize; 247 self->addr2 = mremap(self->addr1, self->size1, self->size2, MREMAP_MAYMOVE); 248 ASSERT_EQ(self->addr2, MAP_FAILED); 249 } 250 251 TEST_F(pfnmap, fork) 252 { 253 pid_t pid; 254 int ret; 255 256 /* fork() a child and test if the child can access the pages. */ 257 pid = fork(); 258 ASSERT_GE(pid, 0); 259 260 if (!pid) { 261 EXPECT_EQ(test_read_access(self->addr1, self->size1, 262 self->pagesize), 0); 263 exit(0); 264 } 265 266 wait(&ret); 267 if (WIFEXITED(ret)) 268 ret = WEXITSTATUS(ret); 269 else 270 ret = -EINVAL; 271 ASSERT_EQ(ret, 0); 272 } 273 274 int main(int argc, char **argv) 275 { 276 for (int i = 1; i < argc; i++) { 277 if (strcmp(argv[i], "--") == 0) { 278 if (i + 1 < argc && strlen(argv[i + 1]) > 0) 279 file = argv[i + 1]; 280 argc = i; 281 break; 282 } 283 } 284 285 pfnmap_init(); 286 287 return test_harness_run(argc, argv); 288 } 289