root/tools/testing/selftests/mm/soft-dirty.c
// SPDX-License-Identifier: GPL-2.0
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <fcntl.h>
#include <stdint.h>
#include <malloc.h>
#include <sys/mman.h>

#include "kselftest.h"
#include "vm_util.h"
#include "thp_settings.h"

#define PAGEMAP_FILE_PATH "/proc/self/pagemap"
#define TEST_ITERATIONS 10000

static void test_simple(int pagemap_fd, int pagesize)
{
        int i;
        char *map;

        map = aligned_alloc(pagesize, pagesize);
        if (!map)
                ksft_exit_fail_msg("mmap failed\n");

        clear_softdirty();

        for (i = 0 ; i < TEST_ITERATIONS; i++) {
                if (pagemap_is_softdirty(pagemap_fd, map) == 1) {
                        ksft_print_msg("dirty bit was 1, but should be 0 (i=%d)\n", i);
                        break;
                }

                clear_softdirty();
                // Write something to the page to get the dirty bit enabled on the page
                map[0]++;

                if (pagemap_is_softdirty(pagemap_fd, map) == 0) {
                        ksft_print_msg("dirty bit was 0, but should be 1 (i=%d)\n", i);
                        break;
                }

                clear_softdirty();
        }
        free(map);

        ksft_test_result(i == TEST_ITERATIONS, "Test %s\n", __func__);
}

static void test_vma_reuse(int pagemap_fd, int pagesize)
{
        char *map, *map2;

        map = mmap(NULL, pagesize, (PROT_READ | PROT_WRITE), (MAP_PRIVATE | MAP_ANON), -1, 0);
        if (map == MAP_FAILED)
                ksft_exit_fail_msg("mmap failed");

        // The kernel always marks new regions as soft dirty
        ksft_test_result(pagemap_is_softdirty(pagemap_fd, map) == 1,
                         "Test %s dirty bit of allocated page\n", __func__);

        clear_softdirty();
        munmap(map, pagesize);

        map2 = mmap(NULL, pagesize, (PROT_READ | PROT_WRITE), (MAP_PRIVATE | MAP_ANON), -1, 0);
        if (map2 == MAP_FAILED)
                ksft_exit_fail_msg("mmap failed");

        // Dirty bit is set for new regions even if they are reused
        if (map == map2)
                ksft_test_result(pagemap_is_softdirty(pagemap_fd, map2) == 1,
                                 "Test %s dirty bit of reused address page\n", __func__);
        else
                ksft_test_result_skip("Test %s dirty bit of reused address page\n", __func__);

        munmap(map2, pagesize);
}

static void test_hugepage(int pagemap_fd, int pagesize)
{
        char *map;
        int i, ret;

        if (!thp_is_enabled()) {
                ksft_test_result_skip("Transparent Hugepages not available\n");
                return;
        }

        size_t hpage_len = read_pmd_pagesize();
        if (!hpage_len)
                ksft_exit_fail_msg("Reading PMD pagesize failed");

        map = memalign(hpage_len, hpage_len);
        if (!map)
                ksft_exit_fail_msg("memalign failed\n");

        ret = madvise(map, hpage_len, MADV_HUGEPAGE);
        if (ret)
                ksft_exit_fail_msg("madvise failed %d\n", ret);

        for (i = 0; i < hpage_len; i++)
                map[i] = (char)i;

        if (check_huge_anon(map, 1, hpage_len)) {
                ksft_test_result_pass("Test %s huge page allocation\n", __func__);

                clear_softdirty();
                for (i = 0 ; i < TEST_ITERATIONS ; i++) {
                        if (pagemap_is_softdirty(pagemap_fd, map) == 1) {
                                ksft_print_msg("dirty bit was 1, but should be 0 (i=%d)\n", i);
                                break;
                        }

                        clear_softdirty();
                        // Write something to the page to get the dirty bit enabled on the page
                        map[0]++;

                        if (pagemap_is_softdirty(pagemap_fd, map) == 0) {
                                ksft_print_msg("dirty bit was 0, but should be 1 (i=%d)\n", i);
                                break;
                        }
                        clear_softdirty();
                }

                ksft_test_result(i == TEST_ITERATIONS, "Test %s huge page dirty bit\n", __func__);
        } else {
                // hugepage allocation failed. skip these tests
                ksft_test_result_skip("Test %s huge page allocation\n", __func__);
                ksft_test_result_skip("Test %s huge page dirty bit\n", __func__);
        }
        free(map);
}

