On 09.05.25 11:49, Lorenzo Stoakes wrote:
On Fri, May 09, 2025 at 12:20:41AM +0200, David Hildenbrand wrote:
Let's test some basic functionality using /dev/mem. These tests will implicitly cover some PAT (Page Attribute Handling) handling on x86.
Ah this is really nice thanks for this!
Thanks for your review!
ok 14 mprotect(PROT_NONE) ok 15 SIGSEGV expected ok 16 mprotect(PROT_READ) ok 17 SIGSEGV not expected ok 18 fork() ok 19 SIGSEGV in child not expected # Totals: pass:19 fail:0 xfail:0 xpass:0 skip:0 error:0
It'd be good to assert that merging doesn't work for VM_PFNMAP, though hm one could argue that's not hugely useful as it's trivially implemented.
But I guess anything like that should live in merge.c.
I assume we'd need is_range_mapped() from mremap_tests.c.
Something for another day :)
[...]
+static void signal_handler(int sig) +{
- if (sig == SIGSEGV)
siglongjmp(env, 1);
- siglongjmp(env, 2);
+}
Hm, wouldn't it be better to only catch these only if you specifically meant to catch a signal?
I had that, but got tired about the repeated register + unregister, after all I really don't want to spend a lot more time on this.
You can see what I did in guard-regions.c for an example (sorry, I'm sure you know exactly how the thing works, just I mean for an easy reminder :P)
Again, time is the limit. But let me see if I can get something done in a reasonable timeframe.
+static void sense_support(void) +{
See below comment about the kselftest_harness, but with that you can literally declare fixture setups/teardowns very nicely :) You can also mmap() these 2 pages and munmap() them afterwards straightforwardly.
- char *addr, tmp;
- int ret;
- dev_mem_fd = open("/dev/mem", O_RDONLY);
- if (dev_mem_fd < 0)
ksft_exit_skip("Cannot open '/dev/mem': %s\n", strerror(errno));
Hm skip, or failure? Skip implies it's expected right? I suppose it's possible a system might be setup without this...
Try as non-root or on a lockdowned system :)
- /* We'll require the first two pages throughout our tests ... */
- addr = mmap(0, pagesize * 2, PROT_READ, MAP_SHARED, dev_mem_fd, 0);
- if (addr == MAP_FAILED)
ksft_exit_skip("Cannot mmap '/dev/mem'");
- /* ... and want to be able to read from them. */
- ret = sigsetjmp(env, 1);
- if (!ret) {
tmp = *addr + *(addr + pagesize);
asm volatile("" : "+r" (tmp));
Is this not pretty much equivalent to a volatile read where you're forcing the compiler to not optimise this unused thing away? In guard-regions I set:
#define FORCE_READ(x) (*(volatile typeof(x) *)x)
For this purpose, which would make this:
FORCE_READ(addr); FORCE_READ(&addr[pagesize]);
Hmmm, a compiler might be allowed to optimize out a volatile read.
- }
- if (ret)
ksft_exit_skip("Cannot read-access mmap'ed '/dev/mem'");
Why are we returning 1 or 2 if we don't differentiate it here?
Copy-and-paste. As we are not registering for SIGBUS, we can just return 1.
- munmap(addr, pagesize * 2);
+}
+static void test_madvise(void) +{ +#define INIT_ADVICE(nr) { nr, #nr}
- const struct {
int nr;
const char *name;
- } advices[] = {
INIT_ADVICE(MADV_DONTNEED),
INIT_ADVICE(MADV_DONTNEED_LOCKED),
INIT_ADVICE(MADV_FREE),
INIT_ADVICE(MADV_WIPEONFORK),
INIT_ADVICE(MADV_COLD),
INIT_ADVICE(MADV_PAGEOUT),
INIT_ADVICE(MADV_POPULATE_READ),
INIT_ADVICE(MADV_POPULATE_WRITE),
- };
- char *addr;
- int ret, i;
- addr = mmap(0, pagesize, PROT_READ, MAP_SHARED, dev_mem_fd, 0);
Nit (same for all mmap() calls) shouldn't this first parameter be NULL, by convention? I mean not a big deal obviously :)
Yes.
- if (addr == MAP_FAILED)
ksft_exit_fail_msg("mmap() failed: %s\n", strerror(errno));
- /* All these advices must be rejected. */
- for (i = 0; i < ARRAY_SIZE(advices); i++) {
ret = madvise(addr, pagesize, advices[i].nr);
ksft_test_result(ret && errno == EINVAL,
"madvise(%s) should be disallowed\n",
advices[i].name);
- }
- munmap(addr, pagesize);
+}
+static void test_munmap_splitting(void) +{
- char *addr1, *addr2;
- int ret;
- addr1 = mmap(0, pagesize * 2, PROT_READ, MAP_SHARED, dev_mem_fd, 0);
- if (addr1 == MAP_FAILED)
ksft_exit_fail_msg("mmap() failed: %s\n", strerror(errno));
- /* Unmap the first pages. */
NIT: pages -> page.
Ack.
- ret = munmap(addr1, pagesize);
- ksft_test_result(!ret, "munmap() splitting\n");
- /* Remap the first page while the second page is still mapped. */
- addr2 = mmap(0, pagesize, PROT_READ, MAP_SHARED, dev_mem_fd, 0);
- ksft_test_result(addr2 != MAP_FAILED, "mmap() after splitting\n");
Hm not sure what the assertion is here per se, that we can munmap() partial bits of the VMA? It'd be pretty weird if we couldn't though?
If it's that we don't get a merge when we remap, we're not really
checking
that, but you actually can, as I added an API to vm_util for this using PROCMAP_QUERY (very handy tool actually - binary version of /proc/smaps).
I don't care about merging tests (I'll leave that to you :P ).
This is a PAT test for upcoming changes where partial unmap can leave the original region reserved. Making sure that re-mapping with the pending reservation still works.
- if (addr2 != MAP_FAILED)
munmap(addr2, pagesize);
- if (!ret)
munmap(addr1 + pagesize, pagesize);
- else
munmap(addr1, pagesize * 2);
There's no need for this dance, you can just munmap() away, it tolerates gaps and multiple VMAs.
Yeah, I know. I was not sure if the ksft_test_result() in between might allocate memory and consume that area.
+}
+static void test_mremap_fixed(void) +{
- char *addr, *new_addr, *ret;
- addr = mmap(0, pagesize * 2, PROT_READ, MAP_SHARED, dev_mem_fd, 0);
- if (addr == MAP_FAILED)
ksft_exit_fail_msg("mmap() failed: %s\n", strerror(errno));
- /* Reserve a destination area. */
- new_addr = mmap(0, pagesize * 2, PROT_READ, MAP_ANON | MAP_PRIVATE, -1, 0);
- if (new_addr == MAP_FAILED)
ksft_exit_fail_msg("mmap() failed: %s\n", strerror(errno));
- /* mremap() over our destination. */
- ret = mremap(addr, pagesize * 2, pagesize * 2,
MREMAP_FIXED | MREMAP_MAYMOVE, new_addr);
- ksft_test_result(ret == new_addr, "mremap(MREMAP_FIXED)\n");
- if (ret != new_addr)
munmap(new_addr, pagesize * 2);
This could only be an error code, and this will fail right?
MREMAP_FIXED is 'do or die' at the new address, not hinting. If there's anything already mapped there it goes a bye bye.
So again, we could just have a standard munmap(), and this lends itself well to a FIXTURE_SETUP()/FIXTURE_TEARDOWN() :P
I'm afraid I cannot spend much more time on these tests :P But let me try for a couple of minutes.
- munmap(addr, pagesize * 2);
+}
+static void test_mremap_shrinking(void) +{
- char *addr, *ret;
- addr = mmap(0, pagesize * 2, PROT_READ, MAP_SHARED, dev_mem_fd, 0);
- if (addr == MAP_FAILED)
ksft_exit_fail_msg("mmap() failed: %s\n", strerror(errno));
- /* Shrinking is expected to work. */
- ret = mremap(addr, pagesize * 2, pagesize, 0);
- ksft_test_result(ret == addr, "mremap() shrinking\n");
- if (ret != addr)
munmap(addr, pagesize * 2);
- else
munmap(addr, pagesize);
I think we're safe to just munmap() as usual here :) (it's nitty but I'm trying to make the case for teardown again of course :P)
Same reasoning as above regarding ksft_test_result().
+}
+static void test_mremap_growing(void) +{
- char *addr, *ret;
- addr = mmap(0, pagesize, PROT_READ, MAP_SHARED, dev_mem_fd, 0);
- if (addr == MAP_FAILED)
ksft_exit_fail_msg("mmap() failed: %s\n", strerror(errno));
- /* Growing is not expected to work. */
God imagine if we did allow it... what hell would it be to figure out how to do this correctly in all cases :P
:)
- ret = mremap(addr, pagesize, pagesize * 2, MREMAP_MAYMOVE);
- ksft_test_result(ret == MAP_FAILED,
"mremap() growing should be disallowed\n");
- if (ret == MAP_FAILED)
munmap(addr, pagesize);
- else
munmap(ret, pagesize * 2);
This is a bit cautious, for a world where we do lose our minds and allow this? :)
Yeah, went back and forth with this error cleanup shit.
+}
+static void test_mprotect(void) +{
- char *addr, tmp;
- int ret;
- addr = mmap(0, pagesize, PROT_READ, MAP_SHARED, dev_mem_fd, 0);
- if (addr == MAP_FAILED)
ksft_exit_fail_msg("mmap() failed: %s\n", strerror(errno));
- /* With PROT_NONE, read access must result in SIGSEGV. */
- ret = mprotect(addr, pagesize, PROT_NONE);
- ksft_test_result(!ret, "mprotect(PROT_NONE)\n");
- ret = sigsetjmp(env, 1);
- if (!ret) {
tmp = *addr;
asm volatile("" : "+r" (tmp));
- }
This code is duplicated, we definitely want to abstract it.
Probably yes.
- ksft_test_result(ret == 1, "SIGSEGV expected\n");
Hmm, what exactly are we testing here though? I mean PROT_NONE will be a failed access for _any_ kind of memory? Is this really worthwhile? Maybe better to mprotect() as PROT_NONE to start then mprotect() to PROT_READ.
But I'm not sure what that really tests? Is it a PAT-specific thing? It
seems if this is broken then the mapping code is more generally broken beyond just VM_PFNMAP mappings right?
Rationale was to test the !vm_normal_folio() code paths that are not covered by "ordinary" mprotect (except the shared zeropage). But there should indeed only be such a check on the prot_numa code path, so I can just drop this test.
[...]
+int main(int argc, char **argv) +{
- int err;
- ksft_print_header();
- ksft_set_plan(19);
I know it's kind of nitpicky, but I really hate this sort of magic number and so on. You don't actually need any of this, the kselftest_harness.h is _really_ powerful, and makes for much much more readable and standardised test code.
You can look at guard-regions.c in the test code (though there's some complexity there because I use 'variants') or the merge.c test code (simpler) for straight-forward examples.
I won't block this change on this however, I don't want to be a pain and you're adding very important tests here, but it'd be really nice if you did use that :>)
Yeah, let me explore that real quick, thanks!