* [PATCH 0/4] xen/mm: Host-side unit/integration test framework for page_alloc.c
@ 2026-04-15 17:34 Bernhard Kaindl
2026-04-15 17:34 ` [PATCH 1/4] tools/tests/alloc: Unit and Integration Test Framework " Bernhard Kaindl
` (3 more replies)
0 siblings, 4 replies; 7+ messages in thread
From: Bernhard Kaindl @ 2026-04-15 17:34 UTC (permalink / raw)
To: xen-devel
Cc: Bernhard Kaindl, Anthony PERARD, Andrew Cooper, Michal Orzel,
Jan Beulich, Julien Grall, Roger Pau Monné,
Stefano Stabellini
Hi all,
This patch series adds a host-side unit and integration test framework
for the Xen page allocator in xen/common/page_alloc.c.
Using this framework, it also adds a host-side integration test suite
for memory claims, including NUMA-aware claim sets.
This test suite complements the functional system tests submitted as
part of the NUMA-aware claims v6 series yesterday.
The purpose is to verify the behaviour of the page allocator when
multi-node claim sets are present in situations that are easier to
create and validate in isolation, with full control over a synthetic
Xen heap state and visibility into the claim state of domains as
claims are made and redeemed through heap allocation.
This series depends on the NUMA claims v6 series, which introduced
NUMA-aware claim sets which I submitted yesterday:
[PATCH v6 0/7] xen/mm: Introduce NUMA-aware claim sets for domains
https://lists.xen.org/archives/html/xen-devel/2026-04/msg00587.html
https://patchwork.kernel.org/project/xen-devel/list/?series=1081139
Its design is described in this design document submitted yesterday:
[PATCH v2] docs: Draft Design Document for NUMA-aware claim sets
https://lists.xen.org/archives/html/xen-devel/2026-04/msg00569.html
https://patchwork.kernel.org/project/xen-devel/list/?series=1081047
Patch summary:
1. tools/tests/alloc: Add test framework for xen/common/page_alloc.c
2. tools/tests/alloc: Add integration tests for claims and claim sets
3. tools/tests/alloc: Add tests for offlining with claims present
4. xen/mm: Fix recalling claims when offlining pages if needed
Thanks,
Bernhard Kaindl
---
PS: The bug fixed in the last commit of this series affects global
outstanding claims as implemented in current Xen master as well.
However, this only arises when Xen offlines pages. Page offlining
code in current Xen still has known limitations, particularly when
offlining pages from larger buddies, and should be avoided in the
interest of heap integrity.
That said, this test framework makes it possible to run targeted
test cases on a synthetic Xen heap and observe logged changes
to the heap during page offlining.
That should make further offlining fixes easier to validate, both by
checking the test results and by inspecting the resulting heap state.
If you are interested in that, the next series will add a test suite
for offlining pages using this test framework. That should make it
possible to observe the effects of the bugs and the corresponding
fixes on any machine able to compile Xen source code.
Bernhard Kaindl (4):
tools/tests/alloc: Unit and Integration Test Framework for
page_alloc.c
tools/tests/alloc: Add integration test suite for memory claims
tools/tests/alloc: Add tests for offlining with claims present
xen/mm: Recall claims when offlining pages if needed
tools/tests/Makefile | 1 +
tools/tests/alloc/.gitignore | 6 +
tools/tests/alloc/Makefile | 141 ++++++
tools/tests/alloc/README.rst | 31 ++
tools/tests/alloc/check-asserts.h | 347 +++++++++++++++
tools/tests/alloc/harness.h | 69 +++
tools/tests/alloc/hypervisor-macros.h | 101 +++++
tools/tests/alloc/libtest-page_alloc.h | 356 +++++++++++++++
tools/tests/alloc/mock-page_list.h | 307 +++++++++++++
tools/tests/alloc/page_alloc-wrapper.h | 465 ++++++++++++++++++++
tools/tests/alloc/page_alloc_shim.h | 433 ++++++++++++++++++
tools/tests/alloc/test-claims_basic.c | 230 ++++++++++
tools/tests/alloc/test-claims_numa_redeem.c | 201 +++++++++
tools/tests/alloc/test-offlining-claims.c | 102 +++++
xen/common/page_alloc.c | 42 ++
15 files changed, 2832 insertions(+)
create mode 100644 tools/tests/alloc/.gitignore
create mode 100644 tools/tests/alloc/Makefile
create mode 100644 tools/tests/alloc/README.rst
create mode 100644 tools/tests/alloc/check-asserts.h
create mode 100644 tools/tests/alloc/harness.h
create mode 100644 tools/tests/alloc/hypervisor-macros.h
create mode 100644 tools/tests/alloc/libtest-page_alloc.h
create mode 100644 tools/tests/alloc/mock-page_list.h
create mode 100644 tools/tests/alloc/page_alloc-wrapper.h
create mode 100644 tools/tests/alloc/page_alloc_shim.h
create mode 100644 tools/tests/alloc/test-claims_basic.c
create mode 100644 tools/tests/alloc/test-claims_numa_redeem.c
create mode 100644 tools/tests/alloc/test-offlining-claims.c
--
2.39.5
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 1/4] tools/tests/alloc: Unit and Integration Test Framework for page_alloc.c
2026-04-15 17:34 [PATCH 0/4] xen/mm: Host-side unit/integration test framework for page_alloc.c Bernhard Kaindl
@ 2026-04-15 17:34 ` Bernhard Kaindl
2026-04-16 7:39 ` Jan Beulich
2026-04-15 17:34 ` [PATCH 2/4] tools/tests/alloc: Add integration test suite for memory claims Bernhard Kaindl
` (2 subsequent siblings)
3 siblings, 1 reply; 7+ messages in thread
From: Bernhard Kaindl @ 2026-04-15 17:34 UTC (permalink / raw)
To: xen-devel; +Cc: Bernhard Kaindl, Anthony PERARD
Add a test framefork for unit and integration test suites testing
the Xen page allocator module xen/common/page_alloc.c in isolation.
It enables test suites to verify the behaviour of the page allocator
in situations that are easier to create and validate in isolation,
with full control over a synthetic Xen heap state and visibility
into the allocator and domain state.
Signed-off-by: Bernhard Kaindl <bernhard.kaindl@citrix.com>
---
tools/tests/Makefile | 1 +
tools/tests/alloc/.gitignore | 6 +
tools/tests/alloc/Makefile | 141 ++++++++
tools/tests/alloc/README.rst | 31 ++
tools/tests/alloc/check-asserts.h | 347 ++++++++++++++++++
tools/tests/alloc/harness.h | 69 ++++
tools/tests/alloc/hypervisor-macros.h | 101 ++++++
tools/tests/alloc/libtest-page_alloc.h | 356 +++++++++++++++++++
tools/tests/alloc/mock-page_list.h | 307 ++++++++++++++++
tools/tests/alloc/page_alloc-wrapper.h | 465 +++++++++++++++++++++++++
tools/tests/alloc/page_alloc_shim.h | 433 +++++++++++++++++++++++
11 files changed, 2257 insertions(+)
create mode 100644 tools/tests/alloc/.gitignore
create mode 100644 tools/tests/alloc/Makefile
create mode 100644 tools/tests/alloc/README.rst
create mode 100644 tools/tests/alloc/check-asserts.h
create mode 100644 tools/tests/alloc/harness.h
create mode 100644 tools/tests/alloc/hypervisor-macros.h
create mode 100644 tools/tests/alloc/libtest-page_alloc.h
create mode 100644 tools/tests/alloc/mock-page_list.h
create mode 100644 tools/tests/alloc/page_alloc-wrapper.h
create mode 100644 tools/tests/alloc/page_alloc_shim.h
diff --git a/tools/tests/Makefile b/tools/tests/Makefile
index 6477a4386dda..ca3de4c7b54a 100644
--- a/tools/tests/Makefile
+++ b/tools/tests/Makefile
@@ -2,6 +2,7 @@ XEN_ROOT = $(CURDIR)/../..
include $(XEN_ROOT)/tools/Rules.mk
SUBDIRS-y :=
+SUBDIRS-y += alloc
SUBDIRS-y += domid
SUBDIRS-y += mem-claim
SUBDIRS-y += paging-mempool
diff --git a/tools/tests/alloc/.gitignore b/tools/tests/alloc/.gitignore
new file mode 100644
index 000000000000..9f77c5879772
--- /dev/null
+++ b/tools/tests/alloc/.gitignore
@@ -0,0 +1,6 @@
+/test-claims_basic
+/test-claims_numa_install
+/test-claims_numa_redeem
+/test-online_page
+/test-offlining-claims
+/test-reserve_offline_page
diff --git a/tools/tests/alloc/Makefile b/tools/tests/alloc/Makefile
new file mode 100644
index 000000000000..f5724aa3f699
--- /dev/null
+++ b/tools/tests/alloc/Makefile
@@ -0,0 +1,141 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Makefile for tools/tests/alloc
+
+XEN_ROOT := $(abspath $(CURDIR)/../../..)
+include $(XEN_ROOT)/tools/Rules.mk
+RELDIR := $(subst $(XEN_ROOT)/,,$(CURDIR))
+
+TEST_SOURCES := $(notdir $(wildcard test-*.c))
+TARGETS := $(TEST_SOURCES:.c=)
+
+.PHONY: all
+all: $(TARGETS)
+
+define RUN_TESTS
+ @echo "Build configuration:"
+ @echo "CC=$(CC)"
+ @echo "CFLAGS='$(CFLAGS)'"
+ @for test in $? ; do \
+ echo;echo "$(RELDIR): RUN_TESTS: $$test...";echo; \
+ ./$$test ; EXIT_CODE=$$? ; \
+ if [ $$EXIT_CODE -ne 0 ]; then \
+ echo "Test $$test failed with exit code $$EXIT_CODE"; \
+ exit 1; \
+ fi; \
+ done
+ @echo
+ @echo "Tests executed successfully:"
+ @for test in $? ; do \
+ echo " - $$test"; \
+ done
+endef
+
+# Run the tests if possible, otherwise print a warning and skip them.
+.PHONY: run
+# Determine if the tests can be run on the build host. If CC and HOSTCC
+# are the same, we can run the tests directly. If they differ, we check
+# if binfmt-support and qemu-binfmt are available to run the tests under
+# using binfmt-misc using qemu-user-static.
+ifeq ($(CC),$(HOSTCC))
+ TESTS_RUNNABLE=yes
+else
+ BINFMT_SUPP := $(if $(wildcard /etc/init.d/binfmt-support),1,0)
+ QEMU_BINFMT := $(if $(wildcard /usr/libexec/qemu-binfmt),1,0)
+ ifeq ($(BINFMT_SUPP)$(QEMU_BINFMT),11)
+ # Running static binaries doesn't need extra setup besides qemu-binfmt
+ CFLAGS += -static
+ TESTS_RUNNABLE=yes
+ else
+ TESTS_RUNNABLE=no
+ endif
+endif
+
+run: $(TARGETS)
+ifeq ($(TESTS_RUNNABLE),yes)
+ $(RUN_TESTS)
+else
+ $(warning HOSTCC != CC, and qemu-binfmt not detected, skipping alloc tests)
+endif
+
+# Run the tests binfmt-misc set up
+BINFMT_SUPP := $(if $(wildcard /etc/init.d/binfmt-support),1,0)
+QEMU_BINFMT := $(if $(wildcard /usr/libexec/qemu-binfmt),1,0)
+.PHONY: run-tests
+run-tests: $(TARGETS)
+ifeq ($(CC),$(HOSTCC))
+ $(RUN_TESTS)
+else ifeq ($(BINFMT_SUPP)$(QEMU_BINFMT),11)
+ $(RUN_TESTS)
+else
+ $(warning Note: binfmt-support or qemu-user not found, skipping run-tests)
+endif
+
+#
+# Build and run the tests for multiple architectures,
+# skipping if the appropriate cross-compiler is not found.
+# The default XEN_TARGET_ARCH is always built and tested as well.
+# This is gcc-specific, but can be adapted for other toolchains.
+#
+ARCHS := arm64-aarch64-linux-gnu arm32-arm-linux-gnueabihf
+ARCHS += x86_32-i686-linux-gnu x86_64-x86_64-linux-gnu
+ARCHS += ppc64-powerpc64le-linux-gnu riscv64-riscv64-linux-gnu
+.PHONY: run-archs
+run-archs: $(TARGETS)
+ifneq ($(CC),gcc)
+ $(warning run-archs target is only supported with CC=gcc for now, skipping)
+else
+ @set -e;PASSES=;SKIPPED_ARCHS=; \
+ MAKEFLAGS="$$MAKEFLAGS --no-print-directory"; \
+
+ for t in $(ARCHS); do \
+ A=$${t%%-*}; C=$${t#*-}; \
+ [ $$A != $(XEN_TARGET_ARCH) ] || continue; \
+ if ! type "$${C}-gcc" >/dev/null 2>&1; then \
+ echo " $${C}-gcc not found, skipping $${A}"; \
+ SKIPPED_ARCHS="$${SKIPPED_ARCHS} $${A}"; continue; \
+ fi; \
+ if [ $${A} = $(XEN_TARGET_ARCH) ]; then C=$(CROSS_COMPILE); fi; \
+ make XEN_TARGET_ARCH="$${A}" CROSS_COMPILE=$$C- clean run-tests; \
+ PASSES="$${PASSES} $${A}"; \
+ done;\
+ echo "$@ successful for:$${PASSES} $(XEN_TARGET_ARCH)";\
+ [ -z "$${SKIPPED_ARCHS}" ] || echo "Skipped architectures:$${SKIPPED_ARCHS}"
+endif
+
+.PHONY: clean
+.NOTPARALLEL: clean
+clean:
+ $(RM) -- *.o $(TARGETS) $(DEPS_RM)
+
+.PHONY: distclean
+distclean: clean
+ $(RM) -- *~
+
+.PHONY: install
+install: all
+ $(INSTALL_DIR) $(DESTDIR)$(LIBEXEC)/tests
+ $(INSTALL_PROG) $(TARGETS) $(DESTDIR)$(LIBEXEC)/tests
+
+.PHONY: uninstall
+uninstall:
+ $(RM) -- $(patsubst %,$(DESTDIR)$(LIBEXEC)/tests/%,$(TARGETS))
+
+# CFLAGS for building the tests
+XEN_INCLUDE_ARCH := $(subst x86_64,x86,$(XEN_COMPILE_ARCH))
+CFLAGS += -D__XEN_TOOLS__
+CFLAGS += $(APPEND_CFLAGS)
+CFLAGS += -I$(XEN_ROOT)/xen/include
+CFLAGS += -I$(XEN_ROOT)/xen/arch/$(XEN_INCLUDE_ARCH)/include
+CFLAGS += $(CFLAGS_xeninclude)
+
+# Enable sanitizers to catch memory errors and undefined behavior in the code
+# for x86_64. Other architectures do not support -fstatic with it.
+ifeq ($(XEN_TARGET_ARCH),x86_64)
+CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
+endif
+
+# Build rules for the tests
+$(TARGETS): %: %.o $(LIB_OBJ)
+ $(CC) -o $@ $^ $(LDFLAGS) $(CPPFLAGS) $(CFLAGS) $(APPEND_CFLAGS)
+
+-include $(DEPS_INCLUDE)
diff --git a/tools/tests/alloc/README.rst b/tools/tests/alloc/README.rst
new file mode 100644
index 000000000000..3ed362598bb3
--- /dev/null
+++ b/tools/tests/alloc/README.rst
@@ -0,0 +1,31 @@
+.. SPDX-License-Identifier: CC-BY-4.0
+
+Unit and Integration test suite for the page allocator
+======================================================
+
+The tests in ``tools/tests/alloc`` contain unit tests for the
+Xen page allocator in ``xen/common/page_alloc.c`` and are
+built as standalone executables.
+
+They are not intended to be run in a Xen environment, but rather to test
+the allocator logic in isolation and can be run on any compatible host
+system at build time and do not use any installed libraries or require
+any special setup or dependencies beyond the standard C library.
+
+The tests use a shim as a substitute for Xen hypervisor code that would
+conflict with running the page allocator as a host executable, and they
+use helper functions to initialize and assert the status of the data
+structures of the allocator such as the page lists and zones.
+
+The tests can be run with the ``run`` target of the ``Makefile``, which
+will execute all the test executables and report their results unless
+you override the TARGETS variable to run a specific test:
+
+.. code:: shell
+
+ make -C tools/tests/alloc clean all run \
+ TARGETS=test-reserve_offline_page-uma
+
+To add a new test, simply create a new C file with a name starting with
+``test-``, implement the test logic, and it will be automatically included
+in the build and run targets by default.
diff --git a/tools/tests/alloc/check-asserts.h b/tools/tests/alloc/check-asserts.h
new file mode 100644
index 000000000000..04a254c0999d
--- /dev/null
+++ b/tools/tests/alloc/check-asserts.h
@@ -0,0 +1,347 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Header-only library for check assertions in unit tests.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+#ifndef _CHECK_ASSERTS_H_
+#define _CHECK_ASSERTS_H_
+
+#include <assert.h>
+#include <execinfo.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/utsname.h>
+
+#ifndef CONFIG_NUMA
+#define CONFIG_NUMA 0
+#endif
+#define __used __attribute__((__used__))
+
+/** ## Global state for the test framework and assertions */
+
+/** Set when assertions are expected to fail */
+static bool testcase_assert_expected_to_fail = false;
+/** Set when verbose assertions are enabled */
+static bool testcase_assert_verbose_assertions = true;
+/**
+ * The current function for verbose assertions, used to avoid repeating the
+ * function name in the logs for multiple assertions within the same function.
+ */
+static const char *testcase_assert_current_func = NULL;
+/** The current indentation level for verbose assertions */
+static int testcase_assert_verbose_indent_level = 0;
+/** Failed checks since the last call call to EXPECTED_TO_FAIL_BEGIN() */
+static int testcase_assert_expected_failures = 0;
+/** Failed checks within EXPECTED_TO_FAIL_BEGIN()/END() in this test case */
+static int testcase_assert_expected_failures_total = 0;
+/** Successful assertions in this test case */
+static int testcase_assert_successful_assert_total = 0;
+#define assert_failed_str "Assertion failed: "
+
+/** ## Assertion macros and helpers */
+
+/** Check a condition and log the result with context. */
+#define CHECK(condition, fmt, ...) \
+ testcase_assert(condition, __FILE__, __LINE__, __func__, fmt, \
+ ##__VA_ARGS__)
+
+/** If the condition is false, treat it as a test assertion failure */
+#define ASSERT(x) \
+ testcase_assert(x, __FILE__, __LINE__, __func__, assert_failed_str #x)
+
+/** If the condition is true, treat it as a bug, used in Xen hypervisor code */
+#define BUG_ON(x) \
+ testcase_assert(!(x), __FILE__, __LINE__, __func__, "BUG_ON: " #x)
+
+/** Assert that the code is unreachable */
+#define ASSERT_UNREACHABLE() assert(false)
+
+/** ## Helpers for expected assertion failures */
+
+/** Marks the beginning of a block where assertions are expected to fail */
+#define EXPECTED_TO_FAIL_BEGIN() (testcase_assert_expected_to_fail = true)
+/** Marks the end of a block where assertions are expected to fail */
+#define EXPECTED_TO_FAIL_END(c) testcase_assert_check_expected_failures(c)
+
+/** Checks the number of expected failures against the actual count */
+static void __used testcase_assert_check_expected_failures(int expected)
+{
+ if ( testcase_assert_expected_failures != expected )
+ {
+ fprintf(stderr, "Test assertion expected %d failures, but got %d\n",
+ expected, testcase_assert_expected_failures);
+ abort();
+ }
+ testcase_assert_expected_to_fail = false;
+ testcase_assert_expected_failures = 0;
+ testcase_assert_expected_failures_total += expected;
+}
+
+/** ## Test case management and reporting */
+
+/** Function pointer used for initializing a test case */
+static void (*testcase_init_func)(const char *, int);
+
+/** Set up the function pointer for initializing a test case */
+static void __used setup_testcase_init_func(void (*init_fn)(const char *, int))
+{
+ testcase_init_func = init_fn;
+}
+
+/**
+ * Assert a condition within a test case
+ *
+ * This function is the core of the assertion mechanism for test cases.
+ * It checks a given condition and handles both expected and unexpected
+ * assertion failures.
+ *
+ * If the assertion is expected to fail, it logs the failure and increments
+ * the expected failure count. If the assertion is not expected to fail but
+ * does, it logs the failure and aborts the test. If the assertion passes,
+ * it increments the successful assertion count and optionally logs the
+ * successful assertion if verbose assertions are enabled.
+ *
+ * Args:
+ * condition (bool): The condition to check. If false, the assertion fails.
+ * file (const char *): The file where the assertion is located, for logging.
+ * line (int): The source line of the assertion, for logging.
+ * func (const char *): The function name where the assertion is located.
+ * fmt (const char *): A printf format string with context for the assertion.
+ * ...: Additional arguments for the format string.
+ */
+static void testcase_assert(bool condition, const char *file, int line,
+ const char *func, const char *fmt, ...)
+{
+ va_list ap;
+ const char *relpath = file;
+
+ while ( (file = strstr(relpath, "../")) )
+ relpath += 3;
+
+ va_start(ap, fmt);
+ if ( testcase_assert_expected_to_fail )
+ {
+ fprintf(stderr, "\n- Test assertion %s at %s:%d:\n ",
+ condition ? "unexpectedly passed" : "expectedly failed",
+ relpath, line);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ fprintf(stderr, "\n");
+
+ if ( condition )
+ abort(); /* Unexpected pass, treat as test failure */
+ else
+ testcase_assert_expected_failures++; /* update for the report */
+ return;
+ }
+ if ( !condition )
+ {
+ fprintf(stderr, "Test assertion failed at %s:%d: ", relpath, line);
+ vfprintf(stderr, fmt, ap);
+ fprintf(stderr, "\n");
+ abort();
+ }
+ testcase_assert_successful_assert_total++;
+ if ( testcase_assert_verbose_assertions )
+ {
+ /* As the assertion didn't actually fail, remove the prefix */
+ if ( strncmp(fmt, assert_failed_str, strlen(assert_failed_str)) == 0 )
+ fmt += strlen(assert_failed_str);
+
+ if ( strcmp(fmt, "ret == 0") == 0 )
+ return;
+
+ for ( int i = 0; i < testcase_assert_verbose_indent_level; i++ )
+ printf(" ");
+
+ printf("%s:%d: ", relpath, line);
+
+ /*
+ * Skip logging the passed function if it was already logged for the
+ * current function, or the source or the function starts with test.
+ */
+ if ( (testcase_assert_current_func == NULL ||
+ strcmp(testcase_assert_current_func, func)) &&
+ (strncmp(relpath, "test-", strlen("test-")) &&
+ strncmp(func, "test_", strlen("test_"))) )
+ printf("%s(): ", func);
+
+ if ( strncmp(fmt, "BUG_ON:", strlen("BUG_ON:")) )
+ printf("ASSERT(");
+
+ vprintf(fmt, ap);
+ va_end(ap);
+
+ if ( strncmp(fmt, "BUG_ON:", strlen("BUG_ON:")) )
+ printf(")");
+
+ printf("\n");
+ }
+}
+
+/** Structure to represent a test case and its results for reporting */
+struct testcase {
+ /** Human-readable name of the test case */
+ const char *name;
+ /** Test ID */
+ const char *tid;
+ /** Integer argument for the test case */
+ int intarg;
+ /** Function pointer to the test case function */
+ void (*func)(int);
+ /** Number of assertions passed */
+ int passed_asserts;
+ /** Number of expected failures occurred */
+ int expected_failures;
+} testcases[40];
+/** Pointer to the current test case being executed, for tracking results */
+struct testcase *current_testcase = testcases;
+
+static void print_testcase_report(struct testcase *tc)
+{
+ printf("- %-5s %-34s %2d: %3d assertions passed", tc->tid, tc->name,
+ tc->intarg, tc->passed_asserts);
+ if ( tc->expected_failures )
+ printf(" (%2d XFAIL)", tc->expected_failures);
+ printf("\n");
+}
+
+/**
+ * Execute the given test function and record the number of assertions
+ * passed and expected failures for the test report. The test function
+ * is expected to use the CHECK, ASSERT, and BUG_ON macros for assertions,
+ * and can use EXPECTED_TO_FAIL_BEGIN and EXPECTED_TO_FAIL_END to mark
+ * assertions that are expected to fail for testing negative scenarios.
+ *
+ * The test function is also passed an integer argument that can be used
+ * to specify different scenarios or parameters for the test.
+ *
+ * The test report will include the name of the test case, the integer
+ * argument, the number of assertions passed, and the number of expected
+ * failures. The test report is printed after the test function completes,
+ * and a summary report is printed after all test cases have been executed.
+ *
+ * The test function can also use the verbose assertion mode to print
+ * additional context for each assertion, which can be helpful for debugging
+ * test failures and understanding the test flow.
+ *
+ * Args:
+ * case_func (void (*)(int)):
+ * The test function to execute, which takes an int argument.
+ * int_arg (int): An argument to pass to the test function, which can be used
+ * to specify different scenarios or parameters for the test.
+ * tid (const char *):
+ * A test id; string identifier for the test case.
+ * case_name (const char *):
+ * A human-readable name for the test case, used for reporting.
+ */
+static void run_testcase(void (*case_func)(int), int int_arg, const char *tid,
+ const char *case_name)
+{
+ printf("\nTest Case: %s...\n", case_name);
+ current_testcase->name = case_name;
+ current_testcase->func = case_func;
+ current_testcase->intarg = int_arg;
+ current_testcase->tid = tid;
+ current_testcase->passed_asserts = 0;
+ current_testcase->expected_failures = 0;
+
+ /*
+ * Call the testcase initialization function if it is set, which can be
+ * used to reset global state or set up specific scenarios for the test.
+ *
+ * For example, the page allocator tests use this to reset the state of the
+ * synthetic page structures and the heap before each test case.
+ */
+ if ( testcase_init_func && int_arg >= 0 )
+ testcase_init_func(case_name, int_arg);
+
+ case_func(int_arg);
+
+ current_testcase->passed_asserts = testcase_assert_successful_assert_total;
+ current_testcase->expected_failures =
+ testcase_assert_expected_failures_total;
+
+ testcase_assert_successful_assert_total = 0;
+ testcase_assert_expected_failures_total = 0;
+
+ printf("\nResults:\n");
+ print_testcase_report(current_testcase);
+ current_testcase++;
+}
+#define RUN_TESTCASE(tid, func, arg) run_testcase(func, arg, #tid, #func)
+
+/**
+ * Provide a report of all test cases executed and their results,
+ * including the total number of assertions passed and expected failures.
+ */
+static int testcase_print_summary(const char *argv0)
+{
+ struct utsname uts;
+ int total_asserts = 0, expected_failures = 0;
+
+ fprintf(stderr, "\nTest Report:\n");
+
+ current_testcase = testcases;
+ for ( size_t i = 0; i < ARRAY_SIZE(testcases) && current_testcase->func;
+ i++ )
+ {
+ print_testcase_report(current_testcase);
+ total_asserts += current_testcase->passed_asserts;
+ expected_failures += current_testcase->expected_failures;
+ current_testcase++;
+ }
+ current_testcase->tid = "Total";
+ current_testcase->name = "";
+ current_testcase->passed_asserts = total_asserts;
+ current_testcase->expected_failures = expected_failures;
+ current_testcase->intarg = current_testcase - testcases;
+ print_testcase_report(current_testcase);
+
+ uname(&uts);
+ printf("\nTest suite %s for %s completed.\n", argv0, uts.machine);
+ return 0;
+}
+
+static const char *parse_args(int argc, char *argv[], const char *topic)
+{
+ const char *program_name = argv[0];
+ struct utsname uts;
+
+ if ( argc != 1 )
+ {
+ fprintf(stderr, "Usage: %s\n", argv[0]);
+ return NULL;
+ }
+ program_name = strrchr(program_name, '/');
+ if ( program_name )
+ program_name++;
+ else
+ program_name = argv[0];
+
+ uname(&uts);
+ printf("Suite : %s\n", program_name);
+ printf("Topic : %s\n", topic);
+ printf("Config: CONFIG_NUMA %s\n",
+ config_enabled(CONFIG_NUMA) ? "enabled" : "disabled");
+#ifndef __clang__
+ printf("Target: gcc %s/%s\n", __VERSION__, uts.machine);
+#else
+ printf("Target: %s/%s\n", __VERSION__, uts.machine);
+#endif
+ return program_name;
+}
+
+#endif /* _CHECK_ASSERTS_H_ */
+
+/*
+ * Local variables:
+ * mode: C
+ * c-file-style: "BSD"
+ * c-basic-offset: 4
+ * indent-tabs-mode: nil
+ * End:
+ */
diff --git a/tools/tests/alloc/harness.h b/tools/tests/alloc/harness.h
new file mode 100644
index 000000000000..946c796c5475
--- /dev/null
+++ b/tools/tests/alloc/harness.h
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Common test harness for page allocation unit tests.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+
+#ifndef _TEST_HARNESS_
+#define _TEST_HARNESS_
+
+#include <assert.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+
+/* Enable debug mode to enable additional checks */
+#define CONFIG_DEBUG
+
+/* Define common macros which are compatible with the test context */
+#include "hypervisor-macros.h"
+
+/* Provide the common check_asserts library for test assertions */
+#include "check-asserts.h"
+
+/* Common Xen types for the test context */
+typedef uint8_t u8;
+typedef uint64_t paddr_t;
+typedef unsigned long cpumask_t;
+typedef long long s_time_t;
+typedef bool spinlock_t;
+
+/*
+ * The original implementation of reserve_offlined_page() causes the GCC
+ * and clang AddressSanitizer (ASAN) to report stack-buffer-overflow
+ * when the test_merge_tail_pair test case is run with ASAN enabled,
+ * when test verifies the state of the free lists in the heap.
+ *
+ * It finds several list pointer errors in the heap state and one of the
+ * appears to trigger ASAN's stack-buffer-overflow detection on x86_64.
+ *
+ * To temporarily work around this issue, we detect if ASAN is enabled
+ * and in order to be able skip the ASSERT_LIST_EQUAL verification step
+ * in the test case that triggers the ASAN error, while still allowing
+ * the rest of the test case to run and verify all execution with ASAN.
+ */
+/* clang-format off */
+#if defined(__has_feature)
+/* Clang uses __has_feature to detect AddressSanitizer */
+# if __has_feature(address_sanitizer)
+# define ASAN_ENABLED 1
+# endif
+/* GCC uses __SANITIZE_ADDRESS__ to detect AddressSanitizer */
+#elif defined(__SANITIZE_ADDRESS__)
+# define ASAN_ENABLED 1
+#else
+# define ASAN_ENABLED 0
+#endif
+/* clang-format on */
+#endif /* _TEST_HARNESS_H_ */
+
+/*
+ * Local variables:
+ * mode: C
+ * c-file-style: "BSD"
+ * c-basic-offset: 4
+ * indent-tabs-mode: nil
+ * End:
+ */
diff --git a/tools/tests/alloc/hypervisor-macros.h b/tools/tests/alloc/hypervisor-macros.h
new file mode 100644
index 000000000000..0d35bd9a806c
--- /dev/null
+++ b/tools/tests/alloc/hypervisor-macros.h
@@ -0,0 +1,101 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Common macros and definitions for building host-side unit tests
+ * the Xen hypervisor.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+
+#ifndef _TEST_ALLOC_XEN_MACROS_
+#define _TEST_ALLOC_XEN_MACROS_
+
+/*
+ * In Xen, STATIC_IF(x) and config_enabled(x) are defined in kconfig.h
+ * which we cannot include, so we need to define the necessary macros.
+ */
+#define STATIC_IF(option) static_if(option)
+#define static_if(value) _static_if(__ARG_PLACEHOLDER_##value)
+#define _static_if(arg1_or_junk) ___config_enabled(arg1_or_junk static, )
+#define __ARG_PLACEHOLDER_1 0,
+#define config_enabled(cfg) _config_enabled(cfg)
+#define _config_enabled(value) __config_enabled(__ARG_PLACEHOLDER_##value)
+
+#define __config_enabled(arg1_or_junk) ___config_enabled(arg1_or_junk 1, 0)
+
+#define ___config_enabled(__ignored, val, ...) val
+
+/*
+ * We include common-macros.h to reuse the Xen-tools macros, which are
+ * not necessarily the same as the Xen hypervisor macros, but are close
+ * enough for the test context.
+ */
+#include <xen-tools/common-macros.h>
+
+/*
+ * Define the header guards of the Xen headers that the Xen hypervisor
+ * variants of the definitions in common-macros.h and bitops.h to prevent
+ * conflicting definitions from those headers that prevent clean compilation.
+ */
+#define __XEN_CONST_H__
+#define __MACROS_H__
+
+/*
+ * We also define the Xen hypervisor macros that are used by page_alloc.c
+ * but not defined by common-macros.h, but needed to build hypervisor code
+ * in the test context, such as IS_ALIGNED() and the ffsl/flsl macros.
+ */
+#define IS_ALIGNED(x, a) (!((x) & ((a) - 1)))
+
+/*
+ * Inclde the Xen-tools bitops.h to reuse the bitops from the tools side.
+ * They are not necessarily the same as the Xen hypervisor bitops, but are
+ * close enough for the test context.
+ */
+#include <xen-tools/bitops.h>
+/*
+ * Afer including the Xen-tools bitops.h, we need to redefine the ffsl and flsl
+ * macros to match the behavior of the Xen hypervisor's ffsl and flsl, which
+ * return unsigned int and are relevant for signed/unsigned conversion checking
+ * and type hints in the test context.
+ * And we need to undefine conflicting macros defined by xen-tools headers.
+ */
+#undef BITS_PER_LONG
+#undef __LITTLE_ENDIAN
+#undef __BIG_ENDIAN
+/* Xen ffsl returns 1-based position of lowest set bit as unsigned int */
+#undef ffsl /* tools/include/xen-tools/bitops.h returns signed int */
+#define ffsl(x) ((unsigned int)__builtin_ffsl(x))
+/* Xen flsl returns 1-based position of highest set bit as unsigned int */
+#define flsl(x) ((unsigned int)((x) ? BITS_PER_LONG - __builtin_clzl(x) : 0))
+
+/* Common assertion and logging macros */
+#define BUG() assert(false)
+#define domain_crash(d) ((void)(d))
+#define PRI_mfn "lu"
+#define PRI_stime "lld"
+#define printk printf
+#define dprintk(level, fmt, ...) printk(fmt, ##__VA_ARGS__)
+#define gdprintk(level, fmt, ...) printk(fmt, ##__VA_ARGS__)
+#define gprintk(level, fmt, ...) printk(fmt, ##__VA_ARGS__)
+#define panic(fmt, ...) \
+ do \
+ { \
+ fprintf(stderr, fmt, ##__VA_ARGS__); \
+ abort(); \
+ } while ( 0 )
+
+/* Support including xen/sections.h and other function attributes */
+#define __initdata
+#define __init __used
+#define __initcall(f) static int __used (*f##_ptr)(void) = (f)
+
+#endif /* _TEST_ALLOC_XEN_MACROS_ */
+
+/*
+ * Local variables:
+ * mode: C
+ * c-file-style: "BSD"
+ * c-basic-offset: 4
+ * indent-tabs-mode: nil
+ * End:
+ */
diff --git a/tools/tests/alloc/libtest-page_alloc.h b/tools/tests/alloc/libtest-page_alloc.h
new file mode 100644
index 000000000000..5654152cd48a
--- /dev/null
+++ b/tools/tests/alloc/libtest-page_alloc.h
@@ -0,0 +1,356 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Test framework for testing the memory-allocation functionality
+ * of xen/common/page_alloc.c, consisting of:
+ *
+ * 1. A header-only shim for page_alloc.c to provide the necessary
+ * definitions and helpers to allow the test framework to include
+ * the real page_alloc.c directly into its translation unit.
+ *
+ * 2. A set of mocks for the Xen types and functions used by page_alloc.c,
+ * sufficient to support the test scenarios in tools/tests/alloc.
+ *
+ * This includes mocks for NUMA topology, designed to allow the test
+ * scenarios to manipulate the state of the allocator and verify its
+ * behavior in a way that is consistent with how page_alloc.c acts when
+ * used by the running Xen hypervisor, while being self-contained and
+ * suitable for unit and integration testing.
+ *
+ * 3. A tiny wrapper which includes the real page_alloc.c for testing.
+ *
+ * It disables a few of the -Wextra warnings enabled by the test
+ * framework that are not yet fixed in page_alloc.c, such as some
+ * sign-compare warnings and unused parameter warnings in its code.
+ *
+ * 4. A library for NUMA heap initialisation, and asserting the heap status.
+ *
+ * This library provides functions to prepare the state of the memory
+ * allocator for the test scenarios, such as:
+ *
+ * a. Initializing the heap before each test case, creating NUMA nodes,
+ * and adding pages to the heap in specific states, such as free,
+ * allocated, marked to be offlined or already offlined.
+ *
+ * b. Verifying the state of the heap and the page_info structures after
+ * test actions, such as checking that pages are allocated or freed
+ * as expected, that the state of the page_info structures is consistent
+ * with the expected state.
+ *
+ * 5. Test case lifecycle management, such as initializing the test context
+ * before each test case, printing the outcome of each test case,
+ * tracking the number of assertions, logging assertions with file
+ * and line information, and printing a summary report at the end.
+ *
+ * 6. A Makefile for discovering, compiling, running the test cases,
+ * and reporting results which test cases were run.
+ *
+ * The Makefile is designed to allow running individual test cases
+ * or the entire test suite for all supported CPU architectures, if
+ * so desired.
+ *
+ * It is also responsible for compiling the tests with address sanitizer
+ * (ASAN) enabled to catch memory errors in the page allocator code
+ * and the test code, especially when manipulating the state of the
+ * page_info structures inside the test scenarios.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+#include <stdarg.h>
+#include <execinfo.h>
+#include <string.h>
+
+/* Enable -Wextra warnings as errors to catch e.g. sign-compare issues */
+#pragma GCC diagnostic error "-Wextra"
+
+/* Support for printing the status of pages for debugging */
+struct page_info;
+static void print_page_info(struct page_info *pos);
+static void print_page_count_info(unsigned long count_info);
+
+#define TEST_USES_PAGE_ALLOC_SHIM
+#include "page_alloc_shim.h"
+
+/* Include the real page_alloc.c for testing */
+
+#include "page_alloc-wrapper.h"
+
+static const unsigned int node = 0;
+static const unsigned int node0 = 0;
+static const unsigned int node1 = 1;
+static const unsigned int order0 = 0;
+static const unsigned int order1 = 1;
+static const unsigned int order2 = 2;
+static const unsigned int order3 = 3;
+
+/**
+ * Functions for setting up test scenarios with a clean allocator state,
+ * and for building synthetic buddy trees with the expected page_info state.
+ */
+
+/* Set up a bare minimum NUMA node topology. */
+static void init_numa_node_data(unsigned int start_mfn)
+{
+ (void)start_mfn;
+#ifdef CONFIG_NUMA
+ /*
+ * For simplicity, we assign each CPU to its own node, and set each
+ * node's cpumask to contain just that CPU.
+ *
+ * If needed, we could easily modify this setup to have multiple CPUs
+ * per node by adjusting the cpu_to_node assignments and node_to_cpumask
+ * values accordingly.
+ *
+ * The test scenarios in this suite do not currently require multiple
+ * CPUs per node, but we could extend them to do so if desired.
+ *
+ * This is just a default setup that the test scenarios can rely on,
+ * and they are free to modify the cpu_to_node and node_to_cpumask
+ * values as needed for their specific test cases.
+ */
+ for ( unsigned int i = 0; i < NR_CPUS; i++ )
+ cpu_to_node[i] = i;
+
+ /* Each node has a single CPU in its cpumask for simplicity */
+ for ( unsigned int i = 0; i < MAX_NUMNODES; i++ )
+ node_to_cpumask[i] = (1UL << i);
+
+ /* Initialize node data structures */
+ for ( unsigned int i = 0; i < MAX_NUMNODES; i++ )
+ {
+ /* Each node has 8 pages for testing for now */
+ node_data[i].node_start_pfn = start_mfn + (i * 8);
+ node_data[i].node_present_pages = 8UL;
+ node_data[i].node_spanned_pages = 8UL;
+ }
+
+ /*
+ * Set up memnodemap so that mfn_to_nid() correctly resolves MFN
+ * ranges to NUMA nodes: with memnode_shift=3 each memnodemap entry
+ * covers 8 MFNs (2^3), matching the 8-page-per-node layout above.
+ * Entry i maps MFNs [i*8 .. (i+1)*8 - 1] to node i.
+ */
+ memnode_shift = 3;
+ for ( unsigned int i = 0; i < 64; i++ )
+ memnodemap[i] = (nodeid_t)i;
+#endif /* CONFIG_NUMA */
+}
+
+static void init_dummy_domains(void);
+/**
+ * Reset all page_alloc translation-unit globals that these tests observe.
+ *
+ * The test program includes xen/common/page_alloc.c directly, so its
+ * file-scope variables become global variables of this translation unit.
+ *
+ * Each test must start from a clean page allocator state, with a clean heap,
+ * clean availability counters, and empty offlined and broken lists.
+ */
+static void reset_page_alloc_state(const char *caller_func, int start_mfn)
+{
+ unsigned int zone;
+ unsigned int order;
+
+ printf("\n%s: start_mfn = %u\n", caller_func, start_mfn);
+
+ /* Clear the test page table used for synthetic page_info objects. */
+ memset(frame_table, 0, sizeof(frame_table));
+
+ /* Clear the backing storage used by the imported allocator globals. */
+ memset(test_heap_storage, 0, sizeof(test_heap_storage));
+ memset(test_avail_storage, 0, sizeof(test_avail_storage));
+
+ /* Clear the shim-owned singleton objects used by helper macros. */
+ memset(&test_dummy_domain1, 0, sizeof(test_dummy_domain1));
+ memset(&test_dummy_domain2, 0, sizeof(test_dummy_domain2));
+ memset(&test_current_vcpu, 0, sizeof(test_current_vcpu));
+
+ /* Reinitialise the global page lists manipulated by the allocator. */
+ INIT_PAGE_LIST_HEAD(&page_offlined_list);
+ INIT_PAGE_LIST_HEAD(&page_broken_list);
+ INIT_PAGE_LIST_HEAD(&test_page_list);
+
+ init_numa_node_data(start_mfn); /* Only used for NUMA-enabled builds */
+
+ /* Reinitialise every per-zone, per-order free-list bucket. */
+ for ( nodeid_t node = 0; node < MAX_NUMNODES; node++ )
+ {
+ _heap[node] = &test_heap_storage[node];
+ avail[node] = test_avail_storage[node];
+ node_avail_pages[node] = 0;
+ for ( zone = 0; zone < NR_ZONES; zone++ )
+ for ( order = 0; order <= MAX_ORDER; order++ )
+ INIT_PAGE_LIST_HEAD(&heap(node, zone, order));
+ }
+
+ total_avail_pages = 0;
+ outstanding_claims = 0;
+ /*
+ * The valid MFN range for the test context is configured to cover only
+ * the test frame table, so that any attempts by page_alloc.c to prevent
+ * functions in page_alloc.c is only manipulating the intended test
+ * state and not accessing uninitialized memory or going out of bounds.
+ *
+ * Set up the initial range of valid pages for mfn_valid() used by
+ * free_heap_pages() as condition if there are successors/predecessors
+ * to merge pages with. Unless successors/predecessors are initialized
+ * to be free, it should forgoe merging and just add the provided page
+ * as-is to the heap, but to prevent it looking up uninitialised memory,
+ * we set the valid MFN range to cover the frame_table only.
+ */
+ first_valid_mfn = start_mfn;
+ max_page = sizeof(frame_table) / sizeof(frame_table[0]);
+ assert(first_valid_mfn < max_page);
+
+ init_dummy_domains();
+}
+
+static void init_dummy_domains(void)
+{
+ nodemask_t dom_node_affinity;
+ struct domain *dom;
+ int dom_id = 1; /* Start domain IDs from 1 for clarity in logs */
+
+ /* Provide a current vcpu/domain pair for code paths that inspect it. */
+ test_current_vcpu.domain = &test_dummy_domain1;
+
+ /* Provide the dummy domains for tests that need some domains */
+ domain_list = &test_dummy_domain1;
+ test_dummy_domain1.next_in_list = &test_dummy_domain2;
+
+ nodes_clear(dom_node_affinity);
+ node_set(node0, dom_node_affinity);
+ node_set(node1, dom_node_affinity);
+
+ for_each_domain ( dom )
+ {
+ dom->node_affinity = dom_node_affinity;
+ dom->max_pages = MAX_PAGES;
+ dom->domain_id = dom_id++;
+ }
+}
+
+/* Initialize the page allocator tests */
+static void __used init_page_alloc_tests(void)
+{
+ /* Define the function above as the testcase initialization function */
+ setup_testcase_init_func(reset_page_alloc_state);
+}
+
+/**
+ * Populate a page descriptor with the minimal state needed by
+ * reserve_offlined_page().
+ *
+ * Tests build synthetic buddy trees by placing a small set of
+ * page_info objects into allocator free lists. This helper
+ * keeps that setup consistent across scenarios.
+ *
+ * Args:
+ * page (struct page_info *): Pointer to the page_info to be initialised.
+ * order (unsigned int): The order to set in the page_info's order field
+ * state (unsigned long): The state bits to set in the page's count_info
+ * field, e.g. PGC_state_inuse for pages to be
+ * added to the heap, or PGC_state_offlined for
+ * pages to be added to the offlined list.
+ */
+static void init_test_page(struct page_info *page, unsigned int order,
+ unsigned long state)
+{
+ mfn_t mfn = page_to_mfn(page);
+
+ if ( mfn < first_valid_mfn && mfn > 0 && mfn < max_page )
+ first_valid_mfn = mfn;
+
+ if ( mfn >= max_page && mfn < ARRAY_SIZE(frame_table) )
+ max_page = mfn + 1;
+
+ CHECK(mfn_valid(mfn), "mfn %lu valid: %lu-%lu", mfn, first_valid_mfn,
+ max_page);
+
+ memset(page, 0, sizeof(*page));
+
+ /* Model the page as a free buddy head of the requested order. */
+ page->v.free.order = order;
+
+ /* Default to no tracked dirty subrange and no active scrubbing. */
+ page->u.free.first_dirty = INVALID_DIRTY_IDX;
+ page->u.free.scrub_state = BUDDY_NOT_SCRUBBING;
+
+ /* Install the requested allocator state bits for this synthetic page. */
+ page->count_info = state;
+}
+
+/**
+ * Initialize the given pages as a buddy of the requested order,
+ * with the first page as the buddy head and the rest as subpages
+ * of it, and add the intialised buddy to the heap.
+ *
+ * This helper is intended to be used by test scenarios to set up
+ * the heap with buddies of the expected order and state for testing
+ * operations that manipulate the heap, such as reserve_offlined_page()
+ * and free_heap_pages(), and to ensure that the heap state is consistent
+ * with the page_info state after those operations.
+ *
+ * The buddy is added to the heap using free_heap_pages() which
+ * models the expected usage of the heap and ensures that the
+ * heap structures are updated correctly according to the logic
+ * of the allocator, which may change over time.
+ *
+ * For example, if the logic for merging buddies or tracking claims changes,
+ * using free_heap_pages() ensures that the test setup will be correct even
+ * after such changes, and that the test scenarios will be testing the real
+ * behaviour of the allocator rather than an idealised version of it.
+ *
+ * Args:
+ * pages (struct page_info *): Pointer to the first page_info in an array.
+ * order (unsigned int): The order of the buddy to be created.
+ * caller (const char *): The name of the calling function for context.
+ * Returns:
+ * The zone of the added buddy, which can be useful for test scenarios that
+ * need to know the zone of the buddy for further operations or assertions.
+ */
+static zone_t __used page_list_add_buddy(struct page_info *pages,
+ unsigned int order, const char *caller)
+{
+ size_t i, num_pages = 1U << order;
+ bool verbose_asserts_save = testcase_assert_verbose_assertions;
+
+ /* Avoid logging spinlocks and verbose assertions during initialization */
+ testcase_assert_verbose_assertions = false;
+
+ /*
+ * Initialize the first page as the head of the buddy with the given order.
+ * All pages are initialized as in-use as this is the API expected by
+ * free_heap_pages() when it adds a buddy to the heap(). This model is
+ * consistent with the way the boot allocator and online_page() handle
+ * page initialization as well as the normal way for used pages to be freed.
+ */
+ init_test_page(&pages[0], order, PGC_state_inuse);
+
+ /* Add the subpages of the buddy as order-0 buddies to the heap */
+ for ( i = 1; i < num_pages; i++ )
+ init_test_page(&pages[i], order0, PGC_state_inuse);
+
+ /*
+ * Add the created buddy to the heap. This uses the same code path as
+ * freeing used pages and is consistent with the way the boot allocator
+ * and online_page() handle page initialization. Using free_heap_pages()
+ * has the additional benefit of ensuring that the heap structures are
+ * consistent even if the internal logic of the heap management changes.
+ *
+ * For example, implementing NUMA claims adds new per-node claims counters
+ * and logic to free_heap_pages(), so using it here ensures that the test
+ * setup will be correct even after such changes.
+ */
+ printf("%s: Adding buddy of order %u at MFN %lu to the heap.\n", caller,
+ order, page_to_mfn(&pages[0]));
+
+ testcase_assert_verbose_assertions = false;
+
+ free_heap_pages(&pages[0], order, false);
+
+ testcase_assert_verbose_assertions = verbose_asserts_save;
+ return page_to_zone(&pages[0]);
+}
+
+#define test_page_list_add_buddy(pages, order) \
+ page_list_add_buddy(pages, order, __func__)
diff --git a/tools/tests/alloc/mock-page_list.h b/tools/tests/alloc/mock-page_list.h
new file mode 100644
index 000000000000..92a0f7c53042
--- /dev/null
+++ b/tools/tests/alloc/mock-page_list.h
@@ -0,0 +1,307 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Mock page list implementation for testing page allocator functions.
+ *
+ * This mock implementation provides a simplified version of the page list
+ * structures and functions used by the page allocator, allowing unit tests
+ * to be written without relying on the full Xen environment.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+#ifndef _MOCK_PAGE_LIST_H_
+#define _MOCK_PAGE_LIST_H_
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "harness.h" /* Provides the generic Xen defintitions needed */
+
+/*
+ * Wrapper around xen/config.h for common Xen definitions for the test context
+ */
+#define __XEN_KCONFIG_H
+#undef __nonnull
+#undef offsetof
+#include <xen/config.h>
+/* Xen code adds cf_check as an attribute macro to functions we don't call */
+#undef cf_check
+#define cf_check __used
+
+#define MAX_ORDER 20
+
+/*
+ * The page_info structures in the frame table are manipulated directly
+ * by page_alloc.c, so they must be defined in a way that is consistent
+ * with how page_alloc.c uses them, while being self-contained and suitable
+ * for unit testing.
+ *
+ * The test scenarios can then manipulate the state of these page_info
+ * structures to set up test conditions and verify the behavior of the
+ * allocator in a way that is consistent with how it acts when used by
+ * the running hypervisor.
+ */
+struct page_info {
+ unsigned long count_info; /* PGC_state_inuse and other flags/counters */
+ union {
+ /* When the page is in use, u.inuse.type_info is used for status */
+ struct {
+ unsigned long type_info;
+ } inuse;
+ /*
+ * When the page is free, u.free is used for buddy management.
+ *
+ * Using BUILD_BUG_ON(sizeof(var->u) != sizeof(long)), page_alloc
+ * enforces that the u.free struct is exactly the size of a long.
+ * 32-bit architectures need to use bitfields to fit the fields
+ * in a long, while 64-bit architectures can use normal fields.
+ */
+ union {
+ struct {
+ unsigned int first_dirty : MAX_ORDER + 1;
+#define INVALID_DIRTY_IDX ((1UL << (MAX_ORDER + 1)) - 1)
+ bool need_tlbflush : 1;
+ unsigned long scrub_state : 2;
+#define BUDDY_NOT_SCRUBBING 0
+#define BUDDY_SCRUBBING 1
+#define BUDDY_SCRUB_ABORT 2
+ };
+ unsigned long val;
+ } free;
+ } u;
+ union {
+ struct {
+ unsigned int order;
+#define PFN_ORDER(pg) ((pg)->v.free.order)
+ } free;
+ unsigned long type_info;
+ } v;
+ uint32_t tlbflush_timestamp;
+ struct domain *owner;
+ struct page_info *list_next;
+ struct page_info *list_prev;
+};
+
+struct page_list_head {
+ struct page_info *head;
+ struct page_info *tail;
+ unsigned int count;
+};
+#define PAGE_LIST_HEAD(name) struct page_list_head name = {NULL, NULL, 0}
+
+static inline void test_page_list_init(struct page_list_head *list)
+{
+ list->head = NULL;
+ list->tail = NULL;
+ list->count = 0;
+}
+#define INIT_PAGE_LIST_HEAD(l) test_page_list_init(l)
+
+/* Used by page_alloc.c */
+#define page_list_for_each_safe(pos, tmp, list) \
+ for ( (pos) = page_list_first(list), \
+ (tmp) = (pos) ? (pos)->list_next : NULL; \
+ (pos) != NULL; \
+ (pos) = (tmp), (tmp) = (pos) ? (pos)->list_next : NULL )
+
+typedef unsigned long mfn_t;
+static struct page_list_head test_page_list;
+
+#define page_to_list(d, pg) (&test_page_list)
+#define page_list_add(pg, list) test_page_list_add((pg), (list))
+#define page_list_add_tail(pg, list) test_page_list_add_tail((pg), (list))
+#define page_list_del(pg, list) test_page_list_del((pg), (list))
+#define page_list_empty(list) ((list)->head == NULL)
+#define page_list_first(list) ((list)->head)
+#define page_list_last(list) ((list)->tail)
+#define page_list_remove_head(list) test_page_list_remove_head((list))
+
+static inline void test_page_list_add_common(struct page_info *pg,
+ struct page_list_head *list,
+ bool at_tail)
+{
+ pg->list_next = NULL;
+ pg->list_prev = NULL;
+
+ if ( list->head == NULL )
+ {
+ list->head = pg;
+ list->tail = pg;
+ }
+ else if ( at_tail )
+ {
+ pg->list_prev = list->tail;
+ list->tail->list_next = pg;
+ list->tail = pg;
+ }
+ else
+ {
+ pg->list_next = list->head;
+ list->head->list_prev = pg;
+ list->head = pg;
+ }
+
+ list->count++;
+}
+
+#define test_page_list_add(pg, list) test_page_list_add_common(pg, list, false)
+#define test_page_list_add_tail(pg, list) \
+ test_page_list_add_common(pg, list, true)
+
+static inline void test_page_list_del(struct page_info *pg,
+ struct page_list_head *list)
+{
+ if ( pg->list_prev )
+ pg->list_prev->list_next = pg->list_next;
+ else
+ list->head = pg->list_next;
+
+ if ( pg->list_next )
+ pg->list_next->list_prev = pg->list_prev;
+ else
+ list->tail = pg->list_prev;
+
+ pg->list_next = NULL;
+ pg->list_prev = NULL;
+
+ ASSERT(list->count > 0);
+ list->count--;
+}
+
+static inline struct page_info *
+test_page_list_remove_head(struct page_list_head *list)
+{
+ struct page_info *pg = list->head;
+
+ if ( pg )
+ test_page_list_del(pg, list);
+
+ return pg;
+}
+
+/*
+ * The frame table is the foundation for the buddy allocator algorithm
+ * implemented by page_alloc.c, and the test scenarios manipulate the
+ * state of the page_info structures in the frame table to set up test
+ * conditions and verify the behavior of the allocator.
+ *
+ * The frame table is indexed by MFN as required by the buddy allocator
+ * algorithm in page_alloc.c, so the translation functions between page_info
+ * pointers and MFNs are defined to allow page_alloc.c to manipulate the
+ * page_info structures in the test frame table using MFN-based translations.
+ */
+extern struct page_info frame_table[];
+#define page_to_mfn(pg) ((mfn_t)((pg) - &frame_table[0]))
+#define mfn_to_page(mfn) (&frame_table[(mfn)])
+#define mfn_valid(mfn) (mfn >= first_valid_mfn && mfn < max_page)
+#define maddr_to_page(pa) (CHECK(false, "Not implemented"))
+
+/*
+ * Helper functions to print the state of the heap and offlined pages
+ * for reference while asserting consistency of the heap and offlined
+ * page state.
+ *
+ * These functions are called at various points in the test scenarios
+ * to validate that the internal state of the allocator is consistent
+ * with expectations.
+ */
+
+/* Architecture-specific page state defines */
+#define PG_shift(idx) (BITS_PER_LONG - (idx))
+#define PG_mask(x, idx) (x##UL << PG_shift(idx))
+#define PGT_count_width PG_shift(2)
+#define PGT_count_mask ((1UL << PGT_count_width) - 1)
+#define PGC_allocated PG_mask(1, 1)
+#define PGC_xen_heap PG_mask(1, 2)
+#define _PGC_need_scrub PG_shift(4)
+#define PGC_need_scrub PG_mask(1, 4)
+#define _PGC_broken PG_shift(7)
+#define PGC_broken PG_mask(1, 7)
+#define PGC_state PG_mask(3, 9)
+#define PGC_state_inuse PG_mask(0, 9)
+#define PGC_state_offlining PG_mask(1, 9)
+#define PGC_state_offlined PG_mask(2, 9)
+#define PGC_state_free PG_mask(3, 9)
+#define page_state_is(pg, st) (((pg)->count_info & PGC_state) == PGC_state_##st)
+#define PGC_count_width PG_shift(9)
+#define PGC_count_mask ((1UL << PGC_count_width) - 1)
+#define _PGC_extra PG_shift(10)
+#define PGC_extra PG_mask(1, 10)
+
+struct PGC_flag_names {
+ unsigned long flag;
+ const char *name;
+} PGC_flag_names[] = {
+ {.flag = PGC_need_scrub, "PGC_need_scrub"},
+ {.flag = PGC_extra, "PGC_extra" },
+ {.flag = PGC_broken, "PGC_broken" },
+ {.flag = PGC_xen_heap, "PGC_xen_heap" },
+};
+
+static const char *pgc_state_name(unsigned long count_info)
+{
+ switch ( count_info & PGC_state )
+ {
+ case PGC_state_inuse:
+ return "PGC_state_inuse";
+ case PGC_state_offlining:
+ return "PGC_state_offlining";
+ case PGC_state_offlined:
+ return "PGC_state_offlined";
+ case PGC_state_free:
+ return "PGC_state_free";
+ default:
+ assert("Invalid page state" && false);
+ }
+}
+
+/* Print the count_info flags of a page_info for reference */
+static void print_page_count_info(unsigned long count_info)
+{
+ printf(" flags: %s", pgc_state_name(count_info));
+ for ( size_t i = 0; i < ARRAY_SIZE(PGC_flag_names); i++ )
+ if ( count_info & PGC_flag_names[i].flag )
+ printf(" %s", PGC_flag_names[i].name);
+ puts("");
+}
+
+/* Print the state of a single page for reference */
+static void print_page_info(struct page_info *pos)
+{
+ printf(" mfn %lu: order %u, first_dirty %x\n", page_to_mfn(pos),
+ PFN_ORDER(pos), pos->u.free.first_dirty);
+ print_page_count_info(pos->count_info);
+}
+
+/* Print and assert the state of an offlined page.*/
+static void print_and_assert_offlined_page(struct page_info *pos)
+{
+ print_page_info(pos);
+ /*
+ * The order of offlined pages must always be 0 because pages are only
+ * offlined as standalone pages. Higher-order pages on the offline lists
+ * are not supported by reserve_offlined_page() and online_page().
+ */
+ CHECK(PFN_ORDER(pos) == 0, "All offlined pages must have order 0");
+
+ /*
+ * Check the first_dirty index of offlined pages: Current code does
+ * not use first_dirty for offlined pages as it only points to the
+ * first dirty subpage within a buddy on the heap, and offlined pages
+ * are not on the heap. As it is not used, current code sets it to
+ * INVALID_DIRTY_IDX when offlining a page, so just confirm that.
+ *
+ * PS: Their scrubbing state is tracked by count_info & PG_need_scrub.
+ * In case an offlined page is onlined, the onlining code will be
+ * responsible to set first_dirty based on the scrubbing state.
+ */
+ if ( pos->u.free.first_dirty != INVALID_DIRTY_IDX )
+ {
+ printf("WARNING: offlined page at MFN %lu has first_dirty %x but "
+ "expected INVALID_DIRTY_IDX\n",
+ page_to_mfn(pos), pos->u.free.first_dirty);
+ ASSERT(pos->u.free.first_dirty == INVALID_DIRTY_IDX);
+ }
+}
+
+#endif /* _MOCK_PAGE_LIST_H_ */
\ No newline at end of file
diff --git a/tools/tests/alloc/page_alloc-wrapper.h b/tools/tests/alloc/page_alloc-wrapper.h
new file mode 100644
index 000000000000..ce1889e3aa37
--- /dev/null
+++ b/tools/tests/alloc/page_alloc-wrapper.h
@@ -0,0 +1,465 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Test framework for testing Xen's memory-allocation functionality.
+ *
+ * This file wraps xen/common/page_alloc.c for the test framework.
+ *
+ * Context:
+ *
+ * The test framework includes the real page_alloc.c directly into its
+ * translation unit, along with mocks for the Xen types and functions
+ * used by page_alloc.c, and a library for NUMA heap initialisation
+ * and asserting the heap status.
+ *
+ * This file serves as the wrapper around page_alloc.c, providing the
+ * necessary definitions and helpers to allow the test framework to
+ * include the real page_alloc.c directly into its translation unit.
+ *
+ * It also provides wrapper functions for key page_alloc.c functions like
+ * mark_page_offline() and offline_page() to allow the test scenarios to
+ * log the actions being taken and the outcomes observed when these functions
+ * are called, which is important for understanding the behavior of the
+ * allocator during the test scenarios and for debugging any issues that arise.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+#include <stdarg.h>
+#include <string.h>
+
+#define TEST_USES_PAGE_ALLOC_SHIM
+#include "page_alloc_shim.h"
+
+/* Include the real page_alloc.c for testing */
+
+#pragma GCC diagnostic push
+/* TODO: We should fix the remaining sign-compare warnings in page_alloc.c */
+#pragma GCC diagnostic ignored "-Wsign-compare"
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#include "../../xen/common/page_alloc.c"
+#pragma GCC diagnostic pop
+
+/* Allows the logging spinlock/unlock mocks to identify the heap lock */
+static spinlock_t *heap_lock_ptr = &heap_lock;
+
+/*
+ * Global state for the test page allocator shim and helpers.
+ *
+ * This includes the heap storage and availability counters that the test
+ * scenarios manipulate, as well as the domain list and a bug counter for
+ * the test program to track any unexpected conditions encountered in the
+ * test helpers.
+ */
+#ifndef PAGES_PER_ZONE
+#define PAGES_PER_ZONE 8
+#endif
+
+#ifndef MAX_PAGES
+#define MAX_PAGES (MAX_NUMNODES * NR_ZONES * PAGES_PER_ZONE)
+#endif
+
+/*
+ * The test frame table serves as the backing storage for the page_info
+ * structures used in the test scenarios, and the page_info structures
+ * are indexed by MFN for easy translation between page_info pointers and
+ * MFNs in the test helpers and assertions.
+ *
+ * The frame table is the foundation for the buddy allocator algorithm
+ * implemented by page_alloc.c, and the test scenarios manipulate the
+ * state of the page_info structures in the frame table to set up test
+ * conditions and verify the behavior of the allocator.
+ */
+struct page_info frame_table[MAX_PAGES];
+
+/* Provide a test pages pointer for the test scenarios */
+static struct page_info *test_pages = frame_table;
+
+/*
+ * Global state for the test page allocator shim and helpers.
+ *
+ * This includes the heap storage and availability counters that the test
+ * scenarios manipulate, as well as the domain list and a bug counter for
+ * the test program to track any unexpected conditions encountered in the
+ * test helpers.
+ */
+static heap_by_zone_and_order_t test_heap_storage[MAX_NUMNODES];
+static unsigned long test_avail_storage[MAX_NUMNODES][NR_ZONES];
+struct domain *domain_list;
+typedef size_t zone_t;
+
+static int __used test_domain_install_claim_set(struct domain *d,
+ unsigned int nr_claims,
+ memory_claim_t *claim_set)
+{
+ bool save_verbose_asserts = testcase_assert_verbose_assertions;
+ char target_str[16];
+
+ /* Avoid logging verbose logging while marking a page offline */
+ testcase_assert_verbose_assertions = false;
+
+ printf("%s => Installing claim set for domain %u:\n", __func__,
+ d->domain_id);
+ for ( unsigned int i = 0; i < nr_claims; i++ )
+ {
+ switch ( claim_set[i].target )
+ {
+ case XEN_DOMCTL_CLAIM_MEMORY_GLOBAL:
+ snprintf(target_str, sizeof(target_str), "GLOBAL");
+ break;
+ case XEN_DOMCTL_CLAIM_MEMORY_LEGACY:
+ snprintf(target_str, sizeof(target_str), "LEGACY");
+ break;
+ default:
+ snprintf(target_str, sizeof(target_str), "NODE %u",
+ claim_set[i].target);
+ break;
+ }
+ printf(" Claim %u: pages=%lu, target=%s\n", i, claim_set[i].pages,
+ target_str);
+ }
+
+ int ret = domain_install_claim_set(d, nr_claims, claim_set);
+ printf("%s <= domain_install_claim_set() returned %d\n", __func__, ret);
+ testcase_assert_verbose_assertions = save_verbose_asserts;
+ return ret;
+}
+#define domain_install_claim_set(d, nr_claims, claim_set) \
+ test_domain_install_claim_set(d, nr_claims, claim_set)
+
+static unsigned long __used test_mark_page_offline(struct page_info *page,
+ int flag,
+ const char *caller_func)
+{
+ bool save_verbose_asserts = testcase_assert_verbose_assertions;
+
+ /* Avoid logging verbose logging while marking a page offline */
+ testcase_assert_verbose_assertions = false;
+
+ printf("%s => Marking page at MFN %lu as %s.\n", caller_func,
+ page_to_mfn(page), flag ? "broken" : "offlined");
+
+ mark_page_offline(page, flag);
+
+ testcase_assert_verbose_assertions = save_verbose_asserts;
+ return 0;
+}
+#define mark_page_offline(pg, flag) test_mark_page_offline(pg, flag, __func__)
+
+static const char *offline_state_name(uint32_t offline_status)
+{
+ switch ( offline_status )
+ {
+ case PG_OFFLINE_FAILED:
+ return "PG_OFFLINE_FAILED";
+ case PG_OFFLINE_PENDING:
+ return "PG_OFFLINE_PENDING";
+ case PG_OFFLINE_OFFLINED:
+ return "PG_OFFLINE_OFFLINED";
+ case PG_OFFLINE_AGAIN:
+ return "PG_OFFLINE_AGAIN";
+ default:
+ return "PG_OFFLINE_UNKNOWN_STATUS";
+ }
+}
+
+static int __used test_offline_page(mfn_t mfn, int broken, uint32_t *status)
+{
+ bool save_verbose_asserts = testcase_assert_verbose_assertions;
+
+ testcase_assert_verbose_assertions = false;
+ printf("%s => Offlining page at MFN %lu with broken=%d\n", __func__, mfn,
+ broken);
+
+ int ret = offline_page(mfn, broken, status);
+
+ printf("%s <= offline_page() returned %d, status=0x%x (%s)\n", __func__,
+ ret, *status, offline_state_name(*status));
+ testcase_assert_verbose_assertions = save_verbose_asserts;
+ return 0;
+}
+#define offline_page(mfn, broken, status) test_offline_page(mfn, broken, status)
+
+static int __used test_set_outstanding_pages(struct domain *dom,
+ unsigned long pages,
+ const char *caller_func)
+{
+ bool save_verbose_asserts = testcase_assert_verbose_assertions;
+
+ /* Avoid logging verbose logging while setting outstanding claims */
+ testcase_assert_verbose_assertions = false;
+ printf("%s => domain_set_outstanding_pages(dom=%u, pages=%lu)\n",
+ caller_func, dom->domain_id, pages);
+
+ int ret = domain_set_outstanding_pages(dom, pages);
+
+ printf("%s <= domain_set_outstanding_pages() = %d\n", caller_func, ret);
+ testcase_assert_current_func = NULL;
+ testcase_assert_verbose_assertions = save_verbose_asserts;
+ return ret;
+}
+#define domain_set_outstanding_pages(dom, pages) \
+ test_set_outstanding_pages(dom, pages, __func__)
+
+static struct page_info *__used test_alloc_domheap(struct domain *dom,
+ unsigned int order,
+ unsigned int memflags,
+ const char *caller_func)
+{
+ bool save_verbose_asserts = testcase_assert_verbose_assertions;
+
+ /* Avoid logging verbose logging while allocating domheap pages */
+ testcase_assert_verbose_assertions = false;
+ printf("%s => alloc_domheap_pages(dom=%u, order=%u, memflags=%x)\n",
+ caller_func, dom->domain_id, order, memflags);
+ testcase_assert_current_func = "alloc_domheap_pages";
+ testcase_assert_verbose_indent_level++;
+ struct page_info *pg = alloc_domheap_pages(dom, order, memflags);
+ testcase_assert_verbose_indent_level--;
+ testcase_assert_current_func = NULL;
+
+ testcase_assert_verbose_assertions = save_verbose_asserts;
+ return pg;
+}
+#define alloc_domheap_pages(dom, order, memflags) \
+ test_alloc_domheap(dom, order, memflags, __func__)
+
+#ifdef CONFIG_SYSCTL
+/* Helper for just getting the number of free pages for ASSERTs */
+static uint64_t __used free_pages(void)
+{
+ uint64_t free_pages, total_claims;
+ bool verbose_asserts_save = testcase_assert_verbose_assertions;
+
+ /* Avoid logging spinlock actions while getting the free page count */
+ testcase_assert_verbose_assertions = false;
+ get_outstanding_claims(&free_pages, &total_claims);
+ testcase_assert_verbose_assertions = verbose_asserts_save;
+ return free_pages;
+}
+#define FREE_PAGES free_pages()
+
+/* Helper for just getting the total number of claimed pages for ASSERTs */
+static uint64_t __used total_claims(void)
+{
+ uint64_t free_pages, total_claims;
+ bool verbose_asserts_save = testcase_assert_verbose_assertions;
+
+ /* Avoid logging spinlock actions while getting the total claims */
+ testcase_assert_verbose_assertions = false;
+ get_outstanding_claims(&free_pages, &total_claims);
+ testcase_assert_verbose_assertions = verbose_asserts_save;
+ return total_claims;
+}
+#define TOTAL_CLAIMS total_claims()
+
+#define DOM_GLOBAL_CLAIMS(d) ((d)->global_claims)
+#define DOM_NODE_CLAIMS(d, n) ((d)->claims[n])
+
+#endif /* CONFIG_SYSCTL */
+
+static void print_order_list(nodeid_t node, zone_t zone, size_t order)
+{
+ struct page_info *pos = page_list_first(&heap(node, zone, order));
+
+ if ( pos )
+ printf(" Heap for zone %zu, order %zu:\n", zone, order);
+
+ while ( pos )
+ {
+ size_t page_order = PFN_ORDER(pos);
+
+ print_page_info(pos);
+ /* Print the subpages of the buddy head */
+ for ( size_t sub_pg = 1; sub_pg < (1U << page_order); sub_pg++ )
+ {
+ struct page_info *sub_pos = pos + sub_pg;
+
+ printf(" ");
+ print_page_info(sub_pos);
+ /* Assert the subpages of a buddy to have order-0. */
+ ASSERT(PFN_ORDER(sub_pos) == 0);
+ }
+ /* Assert that the page_order matches the heap order. */
+ if ( page_order != order )
+ {
+ printf("ERROR:mfn %lu has order %zu but expected %zu "
+ "based on heap position\n",
+ page_to_mfn(pos), page_order, order);
+ ASSERT(page_order == order);
+ }
+ pos = pos->list_next;
+ }
+}
+
+#define CHECK_BUDDY(pages, fmt, ...) \
+ check_buddy(pages, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
+
+/* Function to print the order and first_dirty of each page for debugging. */
+static void check_buddy(struct page_info *pages, const char *file, int line,
+ const char *fmt, ...)
+{
+ size_t size = 1U << PFN_ORDER(pages);
+ bool verbose_asserts_save = testcase_assert_verbose_assertions;
+ va_list args;
+
+ if ( fmt ) /* Print the given message for context in the logs.*/
+ {
+ va_start(args, fmt);
+ printf(" %s:%d: ", file, line);
+ vprintf(fmt, args);
+ puts(":");
+ va_end(args);
+ }
+ else
+ printf(" %s:%d: %s():\n", file, line, __func__);
+
+ /* Avoid logging internal assertions while logging the free list status */
+ testcase_assert_verbose_assertions = false;
+
+ /*
+ * Inside pages, first_dirty must (if not INVALID_DIRTY_IDX) index the
+ * (first) page itself or a subpage within the page's range (<= 2^order).
+ */
+ for ( size_t i = 0; i < size; i++ )
+ {
+ unsigned long first_dirty = pages[i].u.free.first_dirty;
+ unsigned int tail_offset = (1U << PFN_ORDER(&pages[i])) - 1;
+
+ if ( first_dirty != INVALID_DIRTY_IDX && first_dirty > tail_offset )
+ {
+ printf("page at index %zu has first_dirty %lx but expected <= %u "
+ "based on its order\n",
+ i, first_dirty, tail_offset);
+ ASSERT(pages[i].u.free.first_dirty == tail_offset);
+ }
+ }
+
+ /* Traverse the offlined list, print and assert errors in it. */
+ struct page_info *pos = page_list_first(&page_offlined_list);
+ if ( pos )
+ puts(" Offlined list:");
+ while ( pos )
+ {
+ print_and_assert_offlined_page(pos);
+ pos = pos->list_next;
+ }
+
+ /* Traverse the broken list, print and assert errors in it. */
+ pos = page_list_first(&page_broken_list);
+ if ( pos )
+ puts(" Broken list:");
+ while ( pos )
+ {
+ print_and_assert_offlined_page(pos);
+ pos = pos->list_next;
+ }
+
+ /*
+ * Traverse the _heap[node] for each order and zone and print and assert
+ * the order and first_dirty of each page for each heap for debugging.
+ *
+ * This is to help verify that the heap structure is consistent with the
+ * page_info order fields after operations that manipulate both, such as
+ * reserve_offlined_page().
+ */
+ for ( nodeid_t node = 0; node < MAX_NUMNODES; node++ )
+ for ( size_t order = 0; order <= MAX_ORDER; order++ )
+ for ( size_t zone = 0; zone < NR_ZONES; zone++ )
+ print_order_list(node, zone, order);
+ testcase_assert_verbose_assertions = verbose_asserts_save;
+}
+
+/*
+ * Failure reporting helper that prints the provided message, the test
+ * caller context and a native backtrace before aborting.
+ */
+static void fail_with_ctx(const char *caller_file, const char *caller_func,
+ int caller_line, const char *fmt, ...)
+{
+ va_list ap;
+
+ fprintf(stderr, "\n- %s: Assertion failed: ", caller_func);
+ va_start(ap, fmt);
+ testcase_assert(false, caller_file, caller_line, caller_func, fmt, ap);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+}
+
+/*
+ * Assert that a page_list matches the provided sequence of page pointers.
+ *
+ * The public helper below is a macro so call sites can provide a simple list
+ * of page pointers while the implementation works over an ordinary array.
+ */
+static void __used assert_list_eq_array(struct page_list_head *list,
+ struct page_info *const expected[],
+ unsigned int nr_expected,
+ const char *call_file,
+ const char *caller_func,
+ int caller_line)
+{
+ struct page_info *pos;
+ int fails_before = testcase_assert_expected_failures;
+ unsigned int index = 0;
+
+ if ( list->count != nr_expected )
+ fail_with_ctx(call_file, caller_func, caller_line,
+ "list count mismatch: expected %u, got %u", nr_expected,
+ list->count);
+
+ if ( nr_expected == 0 )
+ {
+ if ( page_list_first(list) != NULL )
+ fail_with_ctx(call_file, caller_func, caller_line,
+ "expected empty list but head != NULL");
+ else
+ testcase_assert_successful_assert_total++;
+ return;
+ }
+
+ if ( page_list_first(list) != expected[0] )
+ fail_with_ctx(call_file, caller_func, caller_line,
+ "list head mismatch: expected %p, got %p", expected[0],
+ page_list_first(list));
+
+ for ( pos = page_list_first(list); pos; pos = pos->list_next, index++ )
+ {
+ if ( index >= nr_expected )
+ fail_with_ctx(call_file, caller_func, caller_line,
+ "list contains more elements than expected");
+
+ if ( pos != expected[index] )
+ fail_with_ctx(call_file, caller_func, caller_line,
+ "element %u mismatch: expected %p, got %p", index,
+ expected[index], pos);
+
+ if ( pos->list_prev != (index ? expected[index - 1] : NULL) )
+ fail_with_ctx(call_file, caller_func, caller_line,
+ "list_prev mismatch at index %u", index);
+
+ if ( pos->list_next !=
+ (index + 1 < nr_expected ? expected[index + 1] : NULL) )
+ fail_with_ctx(call_file, caller_func, caller_line,
+ "list_next mismatch at index %u", index);
+ }
+
+ if ( index != nr_expected )
+ fail_with_ctx(
+ call_file, caller_func, caller_line,
+ "list element count consumed mismatch: expected %u, got %u",
+ nr_expected, index);
+
+ if ( testcase_assert_expected_failures == fails_before )
+ testcase_assert_successful_assert_total++;
+}
+
+/** Assert that a page_list matches the provided sequence of page pointers. */
+#define ASSERT_LIST_EQUAL(list, ...) \
+ do \
+ { \
+ struct page_info *const expected[] = {__VA_ARGS__}; \
+ assert_list_eq_array((list), expected, ARRAY_SIZE(expected), \
+ __FILE__, \
+ __func__, __LINE__); \
+ } while ( 0 )
+/** Assert that a page_list is empty. */
+#define ASSERT_LIST_EMPTY(list) ASSERT(page_list_empty(list))
diff --git a/tools/tests/alloc/page_alloc_shim.h b/tools/tests/alloc/page_alloc_shim.h
new file mode 100644
index 000000000000..74459b6dda61
--- /dev/null
+++ b/tools/tests/alloc/page_alloc_shim.h
@@ -0,0 +1,433 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Header-only shim for unit testing functions in xen/common/page_alloc.c.
+ *
+ * This header provides the necessary definitions and helpers to allow:
+ *
+ * 1) the test program to include the real page_alloc.c directly into its
+ * translation unit, and
+ *
+ * 2) the test scenarios to manipulate the allocator state and verify its
+ * behavior in a way that is consistent with the real page_alloc.c, while
+ * being self-contained and suitable for unit testing.
+ *
+ * The test page allocator shim provides the necessary definitions for the
+ * page_info structure and the translation functions between page_info
+ * pointers and MFNs, so that page_alloc.c can manipulate the page_info
+ * structures in the test frame table and verify its behavior in a way
+ * that is consistent with how page_alloc.c acts when used by the running
+ * hypervisor, while being self-contained and suitable for unit testing.
+ *
+ * Shim definitions for Xen types and functions used by page_alloc.c.
+ *
+ * This shim is intended to be included directly in the test program
+ * or header-only library for testing a specific scenario, included
+ * by the test program, for unit testing functions in common/page_alloc.c.
+ *
+ * It provides stubs (minimal definitions for Xen types and functions)
+ * used by page_alloc.c, sufficient to support the test scenarios in
+ * tools/tests/alloc.
+ *
+ * It is not intended to be complete or accurate for general use in
+ * other test contexts or as a general-purpose shim for page_alloc.c.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+#ifndef _TEST_ALLOC_PAGE_ALLOC_SHIM_
+#define _TEST_ALLOC_PAGE_ALLOC_SHIM_
+
+/*.
+ * Guard against language servers and linters picking up this header in
+ * the wrong context.
+ *
+ * This header is only intended to be used in the test program for unit
+ * testing functions in xen/common/page_alloc.c, and test programs define
+ * TEST_USES_PAGE_ALLOC_SHIM to enable the definitions in this header.
+ */
+#ifndef TEST_USES_PAGE_ALLOC_SHIM
+#warning "This header is only for use in page_alloc.c unit tests."
+#else
+/*
+ * Inside the intended test context, provide mocks and stub definitions.
+ */
+
+/* Configure the included headers for the test context */
+#ifndef CONFIG_NR_CPUS
+#define CONFIG_NR_CPUS 64
+#endif
+
+#if defined(CONFIG_NUMA) && !defined(CONFIG_NR_NUMA_NODES)
+#define CONFIG_NR_NUMA_NODES 64
+#endif
+
+#define CONFIG_SCRUB_DEBUG
+
+/* Provide struct page_info and related Xen definitions */
+#include "mock-page_list.h"
+
+/* Include the common check_asserts library for test assertions */
+#include "check-asserts.h"
+
+/*
+ * We add the Xen headers to the include path so page_alloc.c can
+ * resolve its #include directives without having to replicate all
+ * headers as actual files in the test tree:
+ *
+ * We define the header guards of those files to prevent unwanted
+ * definitions from those headers that conflict with the test harness.
+ */
+#define XEN_SOFTIRQ_H
+#define XEN__XVMALLOC_H
+#define _LINUX_INIT_H
+#define _XEN_PARAM_H
+#define __LIB_H__ /* C runtime library, only for the hypervisor */
+#define __LINUX_NODEMASK_H
+#define __FLUSHTLB_H__
+#define __SCHED_H__
+#define __SPINLOCK_H__
+#define __TYPES_H__ /* Conflicts with the compiler-provided types */
+#define __VM_EVENT_H__
+#define __X86_PAGE_H__
+#define __XEN_CPUMASK_H
+#define __XEN_EVENT_H__
+#define __XEN_FRAME_NUM_H__
+#define __XEN_IRQ_H__
+#define __XEN_MM_H__
+#define __XEN_PDX_H__
+
+#include <xen/keyhandler.h>
+#include <xen/page-size.h>
+#include <public/xen.h>
+
+/* Include xen/numa.h with stubs and unused parameter warnings disabled */
+#define cpumask_clear_cpu(cpu, mask) ((void)(cpu), (void)(mask))
+#define mfn_to_pdx(mfn) ((unsigned long)(mfn))
+#pragma GCC diagnostic push
+#ifndef CONFIG_NUMA
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#endif
+#include <xen/numa.h>
+#pragma GCC diagnostic pop
+
+/* Flexible definition to support 32- and 64-bit architectures */
+#undef PADDR_BITS
+#define PADDR_BITS (BITS_PER_LONG - PAGE_SHIFT)
+#define pfn_to_paddr(pfn) ((paddr_t)(pfn) << PAGE_SHIFT)
+#define paddr_to_pfn(pa) ((unsigned long)((pa) >> PAGE_SHIFT))
+#define INVALID_MFN_INITIALIZER (~0UL)
+
+typedef unsigned long nodemask_t;
+
+struct domain {
+ spinlock_t page_alloc_lock;
+ nodemask_t node_affinity;
+ nodeid_t last_alloc_node;
+ domid_t domain_id;
+ unsigned int tot_pages;
+ unsigned int max_pages;
+ unsigned int extra_pages;
+ unsigned int global_claims;
+ unsigned int node_claims;
+ unsigned int claims[MAX_NUMNODES];
+ unsigned int xenheap_pages;
+ bool is_dying;
+ struct domain *next_in_list;
+};
+extern struct domain *domain_list;
+
+struct vcpu {
+ struct domain *domain;
+};
+
+/*
+ * Provide two domains for the test context, so that test helpers can
+ * call allocator functions that require domain context and verify behavior
+ * that depends on domain state, such as claims accounting and page allocation
+ * for specific domains.
+ */
+static struct domain test_dummy_domain1;
+static struct domain test_dummy_domain2;
+static struct domain __used *dom1 = &test_dummy_domain1;
+static struct domain __used *dom2 = &test_dummy_domain2;
+
+/* To provide a current vcpu/domain pair for code paths that inspect it. */
+static unsigned char test_dummy_storage[PAGE_SIZE];
+static struct vcpu test_current_vcpu;
+static struct vcpu *current = &test_current_vcpu;
+static cpumask_t cpu_online_map = ~0UL;
+
+#define for_each_domain(_d) \
+ for ( (_d) = domain_list; (_d) != NULL; (_d) = (_d)->next_in_list )
+#define for_each_online_node(i) for ( (i) = 0; (i) < MAX_NUMNODES; ++(i) )
+#define for_each_cpu(i, mask) for ( (i) = 0; (i) < 1; ++(i) )
+
+/* dom_cow is a domain pointer used by the memory sharing code */
+#ifdef CONFIG_MEM_SHARING
+static struct domain *dom_cow;
+#else
+#define dom_cow NULL
+#endif
+
+/*
+ * Logging spinlock for the test context
+ */
+static spinlock_t *heap_lock_ptr;
+
+/* Helper function to track spinlock actions for additional context */
+static void print_spinlock(const char *action, spinlock_t *lock,
+ const char *file, int line, const char *func)
+{
+ const char *relpath = file;
+
+ if ( !testcase_assert_verbose_assertions )
+ return;
+
+ while ( (file = strstr(relpath, "../")) )
+ relpath += 3;
+
+ for ( int i = 0; i < testcase_assert_verbose_indent_level; i++ )
+ printf(" ");
+
+ /* Print the path first:*/
+ if ( testcase_assert_current_func == NULL ||
+ strcmp(testcase_assert_current_func, func) != 0 )
+ printf("%s:%d: %s(): ", relpath, line, func);
+ else
+ printf("%s:%d: ", relpath, line);
+
+ if ( lock == heap_lock_ptr )
+ printf("heap_lock %s\n", action);
+ else if ( domain_list && lock == &test_dummy_domain1.page_alloc_lock )
+ printf("dom1->page_alloc_lock %s\n", action);
+ else if ( domain_list && lock == &test_dummy_domain2.page_alloc_lock )
+ printf("dom2->page_alloc_lock %s\n", action);
+ else
+ printf("unknown lock %p %s\n", (void *)lock, action);
+}
+
+/*
+ * If testcase_assert_verbose_assertions is enabled, the spinlock
+ * functions print the spinlock being acquired or released along with
+ * the file and line number of the assertion that triggered it.
+ * This can be helpful for debugging test failures and understanding
+ * the sequence of events leading up to the failure.
+ */
+#define spin_lock(l) \
+ (print_spinlock("acquired", l, __FILE__, __LINE__, __func__), (void)(l))
+#define spin_unlock(l) \
+ (print_spinlock("released", l, __FILE__, __LINE__, __func__), (void)(l))
+#define spin_lock_cb(l, cb, data) spin_lock(l)
+#define spin_lock_kick() ((void)0)
+#define nrspin_lock(l) spin_lock(l)
+#define nrspin_unlock(l) spin_unlock(l)
+#define rspin_lock(l) spin_lock(l)
+#define rspin_unlock(l) spin_unlock(l)
+#define DEFINE_SPINLOCK(l) spinlock_t l
+/*
+ * For the test context, we assume all locks are always held to avoid having
+ * to manage lock state in the test helpers. This allows the test helpers
+ * to call allocator functions that require locks to be held without needing
+ * to acquire those locks, which simplifies the test code and focuses on
+ * exercising the allocator logic under test.
+ *
+ * Invariants that would normally be protected by locks must still be upheld
+ * by the test helpers, but the test helpers can assume they have exclusive
+ * access to the allocator state and do not need to worry about concurrency.
+ */
+#define spin_is_locked(l) true
+#define rspin_is_locked(l) true
+
+/* memflags: */
+#define _MEMF_no_refcount 0
+#define MEMF_no_refcount (1U << _MEMF_no_refcount)
+#define _MEMF_populate_on_demand 1
+#define MEMF_populate_on_demand (1U << _MEMF_populate_on_demand)
+#define _MEMF_keep_scrub 2
+#define MEMF_keep_scrub (1U << _MEMF_keep_scrub)
+#define _MEMF_no_dma 3
+#define MEMF_no_dma (1U << _MEMF_no_dma)
+#define _MEMF_exact_node 4
+#define MEMF_exact_node (1U << _MEMF_exact_node)
+#define _MEMF_no_owner 5
+#define MEMF_no_owner (1U << _MEMF_no_owner)
+#define _MEMF_no_tlbflush 6
+#define MEMF_no_tlbflush (1U << _MEMF_no_tlbflush)
+#define _MEMF_no_icache_flush 7
+#define MEMF_no_icache_flush (1U << _MEMF_no_icache_flush)
+#define _MEMF_no_scrub 8
+#define MEMF_no_scrub (1U << _MEMF_no_scrub)
+#define _MEMF_node 16
+#define MEMF_node_mask ((1U << (8 * sizeof(nodeid_t))) - 1)
+#define MEMF_node(n) ((((n) + 1)&MEMF_node_mask) << _MEMF_node)
+#define MEMF_get_node(f) ((((f) >> _MEMF_node) - 1)&MEMF_node_mask)
+#define _MEMF_bits 24
+#define MEMF_bits(n) ((n) << _MEMF_bits)
+
+#define string_param(name, var)
+#define custom_param(name, fn)
+#define size_param(name, var)
+#define boolean_param(name, func)
+#define integer_param(name, var)
+#define ACCESS_ONCE(x) (x)
+#define cmpxchg(ptr, oldv, newv) \
+ ({ \
+ *(ptr) = (newv); \
+ (oldv); \
+ })
+
+#define is_xen_heap_page(pg) false
+#define page_to_virt(pg) ((void *)(pg))
+#define virt_to_page(v) ((struct page_info *)(v))
+#define mfn_to_virt(mfn) ((void *)&test_dummy_storage)
+#define __mfn_to_virt(mfn) mfn_to_virt(mfn)
+#define _mfn(x) ((mfn_t)(x))
+#define mfn_x(x) ((unsigned long)(x))
+#define mfn_add(mfn, nr) ((mfn) + (nr))
+#define mfn_min(a, b) ((a) < (b) ? (a) : (b))
+
+/*
+ * NUMA stubs for unit testing NUMA-aware page allocator logic.
+ *
+ * nodemask_test() and node_set() implement real bit operations so that
+ * domain_install_claim_set() can correctly detect duplicate node entries
+ * in a claim set. mfn_to_pdx() is defined before xen/numa.h is included.
+ */
+
+static nodemask_t node_online_map = ~0UL;
+#define num_online_nodes() MAX_NUMNODES
+#define node_online(node) ((node) < MAX_NUMNODES)
+#define nodes_intersects(a, b) ((a) & (b))
+#define nodes_and(dst, a, b) ((dst) = (a) & (b))
+#define nodes_andnot(dst, a, b) ((dst) = (a) & ~(b))
+#define nodes_clear(dst) ((dst) = 0)
+#define nodemask_test(node, mask) ((*(mask) >> (node)) & 1UL)
+#define node_set(node, mask) ((mask) |= (1UL << (node)))
+#define node_clear(node, mask) ((void)(mask))
+#define node_test_and_set(node, mask) false
+#define first_node(mask) 0U
+#define next_node(node, mask) MAX_NUMNODES
+#define cycle_node(node, mask) 0U
+
+#ifdef CONFIG_NUMA
+#define __node_distance(a, b) 0
+nodeid_t cpu_to_node[NR_CPUS];
+cpumask_t node_to_cpumask[MAX_NUMNODES];
+struct node_data node_data[MAX_NUMNODES];
+unsigned int memnode_shift;
+
+static typeof(*memnodemap) _memnodemap[64];
+nodeid_t *memnodemap = _memnodemap;
+unsigned long memnodemapsize = sizeof(_memnodemap);
+#endif /* CONFIG_NUMA */
+
+/*
+ * Stub definitions for Xen functions and macros used by page_alloc.c,
+ * sufficient to support the test scenarios in tools/tests/alloc.
+ *
+ * These are not intended to be complete or accurate for general use
+ * in other test contexts or as a general-purpose shim for page_alloc.c.
+ */
+#define rcu_lock_domain(id) (&test_dummy_domain1)
+#define rcu_lock_domain_by_any_id(id) (&test_dummy_domain1)
+#define NOW() 0LL
+#define SYS_STATE_active 1
+#define system_state 0
+#define cpu_online(cpu) ((cpu) == 0)
+#define smp_processor_id() 0U
+#define smp_wmb() ((void)0)
+#define cpumask_empty(mask) true
+#define cpumask_clear(mask) ((void)(mask))
+#define cpumask_and(dst, a, b) ((void)(dst), (void)(a), (void)(b))
+#define cpumask_or(dst, a, b) ((void)(dst), (void)(a), (void)(b))
+#define cpumask_copy(dst, src) ((void)(dst), (void)(src))
+#define cpumask_first(mask) 0U
+#define cpumask_intersects(a, b) false
+#define cpumask_weight(mask) 1
+#define __cpumask_set_cpu(cpu, mask) ((void)(cpu), (void)(mask))
+#define page_get_owner(pg) ((pg)->owner)
+#define page_set_owner(pg, d) ((pg)->owner = (d))
+#define page_get_owner_and_reference(pg) ((pg)->owner)
+#define page_set_tlbflush_timestamp(pg) ((pg)->tlbflush_timestamp = 0)
+#define set_gpfn_from_mfn(mfn, gpfn) ((void)0)
+#define page_is_offlinable(mfn) true
+#define is_xen_fixed_mfn(mfn) false
+#define filtered_flush_tlb_mask(ts) ((void)(ts))
+#define accumulate_tlbflush(need, pg, ts) ((void)(need), (void)(pg), (void)(ts))
+#define flush_page_to_ram(mfn, icache) ((void)(mfn), (void)(icache))
+#define scrub_page_hot(ptr) clear_page_hot(ptr)
+#define scrub_page_cold(ptr) clear_page_cold(ptr)
+#define send_global_virq(virq) ((void)(virq))
+#define softirq_pending(cpu) false
+#define process_pending_softirqs() ((void)0)
+#define on_selected_cpus(msk, f, data, w) ((void)0)
+#define cpu_relax() ((void)0)
+#define xmalloc(type) calloc(1, sizeof(type))
+#define xmalloc_array(type, nr) calloc((nr), sizeof(type))
+#define xvzalloc_array(type, nr) calloc((nr), sizeof(type))
+#define xvmalloc_array(type, nr) calloc((nr), sizeof(type))
+#define get_order_from_pages(nr) 0U
+#define get_order_from_bytes(bytes) 0U
+#define arch_mfns_in_directmap(mfn, nr) true
+#define maddr_to_mfn(pa) ((mfn_t)paddr_to_pfn(pa))
+
+#define ASSERT_ALLOC_CONTEXT() ((void)0)
+#define arch_free_heap_page(d, pg) ((void)(d), (void)(pg))
+#define get_knownalive_domain(d) ((void)(d))
+#define domain_clamp_alloc_bitsize(d, bits) (bits)
+#define mem_paging_enabled(d) false
+#define put_domain(d) ((void)(d))
+#define clear_page_hot(ptr) memset((ptr), 0, PAGE_SIZE)
+#define clear_page_cold(ptr) memset((ptr), 0, PAGE_SIZE)
+#define unmap_domain_page(ptr) ((void)(ptr))
+#define put_page(pg) ((void)(pg))
+
+void *alloc_xenheap_pages(unsigned int order, unsigned int memflags);
+void init_domheap_pages(paddr_t ps, paddr_t pe);
+struct page_info *alloc_domheap_pages(struct domain *d, unsigned int order,
+ unsigned int memflags);
+
+/* Additional stubs for test support */
+
+unsigned int arch_get_dma_bitsize(void)
+{
+ return 32U;
+}
+
+/* Return number of pages currently posessed by the domain */
+static inline unsigned int domain_tot_pages(const struct domain *d)
+{
+ assert(d->extra_pages <= d->tot_pages);
+ return d->tot_pages - d->extra_pages;
+}
+
+/* LLC (Last Level Cache) coloring support stubs */
+#define llc_coloring_enabled false
+unsigned int get_max_nr_llc_colors(void)
+{
+ return 1U;
+}
+unsigned int page_to_llc_color(const struct page_info *pg)
+{
+ (void)pg;
+ return 0U;
+}
+
+#define parse_bool(s, e) (-1) /* Not parsed, use the default */
+
+void __init register_keyhandler(unsigned char key, keyhandler_fn_t *fn,
+ const char *desc, bool diagnostic)
+{
+ (void)key;
+ (void)fn;
+ (void)desc;
+ (void)diagnostic;
+}
+
+unsigned long simple_strtoul(const char *cp, const char **endp,
+ unsigned int base)
+{
+ return strtoul(cp, (char **)endp, base);
+}
+
+#endif /* TEST_USES_PAGE_ALLOC_SHIM */
+#endif /* _TEST_ALLOC_PAGE_ALLOC_SHIM_ */
--
2.39.5
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH 2/4] tools/tests/alloc: Add integration test suite for memory claims
2026-04-15 17:34 [PATCH 0/4] xen/mm: Host-side unit/integration test framework for page_alloc.c Bernhard Kaindl
2026-04-15 17:34 ` [PATCH 1/4] tools/tests/alloc: Unit and Integration Test Framework " Bernhard Kaindl
@ 2026-04-15 17:34 ` Bernhard Kaindl
2026-04-15 17:34 ` [PATCH 3/4] tools/tests/alloc: Add tests for offlining with claims present Bernhard Kaindl
2026-04-15 17:34 ` [PATCH 4/4] xen/mm: Recall claims when offlining pages if needed Bernhard Kaindl
3 siblings, 0 replies; 7+ messages in thread
From: Bernhard Kaindl @ 2026-04-15 17:34 UTC (permalink / raw)
To: xen-devel; +Cc: Bernhard Kaindl, Anthony PERARD
Add a host-side integration test suite for memory claims, including
NUMA-aware claim sets.
This complements the functional system tests submitted as part of the
NUMA-aware claims series.
It verifies the behaviour of the page allocator when multi-node claim
sets are present in situations that are easier to create and validate
in isolation, with full control over a synthetic Xen heap state and
visibility into the claim state of domains as claims are made and
redeemed through heap allocation.
The included tests cover a range of claim-related scenarios to ensure
that the Xen page allocator behaves as expected.
Signed-off-by: Bernhard Kaindl <bernhard.kaindl@citrix.com>
---
tools/tests/alloc/test-claims_basic.c | 230 ++++++++++++++++++++
tools/tests/alloc/test-claims_numa_redeem.c | 201 +++++++++++++++++
2 files changed, 431 insertions(+)
create mode 100644 tools/tests/alloc/test-claims_basic.c
create mode 100644 tools/tests/alloc/test-claims_numa_redeem.c
diff --git a/tools/tests/alloc/test-claims_basic.c b/tools/tests/alloc/test-claims_basic.c
new file mode 100644
index 000000000000..f81e75876d30
--- /dev/null
+++ b/tools/tests/alloc/test-claims_basic.c
@@ -0,0 +1,230 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Unit tests for memory claims in xen/common/page_alloc.c.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+
+/* Enable sysctl support in page_alloc.c for testing get_outstanding_claims() */
+#define CONFIG_NUMA 1
+#define CONFIG_SYSCTL 1
+#include "libtest-page_alloc.h"
+
+/*
+ * Wrapper for domain_install_claim_set() with the function signature as
+ * domain_set_outstanding_pages() to test both domain_install_claim_set()
+ * and domain_set_outstanding_pages() by using a function pointer for
+ * setting claims to check feature parity and consistent behavior.
+ */
+
+int test_set_global_claims(struct domain *d, unsigned long pages)
+{
+ memory_claim_t claim_set[] = {
+ {.target = XEN_DOMCTL_CLAIM_MEMORY_GLOBAL, .pages = pages},
+ };
+ return domain_install_claim_set(d, ARRAY_SIZE(claim_set), claim_set);
+}
+typedef int (*set_global_claims)(struct domain *d, unsigned long pages);
+
+/*
+ * Function pointer to test both domain_install_claim_set() and
+ * domain_set_outstanding_pages() interchangeably in the test
+ * scenarios for feature parity and consistent behaviour.
+ */
+set_global_claims install_global_claims = test_set_global_claims;
+
+/*
+ * Test that memory claims are consumed correctly during allocations.
+ */
+static void test_alloc_domheap_consumes_claims(int start_mfn)
+{
+ unsigned long avail_pages_zone;
+ int zone, ret;
+ struct page_info *pages = test_pages + start_mfn, *allocated;
+
+ /*
+ * PREPARE
+ */
+
+ /* Create a buddy of order 2 (4 pages) and add it to the heap. */
+ zone = test_page_list_add_buddy(pages, order2);
+
+ /* Verify the initial state of the heap */
+ ASSERT_LIST_EQUAL(&heap(node, zone, order2), pages);
+ ASSERT(page_list_empty(&heap(node, zone, order1)));
+ ASSERT(page_list_empty(&heap(node, zone, order0)));
+ CHECK_BUDDY(pages, "Order-2 buddy prepared on the heap");
+
+ /*
+ * ACT 1
+ */
+
+ /* Claim 3 out of the 4 pages for the dummy domain */
+ ret = test_set_global_claims(dom1, 3);
+ ASSERT(ret == 0);
+
+ /* Allocate an order-1 page for the dummy domain */
+ allocated = alloc_domheap_pages(dom1, order1, 0);
+ CHECK(allocated == &pages[2], "Expect allocation start at 3rd page");
+
+ /*
+ * ASSERT 1
+ *
+ * The allocation is expected to split the order-2 buddy and allocate
+ * an order-1 chunk from it, leaving the remaining order-1 chunk as a free
+ * available pages, and the claim should have been consumed accordingly.
+ */
+
+ /* Verify the state of the heap after allocation */
+ ASSERT(page_list_empty(&heap(node, zone, order2)));
+ ASSERT(page_list_empty(&heap(node, zone, order0)));
+ /* The remaining order-1 chunk should be the first page */
+ ASSERT_LIST_EQUAL(&heap(node, zone, order1), pages);
+ CHECK_BUDDY(pages, "Buddy after order-1 allocation");
+
+ /* Verify the state of the aggregate counters */
+ CHECK(TOTAL_CLAIMS == 1, "Expect 1 claims left after allocation");
+ CHECK(FREE_PAGES == 2, "Expect 2 available after allocation");
+ CHECK(avail_heap_pages(zone, zone, node) == 2, "Expect 2 in zone");
+
+ /*
+ * ACT 2
+ */
+
+ /* Allocate one of the two remaining order-0 pages for the dummy domain */
+ allocated = alloc_domheap_pages(dom1, order0, 0);
+ CHECK(allocated == &pages[1], "alloc_domheap_pages returned the 2nd page");
+
+ /*
+ * ASSERT 2
+ *
+ * The allocation is expected to split the remaining order-1
+ * buddy and allocate an order-0 page from it, leaving the
+ * remaining order-0 page as a free available page, and the
+ * claim should have been consumed accordingly.
+ */
+
+ /* Verify the state of the heap after allocation */
+ ASSERT(page_list_empty(&heap(node, zone, order2)));
+ ASSERT(page_list_empty(&heap(node, zone, order1)));
+ /* The remaining order-0 page should be the only free page we've left */
+ ASSERT_LIST_EQUAL(&heap(node, zone, order0), pages);
+
+ /* Verify the state of the aggregate counters */
+ CHECK(TOTAL_CLAIMS == 0, "Expect all claims consumed after allocation");
+ CHECK(FREE_PAGES == 1, "Expect one free page after allocation");
+
+ avail_pages_zone = avail_heap_pages(zone, zone, node);
+ CHECK(avail_pages_zone == 1, "Expect one page in zone after allocation");
+
+ /*
+ * PREPARE 3
+ */
+
+ /* Claim all free memory from another domain to block allocations */
+ ret = test_set_global_claims(dom2, FREE_PAGES);
+ ASSERT(ret == 0);
+
+ /*
+ * ACT 3
+ */
+
+ /* Claim more than dom1 already has fails with ENOMEM (claimed by dom2) */
+ ret = test_set_global_claims(dom1, domain_tot_pages(dom1) + 1);
+ CHECK(ret == -ENOMEM, "dom 1 claim +1 fails due to insufficient pages");
+
+ /* Claim more than dom1's d->max_pages fails with EINVAL */
+ ret = test_set_global_claims(dom1, dom1->max_pages + 1);
+ CHECK(ret == -EINVAL, "dom 1 claim fails due to exceeding max_pages");
+
+ /* Attempt to allocate an order-0 page with a foreign claim present */
+ allocated = alloc_domheap_pages(dom1, order0, 0);
+ CHECK(allocated == NULL, "dom 1 alloc fails b/c domain 2's claim");
+
+ /*
+ * ASSERT 3
+ */
+
+ /* Verify the state of the heap after failed allocation (no changes) */
+ ASSERT(page_list_empty(&heap(node, zone, order2)));
+ ASSERT(page_list_empty(&heap(node, zone, order1)));
+ /* Due to the foreign claim, the remaining page should still be free */
+ ASSERT_LIST_EQUAL(&heap(node, zone, order0), pages);
+
+ /* Verify the state of the aggregate counters (no changes expected) */
+ CHECK(TOTAL_CLAIMS == 1, "Expect domain 2's claim to be still present");
+ CHECK(FREE_PAGES == 1, "Expect one free page after failed alloc");
+
+ avail_pages_zone = avail_heap_pages(zone, zone, node);
+ CHECK(avail_pages_zone == 1, "Expect one page in zone after allocation");
+}
+
+/*
+ * Test that memory claims are consumed correctly during allocations.
+ */
+static void test_cancel_claims(int start_mfn)
+{
+ struct page_info *page = test_pages + start_mfn;
+ unsigned long claims;
+
+ /* Create a buddy of order 2 (4 pages) and add it to the heap. */
+ test_page_list_add_buddy(page, order2);
+ claims = FREE_PAGES / 2;
+ /* Claim half of the free pages for dom1 */
+ ASSERT(test_set_global_claims(dom1, claims) == 0);
+ ASSERT(TOTAL_CLAIMS == claims);
+
+ /*
+ * Act: Cancel the claims for the dummy domain and verify that the
+ * claim counts are updated and the free pages are available again.
+ */
+
+ /* Act + Assert 2: Claim all free pages for dom2, should fail */
+ ASSERT(test_set_global_claims(dom2, FREE_PAGES) == -ENOMEM);
+ ASSERT(TOTAL_CLAIMS == claims);
+
+ /* Act + Assert 1: Cancel all claims for dom1 */
+ ASSERT(test_set_global_claims(dom1, 0) == 0);
+ ASSERT(TOTAL_CLAIMS == 0);
+
+ /* Act + Assert 2: Claim all free pages for dom2, should work */
+ ASSERT(test_set_global_claims(dom2, FREE_PAGES) == 0);
+ ASSERT(TOTAL_CLAIMS == FREE_PAGES);
+}
+
+int main(int argc, char *argv[])
+{
+ const char *topic = "Test legacy claims with allocation from the heap";
+ const char *program_name = parse_args(argc, argv, topic);
+
+ if ( !program_name )
+ return EXIT_FAILURE;
+
+ init_page_alloc_tests();
+
+ /* Use domain_set_outstanding_pages() for staking claims */
+ install_global_claims = domain_set_outstanding_pages;
+ RUN_TESTCASE(ADCL, test_alloc_domheap_consumes_claims, 0);
+
+ /*
+ * Use test_set_global_claims() which is a wrapper around
+ * domain_install_claim_set() to check ensure consistent
+ * behavior with domain_set_outstanding_pages().
+ */
+ install_global_claims = test_set_global_claims;
+ RUN_TESTCASE(ADCG, test_alloc_domheap_consumes_claims, 4);
+
+ RUN_TESTCASE(TCCL, test_cancel_claims, 0);
+
+ testcase_print_summary(program_name);
+ return 0;
+}
+
+/*
+ * Local variables:
+ * mode: C
+ * c-file-style: "BSD"
+ * c-basic-offset: 4
+ * indent-tabs-mode: nil
+ * End:
+ */
diff --git a/tools/tests/alloc/test-claims_numa_redeem.c b/tools/tests/alloc/test-claims_numa_redeem.c
new file mode 100644
index 000000000000..61bec12be1c0
--- /dev/null
+++ b/tools/tests/alloc/test-claims_numa_redeem.c
@@ -0,0 +1,201 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Integration tests for redeeming NUMA memory claim set as implemented
+ * in xen/common/page_alloc.c's redeem_claims_for_allocation() and
+ * related functions.
+ *
+ * redeem_claims_for_allocation() is exercised indirectly through
+ * alloc_domheap_pages() which is the primary interface for allocating
+ * pages from a domain's heap.
+ *
+ * By means of domain_install_claim_set(), a claim set with global and
+ * per-NUMA-node claims is installed for a dummy domain, and then
+ * allocations with NUMA node affinity are performed to verify that the
+ * appropriate claims are redeemed (same-node first, global fallback next,
+ * then other nodes to not exceed page limits). The test also verifies that
+ * aggregate counters are updated correctly after each allocation.
+ *
+ * The test verifies that when a domain has a claim set installed with
+ * global and per-NUMA-node claims, allocations that specify NUMA node
+ * affinity will redeem the appropriate claims (same-node first, global
+ * fallback claim next, then other nodes to not exceed page limits).
+ * It also verifies that the aggregate claim counters are updated
+ * correctly after each allocation.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+
+#define CONFIG_NUMA 1
+#define CONFIG_SYSCTL 1
+#include "libtest-page_alloc.h"
+
+/*
+ * Test redeeming NUMA memory claims in exchange for allocations,
+ * and the redeemed claims are correctly reflected in the domain's
+ * claim state and the aggregate claim counters.
+ */
+static void test_claims_numa_install(int start_mfn)
+{
+ unsigned long avail_pages_zone;
+ int zone, ret;
+ struct page_info *pages = test_pages + start_mfn, *allocated;
+
+ /*
+ * PREPARE
+ */
+
+ /*
+ * Node 1's pages start at the pfn set by init_numa_node_data():
+ * node_data[node1].node_start_pfn = start_mfn + 8 (8 MFNs per node with
+ * memnode_shift=3). The order-2 buddy (4 pages) placed there satisfies
+ * the 2-page node1 claim and provides enough total pages for the
+ * 2 global + 2 node0 + 2 node1 = 6-page claim set (2 + 4 = 6 total).
+ */
+ struct page_info *pages_node1 =
+ test_pages + node_data[node1].node_start_pfn;
+
+ /* Create an order-1 buddy (2 pages) for node 0 and add it to the heap. */
+ zone = test_page_list_add_buddy(pages, order1);
+
+ /* Verify the initial state of node 0's heap. */
+ ASSERT_LIST_EQUAL(&heap(node0, zone, order1), pages);
+ ASSERT(page_list_empty(&heap(node0, zone, order0)));
+ CHECK_BUDDY(pages, "Order-1 buddy on node 0 prepared");
+
+ /* Create an order-2 buddy (4 pages) for node 1 and add it to the heap. */
+ test_page_list_add_buddy(pages_node1, order2);
+ CHECK_BUDDY(pages_node1, "Order-2 buddy on node 1 prepared");
+
+ /*
+ * ACT 1
+ */
+
+ /* Install a claim set with global + per-NUMA-node claims. */
+ memory_claim_t claim_set[] = {
+ {.target = XEN_DOMCTL_CLAIM_MEMORY_GLOBAL, .pages = 2},
+ {.target = node0, .pages = 2},
+ {.target = node1, .pages = 2},
+ };
+ ret = domain_install_claim_set(dom1, ARRAY_SIZE(claim_set), claim_set);
+ CHECK(ret == 0, "domain_install_claim_set should succeed: %d", ret);
+
+ /* Assert dom1's claims */
+ CHECK(TOTAL_CLAIMS == 6, "Expect 6 total claims after installation");
+ CHECK(DOM_GLOBAL_CLAIMS(dom1) == 2,
+ "Expect dom1 having 2 global claims after installation");
+ CHECK(DOM_NODE_CLAIMS(dom1, node0) == 2,
+ "Expect dom1 having 2 claims for node0 after installation");
+ CHECK(DOM_NODE_CLAIMS(dom1, node1) == 2,
+ "Expect dom1 having 2 claims for node1 after installation");
+
+ /* Allocate an order-0 page from node 0 for the dummy domain. */
+ allocated = alloc_domheap_pages(dom1, order0, MEMF_node(node0));
+ CHECK(allocated != NULL, "alloc_domheap_pages should succeed");
+
+ /*
+ * ASSERT 1
+ *
+ * The order-0 allocation from node 0 splits the node 0 order-1 buddy:
+ * - The lower half (pages[0]) stays on node 0's order-0 heap.
+ * - The upper half (pages[1]) is returned as the allocated page.
+ * One node 0 claim is consumed by the allocation.
+ */
+ CHECK_BUDDY(pages, "Buddy after order-0 allocation");
+ /* Verify the state of node 0's heap after allocation. */
+ ASSERT(page_list_empty(&heap(node0, zone, order2)));
+ ASSERT(page_list_empty(&heap(node0, zone, order1)));
+ /* The lower half (pages[0]) remains as the sole order-0 buddy on node 0. */
+ ASSERT_LIST_EQUAL(&heap(node0, zone, order0), pages);
+
+ avail_pages_zone = avail_heap_pages(zone, zone, node0);
+ CHECK(avail_pages_zone == 1, "Expect one page in node0 after allocation");
+
+ /* Verify the state of the aggregate counters after allocation. */
+ CHECK(TOTAL_CLAIMS == 5, "Expect 5 total claims left after allocation");
+ CHECK(FREE_PAGES == 5, "Expect 5 free pages left after allocation");
+
+ /* Assert dom1's claims after the allocation from node0 */
+ CHECK(DOM_GLOBAL_CLAIMS(dom1) == 2,
+ "Expect dom1 still having 2 global claims after allocation");
+ CHECK(DOM_NODE_CLAIMS(dom1, node0) == 1,
+ "Expect dom1 having 1 claim for node0 after allocation");
+ CHECK(DOM_NODE_CLAIMS(dom1, node1) == 2,
+ "Expect dom1 still having 2 claims for node1 after allocation");
+
+ /* Allocate an order-0 page from node 1 for the dummy domain. */
+ allocated = alloc_domheap_pages(dom1, order0, MEMF_node(node1));
+ CHECK(allocated != NULL, "order-0 alloc from node1");
+
+ /* Assert dom1's claims after the allocation from node1 */
+ CHECK(DOM_GLOBAL_CLAIMS(dom1) == 2,
+ "Expect dom1 still having 2 global claims after allocation");
+ CHECK(DOM_NODE_CLAIMS(dom1, node0) == 1,
+ "Expect dom1 having 1 claim for node0 after allocation");
+ CHECK(DOM_NODE_CLAIMS(dom1, node1) == 1,
+ "Expect dom1 having 1 claim for node1 after allocation");
+
+ /* Allocate an order-1 page from node 1 for the dummy domain. */
+ allocated = alloc_domheap_pages(dom1, order1, MEMF_node(node1));
+ CHECK(allocated != NULL, "order-1 alloc from node1");
+
+ /* Assert dom1's claims after the allocation from node1 */
+ CHECK(DOM_GLOBAL_CLAIMS(dom1) == 1,
+ "Expect dom1 having redeemed one global claim after allocation");
+ CHECK(DOM_NODE_CLAIMS(dom1, node0) == 1,
+ "Expect dom1 having 1 claim for node0 after allocation");
+ CHECK(DOM_NODE_CLAIMS(dom1, node1) == 0,
+ "Expect dom1 having 0 claims for node1 after allocation");
+
+ /* Allocate an order-0 page from node 1 for the dummy domain. */
+ allocated = alloc_domheap_pages(dom1, order0, MEMF_node(node1));
+ CHECK(allocated != NULL, "order-0 alloc from node1");
+
+ /* Assert dom1's claims after the allocation from node1 */
+ CHECK(DOM_GLOBAL_CLAIMS(dom1) == 0,
+ "Expect dom1 having redeemed one global claim after allocation");
+ CHECK(DOM_NODE_CLAIMS(dom1, node0) == 1,
+ "Expect dom1 having 1 claim for node0 after allocation");
+ CHECK(DOM_NODE_CLAIMS(dom1, node1) == 0,
+ "Expect dom1 having 0 claims for node1 after allocation");
+
+ /* Allocate an order-0 page from node 1 for the dummy domain. */
+ allocated = alloc_domheap_pages(dom1, order0, MEMF_node(node1));
+ CHECK(allocated != NULL, "order-0 alloc from node1");
+
+ /* Assert dom1's claims after the allocation from node1 */
+ CHECK(DOM_GLOBAL_CLAIMS(dom1) == 0,
+ "Expect dom1 having redeemed one global claim after allocation");
+ CHECK(DOM_NODE_CLAIMS(dom1, node0) == 0,
+ "Expect dom1 having 0 claims for node0 after allocation");
+ CHECK(DOM_NODE_CLAIMS(dom1, node1) == 0,
+ "Expect dom1 having 0 claims for node1 after allocation");
+}
+
+int main(int argc, char *argv[])
+{
+ const char *topic = "Test legacy claims with allocation from the heap";
+ const char *program_name = parse_args(argc, argv, topic);
+
+ if ( !program_name )
+ return EXIT_FAILURE;
+
+ init_page_alloc_tests();
+ /*
+ * Use test_set_global_claims() which is a wrapper around
+ * domain_install_claim_set() to check ensure consistent
+ * behavior with domain_set_outstanding_pages().
+ */
+ RUN_TESTCASE(CNI0, test_claims_numa_install, 0);
+
+ testcase_print_summary(program_name);
+ return 0;
+}
+
+/*
+ * Local variables:
+ * mode: C
+ * c-file-style: "BSD"
+ * c-basic-offset: 4
+ * indent-tabs-mode: nil
+ * End:
+ */
--
2.39.5
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH 3/4] tools/tests/alloc: Add tests for offlining with claims present
2026-04-15 17:34 [PATCH 0/4] xen/mm: Host-side unit/integration test framework for page_alloc.c Bernhard Kaindl
2026-04-15 17:34 ` [PATCH 1/4] tools/tests/alloc: Unit and Integration Test Framework " Bernhard Kaindl
2026-04-15 17:34 ` [PATCH 2/4] tools/tests/alloc: Add integration test suite for memory claims Bernhard Kaindl
@ 2026-04-15 17:34 ` Bernhard Kaindl
2026-04-15 17:34 ` [PATCH 4/4] xen/mm: Recall claims when offlining pages if needed Bernhard Kaindl
3 siblings, 0 replies; 7+ messages in thread
From: Bernhard Kaindl @ 2026-04-15 17:34 UTC (permalink / raw)
To: xen-devel; +Cc: Bernhard Kaindl, Anthony PERARD
Add an integration test for offlining pages with outstanding claims.
The test offlines two pages, with the second offline operation
recalling one claim to prevent over-claiming beyond the available
memory. Due to missing checks in the offlining code, there are
expected failures which will be fixed in a test-driven manner.
Run this test with both global claims and node-local claims.
Signed-off-by: Bernhard Kaindl <bernhard.kaindl@citrix.com>
---
tools/tests/alloc/test-offlining-claims.c | 106 ++++++++++++++++++++++
1 file changed, 106 insertions(+)
create mode 100644 tools/tests/alloc/test-offlining-claims.c
diff --git a/tools/tests/alloc/test-offlining-claims.c b/tools/tests/alloc/test-offlining-claims.c
new file mode 100644
index 000000000000..d22d270ceeb4
--- /dev/null
+++ b/tools/tests/alloc/test-offlining-claims.c
@@ -0,0 +1,106 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Integration test for offlining pages with outstanding claims in page_alloc.c.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+
+/* Enable sysctl support in page_alloc.c for testing get_outstanding_claims() */
+#define CONFIG_SYSCTL
+#define CONFIG_NUMA
+#include "libtest-page_alloc.h"
+
+/*
+ * Claim 3 of 4 pages globally, offline two pages, and the 2nd offline should
+ * recall one claim to prevent over-claiming beyond the available memory.
+ *
+ * As part of offline_page(), reserve_offlined_page() should recall the
+ * needed claims to not exceed the number of pages that are are remaining.
+ */
+static void test_offlining_with_global_claims(int mfn)
+{
+ struct page_info *page = test_pages + mfn;
+ uint32_t status = 0;
+ memory_claim_t claims[1] = {
+ {.pages = 3, .target = XEN_DOMCTL_CLAIM_MEMORY_GLOBAL}
+ };
+
+ /* PREPARE */
+ test_page_list_add_buddy(page, order2); /* Add a buddy with 4 free pages*/
+ ASSERT(domain_install_claim_set(dom1, ARRAY_SIZE(claims), claims) == 0);
+ offline_page(mfn + 3, 0, &status); /* Offline the 1st page */
+ ASSERT(status & PG_OFFLINE_OFFLINED);
+ CHECK(TOTAL_CLAIMS == 3, "Still 3 claims before offlining the 2nd page");
+
+ /* ACT */
+ /* Offlining the 2nd page must reduce the free pages and claims to 2 */
+ status = 0;
+ offline_page(mfn + 1, 0, &status); /* Offline the 2nd page */
+ ASSERT(status & PG_OFFLINE_OFFLINED);
+
+ /* ASSERT */
+ CHECK_BUDDY(page, "After offlining the 2nd page");
+ CHECK(FREE_PAGES == 2, "Expect 2 free pages after offlining two pages");
+ EXPECTED_TO_FAIL_BEGIN();
+ CHECK(TOTAL_CLAIMS == 2, "Expect 2 claims after offlining two pages");
+ EXPECTED_TO_FAIL_END(1);
+}
+
+
+/*
+ * Claim 3 of 4 pages on node0, offline two pages, and the 2nd offline should
+ * recall one claim to prevent over-claiming beyond the available memory.
+ *
+ * As part of offline_page(), reserve_offlined_page() should recall the
+ * needed claims to not exceed the number of pages that are are remaining.
+ */
+static void test_offlining_with_node_claims(int mfn)
+{
+ struct page_info *page = test_pages + mfn;
+ uint32_t status = 0;
+ memory_claim_t claims[1] = { {.pages = 3, .target = node0} };
+
+ /* PREPARE */
+ test_page_list_add_buddy(page, order2); /* Add a buddy with 4 free pages*/
+ ASSERT(domain_install_claim_set(dom1, ARRAY_SIZE(claims), claims) == 0);
+ ASSERT(offline_page(mfn + 3, 0, &status) == 0); /* Offline the 1st page */
+ ASSERT(status & PG_OFFLINE_OFFLINED);
+ CHECK(TOTAL_CLAIMS == 3, "Still 3 claims before offlining the 2nd page");
+
+ /* ACT */
+ /* Offlining the 2nd page must reduce the free pages and claims to 2 */
+ ASSERT(offline_page(mfn + 1, 0, &status) == 0); /* Offline the 2nd page */
+ ASSERT(status & PG_OFFLINE_OFFLINED);
+
+ /* ASSERT */
+ CHECK_BUDDY(page, "After offlining the 2nd page");
+ CHECK(FREE_PAGES == 2, "Expect 2 free pages after offlining two pages");
+ EXPECTED_TO_FAIL_BEGIN();
+ CHECK(TOTAL_CLAIMS == 2, "Expect 2 claims after offlining two pages");
+ EXPECTED_TO_FAIL_END(1);
+}
+
+int main(int argc, char *argv[])
+{
+ const char *topic = "Test offlining with memory claims";
+ const char *program_name = parse_args(argc, argv, topic);
+
+ if ( !program_name )
+ return EXIT_FAILURE;
+
+ init_page_alloc_tests();
+
+ RUN_TESTCASE(OWGC, test_offlining_with_global_claims, 0);
+ RUN_TESTCASE(OWNC, test_offlining_with_node_claims, 0);
+
+ return testcase_print_summary(program_name);
+}
+
+/*
+ * Local variables:
+ * mode: C
+ * c-file-style: "BSD"
+ * c-basic-offset: 4
+ * indent-tabs-mode: nil
+ * End:
+ */
--
2.39.5
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH 4/4] xen/mm: Recall claims when offlining pages if needed
2026-04-15 17:34 [PATCH 0/4] xen/mm: Host-side unit/integration test framework for page_alloc.c Bernhard Kaindl
` (2 preceding siblings ...)
2026-04-15 17:34 ` [PATCH 3/4] tools/tests/alloc: Add tests for offlining with claims present Bernhard Kaindl
@ 2026-04-15 17:34 ` Bernhard Kaindl
3 siblings, 0 replies; 7+ messages in thread
From: Bernhard Kaindl @ 2026-04-15 17:34 UTC (permalink / raw)
To: xen-devel
Cc: Bernhard Kaindl, Anthony PERARD, Andrew Cooper, Michal Orzel,
Jan Beulich, Julien Grall, Roger Pau Monné,
Stefano Stabellini
Fix a bug where offlining pages could cause an unsigned underflow
in total_avail_pages - outstanding_claims, leading to incorrect
claim behavior.
This issue arises when outstanding claims are close to the total
available pages. It occurse when domain_set_outstanding_claims()
and domain_install_claim_set effectively do this:
unsigned long avail_pages = total_avail_pages - outstanding_claims;
When this unsigned subtraction underflows, staking claims can succeed
even when there is insufficient unclaimed memory for the new claim.
This leads to a state where claims always succeed, regardless of
actual memory availability.
To prevent this, recall claims when offlining pages if needed to maintain
equilibrium between `total_avail_pages` and outstanding claims for global
and for per-NUMA-node claims.
Signed-off-by: Bernhard Kaindl <bernhard.kaindl@citrix.com>
---
tools/tests/alloc/test-offlining-claims.c | 4 ---
xen/common/page_alloc.c | 42 +++++++++++++++++++++++
2 files changed, 42 insertions(+), 4 deletions(-)
diff --git a/tools/tests/alloc/test-offlining-claims.c b/tools/tests/alloc/test-offlining-claims.c
index d22d270ceeb4..0844c792e740 100644
--- a/tools/tests/alloc/test-offlining-claims.c
+++ b/tools/tests/alloc/test-offlining-claims.c
@@ -41,9 +41,7 @@ static void test_offlining_with_global_claims(int mfn)
/* ASSERT */
CHECK_BUDDY(page, "After offlining the 2nd page");
CHECK(FREE_PAGES == 2, "Expect 2 free pages after offlining two pages");
- EXPECTED_TO_FAIL_BEGIN();
CHECK(TOTAL_CLAIMS == 2, "Expect 2 claims after offlining two pages");
- EXPECTED_TO_FAIL_END(1);
}
@@ -75,9 +73,7 @@ static void test_offlining_with_node_claims(int mfn)
/* ASSERT */
CHECK_BUDDY(page, "After offlining the 2nd page");
CHECK(FREE_PAGES == 2, "Expect 2 free pages after offlining two pages");
- EXPECTED_TO_FAIL_BEGIN();
CHECK(TOTAL_CLAIMS == 2, "Expect 2 claims after offlining two pages");
- EXPECTED_TO_FAIL_END(1);
}
int main(int argc, char *argv[])
diff --git a/xen/common/page_alloc.c b/xen/common/page_alloc.c
index 6101bd6be9a9..adedf6fae590 100644
--- a/xen/common/page_alloc.c
+++ b/xen/common/page_alloc.c
@@ -1575,6 +1575,48 @@ static int reserve_offlined_page(struct page_info *head)
count++;
}
+ if ( count )
+ {
+ long recall_pages;
+ struct domain *d;
+
+ /* Ensure that claims on the node are in line with its free memory. */
+ recall_pages = node_outstanding_claims[node] - node_avail_pages[node];
+ if ( recall_pages > 0 )
+ /*
+ * node_avail_pages slipped below node_outstanding_claims.
+ * We need to recall claimed pages until the amount of claimed
+ * memory is in line with the amount of available memory again.
+ */
+ for_each_domain ( d )
+ {
+ if ( d->claims[node] )
+ {
+ recall_pages -= deduct_node_claims(d, node, recall_pages);
+ if ( recall_pages <= 0 )
+ break;
+ }
+ }
+
+ /* Ensure that outstanding claims are in line with available memory. */
+ recall_pages = outstanding_claims - total_avail_pages;
+ if ( recall_pages > 0 )
+ /*
+ * total_avail_pages slipped below outstanding_claims.
+ * We need to recall claimed pages until the amount of claimed
+ * memory is in line with the amount of available memory again.
+ */
+ for_each_domain ( d )
+ {
+ if ( d->global_claims )
+ {
+ recall_pages -= deduct_global_claims(d, recall_pages);
+ if ( recall_pages <= 0 )
+ break;
+ }
+ }
+ }
+
return count;
}
--
2.39.5
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH 1/4] tools/tests/alloc: Unit and Integration Test Framework for page_alloc.c
2026-04-15 17:34 ` [PATCH 1/4] tools/tests/alloc: Unit and Integration Test Framework " Bernhard Kaindl
@ 2026-04-16 7:39 ` Jan Beulich
2026-04-18 17:11 ` Bernhard Kaindl
0 siblings, 1 reply; 7+ messages in thread
From: Jan Beulich @ 2026-04-16 7:39 UTC (permalink / raw)
To: Bernhard Kaindl; +Cc: Anthony PERARD, xen-devel
On 15.04.2026 19:34, Bernhard Kaindl wrote:
> Add a test framefork for unit and integration test suites testing
> the Xen page allocator module xen/common/page_alloc.c in isolation.
>
> It enables test suites to verify the behaviour of the page allocator
> in situations that are easier to create and validate in isolation,
> with full control over a synthetic Xen heap state and visibility
> into the allocator and domain state.
>
> Signed-off-by: Bernhard Kaindl <bernhard.kaindl@citrix.com>
Just two remarks (nits?) for now:
> ---
> tools/tests/Makefile | 1 +
> tools/tests/alloc/.gitignore | 6 +
> tools/tests/alloc/Makefile | 141 ++++++++
> tools/tests/alloc/README.rst | 31 ++
> tools/tests/alloc/check-asserts.h | 347 ++++++++++++++++++
> tools/tests/alloc/harness.h | 69 ++++
> tools/tests/alloc/hypervisor-macros.h | 101 ++++++
> tools/tests/alloc/libtest-page_alloc.h | 356 +++++++++++++++++++
> tools/tests/alloc/mock-page_list.h | 307 ++++++++++++++++
> tools/tests/alloc/page_alloc-wrapper.h | 465 +++++++++++++++++++++++++
> tools/tests/alloc/page_alloc_shim.h | 433 +++++++++++++++++++++++
> 11 files changed, 2257 insertions(+)
This is a lot of new code.
> create mode 100644 tools/tests/alloc/.gitignore
> create mode 100644 tools/tests/alloc/Makefile
> create mode 100644 tools/tests/alloc/README.rst
> create mode 100644 tools/tests/alloc/check-asserts.h
> create mode 100644 tools/tests/alloc/harness.h
> create mode 100644 tools/tests/alloc/hypervisor-macros.h
> create mode 100644 tools/tests/alloc/libtest-page_alloc.h
> create mode 100644 tools/tests/alloc/mock-page_list.h
> create mode 100644 tools/tests/alloc/page_alloc-wrapper.h
> create mode 100644 tools/tests/alloc/page_alloc_shim.h
No underscores please in new files' names when dashes will do.
Jan
^ permalink raw reply [flat|nested] 7+ messages in thread
* RE: [PATCH 1/4] tools/tests/alloc: Unit and Integration Test Framework for page_alloc.c
2026-04-16 7:39 ` Jan Beulich
@ 2026-04-18 17:11 ` Bernhard Kaindl
0 siblings, 0 replies; 7+ messages in thread
From: Bernhard Kaindl @ 2026-04-18 17:11 UTC (permalink / raw)
To: Jan Beulich; +Cc: Anthony PERARD, xen-devel@lists.xenproject.org
> > Add a test framefork for unit and integration test suites testing
> > the Xen page allocator module xen/common/page_alloc.c in isolation.
> Just two remarks (nits?) for now:
[...]
> > tools/tests/alloc/hypervisor-macros.h | 101 ++++++
> > tools/tests/alloc/libtest-page_alloc.h | 356 +++++++++++++++++++
> > tools/tests/alloc/mock-page_list.h | 307 ++++++++++++++++
> > tools/tests/alloc/page_alloc-wrapper.h | 465 +++++++++++++++++++++++++
> > tools/tests/alloc/page_alloc_shim.h | 433 +++++++++++++++++++++++
> > 11 files changed, 2257 insertions(+)
>
> This is a lot of new code.
Ack, improved for v2. It will be reduced to 1411 lines, 60% smaller.
Changes for v2:
1. Refactor the test environment for minimalism:
- Improve separation of concerns.
- Move extensions to later patch series.
- Simplify code with targeted assertions and concise comments.
2. Fix file names to avoid underscores.
3. Update the include guards to comply with CODING_STYLE.
Bernhard
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-04-18 17:11 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-15 17:34 [PATCH 0/4] xen/mm: Host-side unit/integration test framework for page_alloc.c Bernhard Kaindl
2026-04-15 17:34 ` [PATCH 1/4] tools/tests/alloc: Unit and Integration Test Framework " Bernhard Kaindl
2026-04-16 7:39 ` Jan Beulich
2026-04-18 17:11 ` Bernhard Kaindl
2026-04-15 17:34 ` [PATCH 2/4] tools/tests/alloc: Add integration test suite for memory claims Bernhard Kaindl
2026-04-15 17:34 ` [PATCH 3/4] tools/tests/alloc: Add tests for offlining with claims present Bernhard Kaindl
2026-04-15 17:34 ` [PATCH 4/4] xen/mm: Recall claims when offlining pages if needed Bernhard Kaindl
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.