From: "Kiryl Shutsemau (Meta)" <[email protected]> A GUP pin holds a reference nobody else can account for, so a pinned page cannot be collapsed. The freeze needs the folio's reference count to match what its mappings and cache membership explain, and a pin makes it not.
The window holding one has to be refused. The windows beside it should not be, as with a lazyfree page. Pin one page for the duration of a khugepaged pass through gup_test, which makes the refusal deterministic instead of a race. Check the same three things the lazyfree case does: the rest of the table collapsed, that window not, and the pinned page still backed by an order-0 folio, since selection descends orders on a refusal. Skipped without CONFIG_GUP_TEST or the privilege to use it. Assisted-by: Claude-Code:claude-opus-5 Signed-off-by: Kiryl Shutsemau (Meta) <[email protected]> --- tools/testing/selftests/mm/khugepaged.c | 64 +++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tools/testing/selftests/mm/khugepaged.c b/tools/testing/selftests/mm/khugepaged.c index 58cb7364652e..bd684cd25fed 100644 --- a/tools/testing/selftests/mm/khugepaged.c +++ b/tools/testing/selftests/mm/khugepaged.c @@ -21,7 +21,9 @@ #include "linux/magic.h" +#include <sys/ioctl.h> #include "vm_util.h" +#include "../../../../mm/gup_test.h" #include "hugepage_settings.h" #define BASE_ADDR ((void *)(1UL << 30)) @@ -1728,6 +1730,67 @@ static void collapse_order_lazyfree_window(struct collapse_context *c, ksft_test_result_report(exit_status, "%s\n", __func__); } +/* + * A GUP pin on one page holds a reference the collapse cannot account for, so + * khugepaged must leave the window holding it alone -- and collapse the rest. + * The pin is held across the whole khugepaged pass, which is what makes the + * refusal deterministic rather than a race. + */ +static void collapse_order_pinned_window(struct collapse_context *c, + struct mem_ops *ops) +{ + struct pin_longterm_test pin = {}; + size_t window = mthp_window_size(); + void *p; + int fd; + + fd = open("/sys/kernel/debug/gup_test", O_RDWR); + if (fd < 0) { + ksft_test_result_skip("%s: gup_test needs CONFIG_GUP_TEST and root\n", + __func__); + return; + } + + mthp_push_target_order(); + + p = ops->setup_area(1); + ops->fault(p, 0, hpage_pmd_size); + if (!window_not_collapsed(p, hpage_pmd_size)) + ksft_exit_fail_msg("Unexpected large folio after fault\n"); + + pin.addr = (__u64)(unsigned long)p; + pin.size = page_size; + pin.flags = PIN_LONGTERM_TEST_FLAG_USE_WRITE; + if (ioctl(fd, PIN_LONGTERM_TEST_START, &pin)) { + ops->cleanup_area(p, hpage_pmd_size); + close(fd); + thp_pop_settings(); + ksft_test_result_skip("%s: cannot pin\n", __func__); + return; + } + + madvise(p, hpage_pmd_size, MADV_HUGEPAGE); + ksft_print_msg("Collapse the windows beside a pinned page..."); + if (!khugepaged_wait_full_pass()) + fail("Timeout"); + else if (window_collapsed(p + window, hpage_pmd_size - window) && + window_not_collapsed(p, window) && + /* Left alone at every order, not just the target one */ + is_range_backed_by_folio_orders(p, page_size, 0, + pagemap_fd, kpageflags_fd)) + success("OK"); + else + fail("Fail"); + + ioctl(fd, PIN_LONGTERM_TEST_STOP); + close(fd); + + validate_memory(p, 0, hpage_pmd_size); + ops->cleanup_area(p, hpage_pmd_size); + thp_pop_settings(); + ksft_test_result_report(exit_status, "%s\n", __func__); +} + static void usage(void) { fprintf(stderr, "\nUsage: ./khugepaged [OPTIONS] <test type> [dir]\n\n"); @@ -2020,6 +2083,7 @@ int main(int argc, char **argv) TEST(collapse_order_sub_pmd_holes, mthp_khugepaged_context, anon_ops); TEST(collapse_order_mlocked, mthp_khugepaged_context, anon_ops); TEST(collapse_order_lazyfree_window, mthp_khugepaged_context, anon_ops); + TEST(collapse_order_pinned_window, mthp_khugepaged_context, anon_ops); } TEST(collapse_full, madvise_context, anon_ops); -- 2.54.0