static void test_mprotect(int pagemap_fd, int pagesize, bool anon)
{
        const char *type[] = {"file", "anon"};
        const char *fname = "./soft-dirty-test-file";
        int test_fd = 0;
        char *map;

        if (anon) {
                map = mmap(NULL, pagesize, PROT_READ|PROT_WRITE,
                           MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);
                if (!map)
                        ksft_exit_fail_msg("anon mmap failed\n");
        } else {
                test_fd = open(fname, O_RDWR | O_CREAT, 0664);
                if (test_fd < 0) {
                        ksft_test_result_skip("Test %s open() file failed\n", __func__);
                        return;
                }
                unlink(fname);
                ftruncate(test_fd, pagesize);
                map = mmap(NULL, pagesize, PROT_READ|PROT_WRITE,
                           MAP_SHARED, test_fd, 0);
                if (!map)
                        ksft_exit_fail_msg("file mmap failed\n");
        }

        *map = 1;
        ksft_test_result(pagemap_is_softdirty(pagemap_fd, map) == 1,
                         "Test %s-%s dirty bit of new written page\n",
                         __func__, type[anon]);
        clear_softdirty();
        ksft_test_result(pagemap_is_softdirty(pagemap_fd, map) == 0,
                         "Test %s-%s soft-dirty clear after clear_refs\n",
                         __func__, type[anon]);
        mprotect(map, pagesize, PROT_READ);
        ksft_test_result(pagemap_is_softdirty(pagemap_fd, map) == 0,
                         "Test %s-%s soft-dirty clear after marking RO\n",
                         __func__, type[anon]);
        mprotect(map, pagesize, PROT_READ|PROT_WRITE);
        ksft_test_result(pagemap_is_softdirty(pagemap_fd, map) == 0,
                         "Test %s-%s soft-dirty clear after marking RW\n",
                         __func__, type[anon]);
        *map = 2;
        ksft_test_result(pagemap_is_softdirty(pagemap_fd, map) == 1,
                         "Test %s-%s soft-dirty after rewritten\n",
                         __func__, type[anon]);

        munmap(map, pagesize);

        if (!anon)
                close(test_fd);
}

