| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * The test creates shmem PMD huge pages, fills all pages with known patterns, | |
| * then continuously verifies non-punched pages with 16 threads. Meanwhile, the | |
| * main thread punches holes via MADV_REMOVE on the shmem. | |
| * | |
| * It tests the race condition between folio_split() and filemap_get_entry(), | |
| * where the hole punches on shmem lead to folio_split() and reading the shmem | |
| * lead to filemap_get_entry(). | |
| */ | |
| #define _GNU_SOURCE | |
| #include <errno.h> | |
| #include <inttypes.h> | |
| #include <linux/mman.h> | |
| #include <pthread.h> | |
| #include <stdatomic.h> | |
| #include <stdbool.h> | |
| #include <stdint.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <sys/mman.h> | |
| #include <signal.h> | |
| #include <unistd.h> | |
| #include "vm_util.h" | |
| #include "kselftest.h" | |
| #include "hugepage_settings.h" | |
| uint64_t page_size; | |
| uint64_t pmd_pagesize; | |
| #define NR_PMD_PAGE 5 | |
| #define FILE_SIZE (pmd_pagesize * NR_PMD_PAGE) | |
| #define TOTAL_PAGES (FILE_SIZE / page_size) | |
| /* Every N-th to N+M-th pages are punched; not aligned with huge page boundaries. */ | |
| #define PUNCH_INTERVAL 50 /* N */ | |
| #define PUNCH_SIZE_FACTOR 3 /* M */ | |
| #define NUM_READER_THREADS 16 | |
| #define FILL_BYTE 0xAF | |
| #define NUM_ITERATIONS 100 | |
| /* Shared control block: control reading threads and record stats */ | |
| struct shared_ctl { | |
| atomic_uint_fast32_t stop; | |
| atomic_uint_fast64_t reader_failures; | |
| atomic_uint_fast64_t reader_verified; | |
| pthread_barrier_t barrier; | |
| }; | |
| static void fill_page(unsigned char *base, size_t page_idx) | |
| { | |
| unsigned char *page_ptr = base + page_idx * page_size; | |
| uint64_t idx = (uint64_t)page_idx; | |
| memset(page_ptr, FILL_BYTE, page_size); | |
| memcpy(page_ptr, &idx, sizeof(idx)); | |
| } | |
| /* Returns true if valid, false if corrupted. */ | |
| static bool check_page(unsigned char *base, uint64_t page_idx) | |
| { | |
| unsigned char *page_ptr = base + page_idx * page_size; | |
| uint64_t expected_idx = (uint64_t)page_idx; | |
| uint64_t got_idx; | |
| memcpy(&got_idx, page_ptr, 8); | |
| if (got_idx != expected_idx) { | |
| uint64_t off; | |
| int all_zero = 1; | |
| for (off = 0; off < page_size; off++) { | |
| if (page_ptr[off] != 0) { | |
| all_zero = 0; | |
| break; | |
| } | |
| } | |
| if (all_zero) { | |
| ksft_print_msg("CORRUPTED: page %" PRIu64 | |
| " (huge page %" PRIu64 | |
| ") is ALL ZEROS\n", | |
| page_idx, | |
| (page_idx * page_size) / pmd_pagesize); | |
| } else { | |
| ksft_print_msg("CORRUPTED: page %" PRIu64 | |
| " (huge page %" PRIu64 | |
| "): expected idx %" PRIu64 | |
| ", got %" PRIu64 "\n", | |
| page_idx, | |
| (page_idx * page_size) / pmd_pagesize, | |
| page_idx, got_idx); | |
| } | |
| return false; | |
| } | |
| return true; | |
| } | |
| struct reader_arg { | |
| unsigned char *base; | |
| struct shared_ctl *ctl; | |
| int tid; | |
| atomic_uint_fast64_t *failures; | |
| atomic_uint_fast64_t *verified; | |
| }; | |
| static void *reader_thread(void *arg) | |
| { | |
| struct reader_arg *ra = (struct reader_arg *)arg; | |
| unsigned char *base = ra->base; | |
| struct shared_ctl *ctl = ra->ctl; | |
| int tid = ra->tid; | |
| atomic_uint_fast64_t *failures = ra->failures; | |
| atomic_uint_fast64_t *verified = ra->verified; | |
| uint64_t page_idx; | |
| pthread_barrier_wait(&ctl->barrier); | |
| while (atomic_load_explicit(&ctl->stop, memory_order_acquire) == 0) { | |
| for (page_idx = (size_t)tid; page_idx < TOTAL_PAGES; | |
| page_idx += NUM_READER_THREADS) { | |
| /* | |
| * page_idx % PUNCH_INTERVAL is in [0, PUNCH_INTERVAL), | |
| * skip [0, PUNCH_SIZE_FACTOR) | |
| */ | |
| if (page_idx % PUNCH_INTERVAL < PUNCH_SIZE_FACTOR) | |
| continue; | |
| if (check_page(base, page_idx)) | |
| atomic_fetch_add_explicit(verified, 1, | |
| memory_order_relaxed); | |
| else | |
| atomic_fetch_add_explicit(failures, 1, | |
| memory_order_relaxed); | |
| } | |
| if (atomic_load_explicit(failures, memory_order_relaxed) > 0) | |
| break; | |
| } | |
| return NULL; | |
| } | |
| static void create_readers(pthread_t *threads, struct reader_arg *args, | |
| unsigned char *base, struct shared_ctl *ctl) | |
| { | |
| int i; | |
| for (i = 0; i < NUM_READER_THREADS; i++) { | |
| args[i].base = base; | |
| args[i].ctl = ctl; | |
| args[i].tid = i; | |
| args[i].failures = &ctl->reader_failures; | |
| args[i].verified = &ctl->reader_verified; | |
| if (pthread_create(&threads[i], NULL, reader_thread, | |
| &args[i]) != 0) | |
| ksft_exit_fail_msg("pthread_create failed\n"); | |
| } | |
| } | |
| /* Run a single iteration. Returns total number of corrupted pages. */ | |
| static uint64_t run_iteration(void) | |
| { | |
| uint64_t reader_failures, reader_verified; | |
| struct reader_arg args[NUM_READER_THREADS]; | |
| pthread_t threads[NUM_READER_THREADS]; | |
| unsigned char *mmap_base; | |
| struct shared_ctl ctl; | |
| uint64_t i; | |
| memset(&ctl, 0, sizeof(struct shared_ctl)); | |
| mmap_base = mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, | |
| MAP_SHARED | MAP_ANONYMOUS, -1, 0); | |
| if (mmap_base == MAP_FAILED) | |
| ksft_exit_fail_msg("mmap failed: %d\n", errno); | |
| if (madvise(mmap_base, FILE_SIZE, MADV_HUGEPAGE) != 0) | |
| ksft_exit_fail_msg("madvise(MADV_HUGEPAGE) failed: %d\n", | |
| errno); | |
| for (i = 0; i < TOTAL_PAGES; i++) | |
| fill_page(mmap_base, i); | |
| if (!check_huge_shmem(mmap_base, FILE_SIZE, NR_PMD_PAGE, pmd_pagesize)) | |
| ksft_exit_fail_msg("No shmem THP is allocated\n"); | |
| if (pthread_barrier_init(&ctl.barrier, NULL, NUM_READER_THREADS + 1) != 0) | |
| ksft_exit_fail_msg("pthread_barrier_init failed\n"); | |
| create_readers(threads, args, mmap_base, &ctl); | |
| /* Wait for all reader threads to be ready before punching holes. */ | |
| pthread_barrier_wait(&ctl.barrier); | |
| for (i = 0; i < TOTAL_PAGES; i++) { | |
| if (i % PUNCH_INTERVAL != 0) | |
| continue; | |
| if (madvise(mmap_base + i * page_size, | |
| PUNCH_SIZE_FACTOR * page_size, MADV_REMOVE) != 0) { | |
| ksft_exit_fail_msg( | |
| "madvise(MADV_REMOVE) failed on page %" PRIu64 ": %d\n", | |
| i, errno); | |
| } | |
| i += PUNCH_SIZE_FACTOR - 1; | |
| } | |
| atomic_store_explicit(&ctl.stop, 1, memory_order_release); | |
| for (i = 0; i < NUM_READER_THREADS; i++) | |
| pthread_join(threads[i], NULL); | |
| pthread_barrier_destroy(&ctl.barrier); | |
| reader_failures = atomic_load_explicit(&ctl.reader_failures, | |
| memory_order_acquire); | |
| reader_verified = atomic_load_explicit(&ctl.reader_verified, | |
| memory_order_acquire); | |
| if (reader_failures) | |
| ksft_print_msg("Child: %" PRIu64 " pages verified, %" PRIu64 " failures\n", | |
| reader_verified, reader_failures); | |
| munmap(mmap_base, FILE_SIZE); | |
| return reader_failures; | |
| } | |
| int main(void) | |
| { | |
| struct thp_settings current_settings; | |
| uint64_t corrupted_pages; | |
| uint64_t iter; | |
| ksft_print_header(); | |
| page_size = getpagesize(); | |
| pmd_pagesize = read_pmd_pagesize(); | |
| if (!thp_available() || !pmd_pagesize) | |
| ksft_exit_skip("Transparent Hugepages not available\n"); | |
| if (geteuid() != 0) | |
| ksft_exit_skip("Please run the test as root\n"); | |
| thp_save_settings(); | |
| thp_read_settings(¤t_settings); | |
| current_settings.shmem_enabled = SHMEM_ADVISE; | |
| thp_write_settings(¤t_settings); | |
| ksft_set_plan(1); | |
| ksft_print_msg("folio split race test\n"); | |
| for (iter = 0; iter < NUM_ITERATIONS; iter++) { | |
| corrupted_pages = run_iteration(); | |
| if (corrupted_pages > 0) | |
| break; | |
| } | |
| if (iter < NUM_ITERATIONS) | |
| ksft_test_result_fail("FAILED on iteration %" PRIu64 | |
| ": %" PRIu64 | |
| " pages corrupted by MADV_REMOVE!\n", | |
| iter, corrupted_pages); | |
| else | |
| ksft_test_result_pass("All %d iterations passed\n", | |
| NUM_ITERATIONS); | |
| ksft_exit(iter == NUM_ITERATIONS); | |
| return 0; | |
| } |