xref: /linux/tools/testing/selftests/mm/pfnmap.c (revision fde8353121aa304ee88542f011dd5dc83ced47e4)
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