linux/tools/testing/selftests/mm/hugetlb_madv_vs_map.c

// SPDX-License-Identifier: GPL-2.0
/*
 * A test case that must run on a system with one and only one huge page available.
 *	# echo 1 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
 *
 * During setup, the test allocates the only available page, and starts three threads:
 *  - thread1:
 *	* madvise(MADV_DONTNEED) on the allocated huge page
 *  - thread 2:
 *	* Write to the allocated huge page
 *  - thread 3:
 *	* Try to allocated an extra huge page (which must not available)
 *
 *  The test fails if thread3 is able to allocate a page.
 *
 *  Touching the first page after thread3's allocation will raise a SIGBUS
 *
 *  Author: Breno Leitao <[email protected]>
 */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>

#include "vm_util.h"
#include "../kselftest.h"

#define INLOOP_ITER 100

size_t mmap_size;
char *huge_ptr;

/* Touch the memory while it is being madvised() */
void *touch(void *unused)
{
	for (int i = 0; i < INLOOP_ITER; i++)
		huge_ptr[0] = '.';

	return NULL;
}

void *madv(void *unused)
{
	for (int i = 0; i < INLOOP_ITER; i++)
		madvise(huge_ptr, mmap_size, MADV_DONTNEED);

	return NULL;
}

/*
 * We got here, and there must be no huge page available for mapping
 * The other hugepage should be flipping from used <-> reserved, because
 * of madvise(DONTNEED).
 */
void *map_extra(void *unused)
{
	void *ptr;

	for (int i = 0; i < INLOOP_ITER; i++) {
		ptr = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE,
			   MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
			   -1, 0);

		if ((long)ptr != -1) {
			/* Touching the other page now will cause a SIGBUG
			 * huge_ptr[0] = '1';
			 */
			return ptr;
		}
	}

	return NULL;
}

int main(void)
{
	pthread_t thread1, thread2, thread3;
	unsigned long free_hugepages;
	void *ret;

	/*
	 * On kernel 6.7, we are able to reproduce the problem with ~10
	 * interactions
	 */
	int max = 10;

	free_hugepages = get_free_hugepages();

	if (free_hugepages != 1) {
		ksft_exit_skip("This test needs one and only one page to execute. Got %lu\n",
			       free_hugepages);
	}

	mmap_size = default_huge_page_size();

	while (max--) {
		huge_ptr = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE,
				MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
				-1, 0);

		if ((unsigned long)huge_ptr == -1) {
			ksft_test_result_fail("Failed to allocate huge page\n");
			return KSFT_FAIL;
		}

		pthread_create(&thread1, NULL, madv, NULL);
		pthread_create(&thread2, NULL, touch, NULL);
		pthread_create(&thread3, NULL, map_extra, NULL);

		pthread_join(thread1, NULL);
		pthread_join(thread2, NULL);
		pthread_join(thread3, &ret);

		if (ret) {
			ksft_test_result_fail("Unexpected huge page allocation\n");
			return KSFT_FAIL;
		}

		/* Unmap and restart */
		munmap(huge_ptr, mmap_size);
	}

	return KSFT_PASS;
}