static void test_merge(int pagemap_fd, int pagesize)
{
        char *reserved, *map, *map2;

        /*
         * Reserve space for tests:
         *
         *   ---padding to ---
         *   |   avoid adj.  |
         *   v     merge     v
         * |---|---|---|---|---|
         * |   | 1 | 2 | 3 |   |
         * |---|---|---|---|---|
         */
        reserved = mmap(NULL, 5 * pagesize, PROT_NONE,
                        MAP_ANON | MAP_PRIVATE, -1, 0);
        if (reserved == MAP_FAILED)
                ksft_exit_fail_msg("mmap failed\n");
        munmap(reserved, 4 * pagesize);

        /*
         * Establish initial VMA:
         *
         *      S/D
         * |---|---|---|---|---|
         * |   | 1 |   |   |   |
         * |---|---|---|---|---|
         */
        map = mmap(&reserved[pagesize], pagesize, PROT_READ | PROT_WRITE,
                   MAP_ANON | MAP_PRIVATE | MAP_FIXED, -1, 0);
        if (map == MAP_FAILED)
                ksft_exit_fail_msg("mmap failed\n");

        /* This will clear VM_SOFTDIRTY too. */
        clear_softdirty();

        /*
         * Now place a new mapping which will be marked VM_SOFTDIRTY. Away from
         * map:
         *
         *       -      S/D
         * |---|---|---|---|---|
         * |   | 1 |   | 2 |   |
         * |---|---|---|---|---|
         */
        map2 = mmap(&reserved[3 * pagesize], pagesize, PROT_READ | PROT_WRITE,
                    MAP_ANON | MAP_PRIVATE | MAP_FIXED, -1, 0);
        if (map2 == MAP_FAILED)
                ksft_exit_fail_msg("mmap failed\n");

        /*
         * Now remap it immediately adjacent to map, if the merge correctly
         * propagates VM_SOFTDIRTY, we should then observe the VMA as a whole
         * being marked soft-dirty:
         *
         *       merge
         *        S/D
         * |---|-------|---|---|
         * |   |   1   |   |   |
         * |---|-------|---|---|
         */
        map2 = mremap(map2, pagesize, pagesize, MREMAP_FIXED | MREMAP_MAYMOVE,
                      &reserved[2 * pagesize]);
        if (map2 == MAP_FAILED)
                ksft_exit_fail_msg("mremap failed\n");
        ksft_test_result(pagemap_is_softdirty(pagemap_fd, map) == 1,
                         "Test %s-anon soft-dirty after remap merge 1st pg\n",
                         __func__);
        ksft_test_result(pagemap_is_softdirty(pagemap_fd, map2) == 1,
                         "Test %s-anon soft-dirty after remap merge 2nd pg\n",
                         __func__);

        munmap(map, 2 * pagesize);

        /*
         * Now establish another VMA:
         *
         *      S/D
         * |---|---|---|---|---|
         * |   | 1 |   |   |   |
         * |---|---|---|---|---|
         */
        map = mmap(&reserved[pagesize], pagesize, PROT_READ | PROT_WRITE,
                   MAP_ANON | MAP_PRIVATE | MAP_FIXED, -1, 0);
        if (map == MAP_FAILED)
                ksft_exit_fail_msg("mmap failed\n");

        /* Clear VM_SOFTDIRTY... */
        clear_softdirty();
        /* ...and establish incompatible adjacent VMA:
         *
         *       -  S/D
         * |---|---|---|---|---|
         * |   | 1 | 2 |   |   |
         * |---|---|---|---|---|
         */
        map2 = mmap(&reserved[2 * pagesize], pagesize,
        PROT_READ | PROT_WRITE | PROT_EXEC,
                   MAP_ANON | MAP_PRIVATE | MAP_FIXED, -1, 0);
        if (map2 == MAP_FAILED)
                ksft_exit_fail_msg("mmap failed\n");

        /*
         * Now mprotect() VMA 1 so it's compatible with 2 and therefore merges:
         *
         *       merge
         *        S/D
         * |---|-------|---|---|
         * |   |   1   |   |   |
         * |---|-------|---|---|
         */
        if (mprotect(map, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC))
                ksft_exit_fail_msg("mprotect failed\n");

        ksft_test_result(pagemap_is_softdirty(pagemap_fd, map) == 1,
                         "Test %s-anon soft-dirty after mprotect merge 1st pg\n",
                         __func__);
        ksft_test_result(pagemap_is_softdirty(pagemap_fd, map2) == 1,
                         "Test %s-anon soft-dirty after mprotect merge 2nd pg\n",
                         __func__);

        munmap(map, 2 * pagesize);
}

static void test_mprotect_anon(int pagemap_fd, int pagesize)
{
        test_mprotect(pagemap_fd, pagesize, true);
}

static void test_mprotect_file(int pagemap_fd, int pagesize)
{
        test_mprotect(pagemap_fd, pagesize, false);
}

int main(int argc, char **argv)
{
        int pagemap_fd;
        int pagesize;

        ksft_print_header();

        if (!softdirty_supported())
                ksft_exit_skip("soft-dirty is not support\n");

        ksft_set_plan(19);
        pagemap_fd = open(PAGEMAP_FILE_PATH, O_RDONLY);
        if (pagemap_fd < 0)
                ksft_exit_fail_msg("Failed to open %s\n", PAGEMAP_FILE_PATH);

        pagesize = getpagesize();

        test_simple(pagemap_fd, pagesize);
        test_vma_reuse(pagemap_fd, pagesize);
        test_hugepage(pagemap_fd, pagesize);
        test_mprotect_anon(pagemap_fd, pagesize);
        test_mprotect_file(pagemap_fd, pagesize);
        test_merge(pagemap_fd, pagesize);

        close(pagemap_fd);

        ksft_finished();
}