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!

--
Cheers,

David / dhildenb


Reply via email to