Skip to contentSkip to conversation

mm/page_owner: add per-fd filter infrastructure for print_mode and NUMA filtering

PATCH Superseded Zhen Ni 3 participants 11 messages May 25 – Jun 23

Subsystems (4)

v9 of 12 Superseded Latest is v12 →

lore
Share & tools
lore

Zhen Ni

author
#0 lore raw

v9 cover mm/page_owner: add per-fd filter infrastructure for print_mode and NUMA filtering

This patch series introduces per-file-descriptor filtering capabilities to the page_owner feature.

Changes in v9:
- Add spinlock_t lock to struct page_owner_filter_state
- Use memdesc_nid() instead of page_to_nid()
- Fix isdigit() usage
- Optimize I/O performance

Changes in v8:
- Fix buffer overflow, strsep() memory corruption, and unsafe string handling issues
- Add cond_resched() to prevent RCU stalls in page iteration loop
- Improve validation and error handling (e.g., "1-2-3") in userspace tool
- Fix documentation warnings and improve code comments

v8 additional testing with invalid inputs:
./page_owner_filter -n 1-2-3
Error: Multiple dashes in nid_list

./page_owner_filter -n 0,1-2-3
Error: Multiple dashes in nid_list

./page_owner_filter -n 1-2-3,2-3
Error: Multiple dashes in nid_list

Changes in v7:
- print_mode and NUMA node filter implementation (patches 1-2)
- Add page_owner_filter userspace tool (patch 3)
- Update documentation for per-fd interface (patch 4)

Changes in v6:
- Address SeongJae Park's review comments for patch 1/3:
* Remove unnecessary braces in if/else statement
* Use stack array instead of kmalloc for input buffer
- Address SeongJae Park's review comments for patch 2/3:
* Add node validity check using nodes_subset() to reject non-existent nodes
* Separate variable declaration and statement
* Use kmalloc_objs() for consistency with kernel patterns
* Remove 100 bytes overhead
- Add lore links to all previous versions

Changes in v5:
- Optimize nodes_empty() check in page iteration loop
- Add __data_racy qualifier to nid_mask field

Changes in v4:
- Change print_mode from numeric (0/1) to string-based interface
* Use "full_stack"/"stack_handle" strings instead of numbers
* Display current mode with bracket notation: "[full_stack] stack_handle"
- Remove "-1" support from NUMA filter
* Use empty string to clear filter (echo > nid)
- Use strncpy_from_user() instead of copy_from_user()
- Rename nid_filter_fops to page_owner_nid_filter_fops for consistency
- Merge patch 1 (infrastructure) and patch 2 (print_mode) from v3
- Update documentation to match new interface
* String-based examples
* Tab indentation in code blocks

Changes in v3:
- Remove READ_ONCE/WRITE_ONCE for nodemask_t (fixes compilation errors)
* nodemask_t is a large structure (128 bytes) that triggers compile-time asserts
* Direct assignment is safe for this use case
- Add comment explaining input length calculation formula
* 6 bytes = ",NNNNN" (comma + 5-digit node number)
- Simplify "-1" check using kstrtoint() instead of dual strcmp()
- Move nodemask_t mask read outside PFN iteration loop for performance
* Avoids 128-byte structure copy on each iteration
- Add documentation for filter features (patch 3/3)

Changes in v2:
- Renamed 'compact' to 'print_mode' with enum type for better clarity
* PAGE_OWNER_PRINT_FULL_STACK (0): print full stack traces
* PAGE_OWNER_PRINT_STACK_HANDLE (1): print only stack handles
- Changed NUMA filter from single node to nodelist with bitmask support
* Uses nodelist_parse() to support "0", "0,2", "0-3", "0,2-4,7" formats
* Uses nodemask_t internally for efficient multi-node filtering
* Output uses %*pbl format (e.g., "0-2", "0,2-4,7")
- Improved memory handling in nid_filter_write using dynamic allocation
* Limit: (100 + 6 * MAX_NUMNODES) to handle worst-case input

Problem Statement
=================

In production environments with large memory configurations (e.g., 250GB+), collecting page_owner information often results in files ranging from several gigabytes to over 10GB. This creates significant challenges:

1. Storage pressure on production systems
2. Difficulty transferring large files from production environments
3. Post-processing overhead with tools/mm/page_owner_sort.c

The primary contributor to file size is redundant stack trace information. While the kernel already deduplicates stacks via stackdepot, page_owner retrieves and stores full stack traces for each page, only to deduplicate them again during post-processing.

Additionally, in NUMA-aware environments (e.g., DPDK-based cloud deployments where QEMU processes are bound to specific NUMA nodes), OOM events are often node-specific rather than system-wide. Previously, page_owner could not filter by NUMA node, forcing users to collect and analyze data for all nodes.

Solution
========

This patch series introduces a per-file-descriptor filter infrastructure with two initial filters:

1. **Print Mode Filter**: Outputs only stack handles instead of full stack traces. The handle-to-stack mapping can be retrieved from the existing show_stacks_handles interface. This dramatically reduces output size while preserving all allocation metadata.

2. **NUMA Node Filter**: Allows filtering pages by specific NUMA node(s) using flexible nodelist format, enabling targeted analysis of memory issues in NUMA-aware deployments.

The per-fd design allows multiple concurrent page_owner reads with different filters, solving coordination issues in multi-user production environments.

Implementation
==============

The series is structured as follows:

- Patch 1: Implement print_mode filter infrastructure
* Add file->private_data to store per-fd filter state
* Add .open, .release, and .write file operations
* Support "stack", "handle", and "stack_handle" modes via "mode=" write commands
- Patch 2: Implement NUMA node filter infrastructure
* Add nid_filter field to per-fd state
* Support flexible nodelist format via "nid=" write commands (single, multiple, ranges)
* Validate nodes and reject non-existent nodes using nodes_subset()
- Patch 3: Add page_owner_filter userspace tool
* Manages per-fd filters via write() interface
* Provides user-friendly command-line interface
* Includes comprehensive input validation
- Patch 4: Document filter features and usage

Usage Example
=============

Using the page_owner_filter tool with per-fd filters:

    # ./page_owner_filter -m stack_handle -n "0,2-3" -o page_owner.txt

The tool opens /sys/kernel/debug/page_owner, sets filters via write(), then reads the filtered output to the specified file (or stdout).

Sample print_mode output (showing handles only):

    Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper),
    ts 0 ns PFN 0x40000 type Unmovable Block 512 type Unmovable
    Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
    handle: 1048577

    Page allocated via order 0, mask 0x252000(__GFP_NOWARN|
    __GFP_NORETRY|__GFP_COMP|__GFP_THISNODE), pid 0, tgid 0 (swapper),
    ts 0 ns PFN 0x40002 type Unmovable Block 512 type Unmovable
    Flags 0x23fffe0000000200(workingset|node=0|zone=0|lastcpupid=0x1ffff)
    handle: 1048577

Testing
=======

Tested on a 4-node NUMA system. Verified that:

1. **Kernel without page_owner enabled**:
Tool properly detects and reports missing page_owner support:

   ```
   $ ./page_owner_filter -m stack
   Error: /sys/kernel/debug/page_owner does not exist
   Make sure page_owner is enabled in kernel
   ```

2. **Kernel without per-fd filter support**:
Tool properly detects and reports missing filter support:

   ```
   $ ./page_owner_filter -m stack
   Error: Kernel rejected the filter command.
   Possible causes:
     - Kernel does not support per-fd filtering
     - NUMA node has no memory
     - Unknown reason
   ```

3. **Comprehensive userspace tool testing**:
Tested 26 test cases covering:
- Help messages (-h, --help)
- Invalid inputs (mode, nid format, range validation)
- Valid modes (stack, handle, stack_handle)
- Valid nid filters (single node, multiple nodes, ranges)
- Combined mode and nid filters
- Node validity verification (grep-based verification)
- Error handling for out-of-range nodes

Test script (test_page_owner_filter.sh):

   ```bash
   #!/bin/bash
   # Test script for page_owner_filter tool

   cd "$(dirname "$0")"

   echo "========================================="
   echo "page_owner_filter Test Suite"
   echo "========================================="
   echo

   echo "Test 1: -h"
   echo "./page_owner_filter -h"
   ./page_owner_filter -h
   echo

   echo "Test 2: --help"
   echo "./page_owner_filter --help"
   ./page_owner_filter --help
   echo

   echo "Test 3: Invalid mode"
   echo ./page_owner_filter -m invalid
   ./page_owner_filter -m invalid
   echo

   echo "Test 4: Invalid nid with letters"
   echo ./page_owner_filter -n 0,a,2
   ./page_owner_filter -n 0,a,2
   echo

   echo "Test 5: Invalid nid with double comma"
   echo ./page_owner_filter -n 0,,2
   ./page_owner_filter -n 0,,2
   echo

   echo "Test 6: Invalid nid starting with comma"
   echo ./page_owner_filter -n ,0,1
   ./page_owner_filter -n ,0,1
   echo

   echo "Test 7: Invalid nid ending with comma"
   echo ./page_owner_filter -n "0,1,"
   ./page_owner_filter -n "0,1,"
   echo

   echo "Test 8: No filters specified"
   echo ./page_owner_filter
   ./page_owner_filter
   echo

   echo "Test 9: Invalid nid - node 4 (out of range)"
   echo ./page_owner_filter -n 4
   ./page_owner_filter -n 4
   echo

   echo "Test 10: Invalid nid - large number"
   echo './page_owner_filter -n 65535'
   ./page_owner_filter -n 65535
   echo

   echo "Test 11: Invalid mode AND invalid nid"
   echo ./page_owner_filter -m wrong -n abc
   ./page_owner_filter -m wrong -n abc
   echo

   echo "Test 12: Two invalid modes (try both)"
   echo ./page_owner_filter -m wrong1 -m wrong2
   ./page_owner_filter -m wrong1 -m wrong2
   echo

   echo "Test 13: Valid mode - stack"
   echo './page_owner_filter -m stack | head -20'
   ./page_owner_filter -m stack | head -20
   echo

   echo "Test 14: Valid mode - handle"
   echo './page_owner_filter -m handle | head -20'
   ./page_owner_filter -m handle | head -20
   echo

   echo "Test 15: Valid mode - stack_handle"
   echo './page_owner_filter -m stack_handle | head -20'
   ./page_owner_filter -m stack_handle | head -20
   echo

   echo "Test 16: All modes"
   echo './page_owner_filter -m stack -m handle -m stack_handle | head -20'
   ./page_owner_filter -m stack -m handle -m stack_handle | head -20
   echo

   echo "Test 17: Valid nid - single"
   echo './page_owner_filter -n 0 | head -20'
   ./page_owner_filter -n 0 | head -20
   echo 'Verify: should have node=0, should NOT have node=1,2,3'
   echo './page_owner_filter -n 0 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c'
   ./page_owner_filter -n 0 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
   echo

   echo "Test 18: Valid nid - multiple"
   echo 'Verify: should have node=0,1,3, should NOT have node=2'
   echo './page_owner_filter -n 0,1,3 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c'
   ./page_owner_filter -n 0,1,3 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
   echo

   echo "Test 19: Valid nid - range"
   echo 'Verify: should have node=2,3, should NOT have node=0,1'
   echo './page_owner_filter -n 2-3 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c'
   ./page_owner_filter -n 2-3 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
   echo

   echo "Test 20: Valid nid - range"
   echo 'Verify: should have node=0,1,2,3'
   echo './page_owner_filter -n 2-3,0-1 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c'
   ./page_owner_filter -n 2-3,0-1 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
   echo

   echo "Test 21: Valid nid - range"
   echo 'Verify: should have node=2, should NOT have node=0,1,3'
   echo './page_owner_filter -n 2-2 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c'
   ./page_owner_filter -n 2-2 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
   echo

   echo "Test 22: Invalid nid - range start must be <= end"
   echo './page_owner_filter -n 3-0'
   ./page_owner_filter -n 3-0
   echo

   echo './page_owner_filter -n 1-0,0-1'
   ./page_owner_filter -n 1-0,0-1
   echo

   echo './page_owner_filter -n 2-3,1-0,0-1'
   ./page_owner_filter -n 2-3,1-0,0-1
   echo

   echo './page_owner_filter -n 3,1-0,1'
   ./page_owner_filter -n 3,1-0,1
   echo

   echo "Test 23: Invalid nid - NUMA node 4 and above have no memory"
   echo './page_owner_filter -n 0-4'
   ./page_owner_filter -n 0-4
   echo

   echo './page_owner_filter -n 1,0-4'
   ./page_owner_filter -n 1,0-4
   echo

   echo './page_owner_filter -n 7-8'
   ./page_owner_filter -n 7-8
   echo

   echo './page_owner_filter -n 8-1'
   ./page_owner_filter -n 8-1
   echo

   echo "Test 24: Valid nid - range and comma mixed"
   echo 'Verify: should have node=0,2,3, should NOT have node=1'
   echo './page_owner_filter -n 2-3,0| grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c'
   ./page_owner_filter -n 2-3,0 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
   echo

   echo "Test 25: Valid nid - range and comma mixed"
   echo 'Verify: should have node=1,2,3, should NOT have node=0'
   echo './page_owner_filter -n 1,2-3| grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c'
   ./page_owner_filter -n 1,2-3 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
   echo

   echo "Test 26: Valid handle mode + nid filter"
   echo './page_owner_filter -m handle -n "0,1" | head -20'
   ./page_owner_filter -m handle -n "0,1" | head -20
   echo 'Verify: should show stacks, and only node=0,1 (not 2,3)'
   echo './page_owner_filter -m handle -n "0,1" | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c'
   ./page_owner_filter -m handle -n "0,1" | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
   echo


   echo "========================================="
   echo "Tests completed. Please check output above."
   echo "========================================="
   ```

Test output:

   ```
   =========================================
   page_owner_filter Test Suite
   =========================================

   Test 1: -h
   ./page_owner_filter -h
   Usage: ./page_owner_filter [OPTIONS]

   Options:
     -m, --mode MODE      : print_mode (stack, handle, or stack_handle)
     -n, --nid NID_LIST   : NUMA node IDs (comma-separated or ranges)
     -o, --output FILE    : output file (default: stdout)
     -h, --help           : show this help message

   Examples:
     ./page_owner_filter -m stack
     ./page_owner_filter -m handle
     ./page_owner_filter -m stack_handle
     ./page_owner_filter -m stack -o output.txt
     ./page_owner_filter -n 0,1,2
     ./page_owner_filter -m stack -n 0

   Test 2: --help
   ./page_owner_filter --help
   Usage: ./page_owner_filter [OPTIONS]

   Options:
     -m, --mode MODE      : print_mode (stack, handle, or stack_handle)
     -n, --nid NID_LIST   : NUMA node IDs (comma-separated or ranges)
     -o, --output FILE    : output file (default: stdout)
     -h, --help           : show this help message

   Examples:
     ./page_owner_filter -m stack
     ./page_owner_filter -m handle
     ./page_owner_filter -m stack_handle
     ./page_owner_filter -m stack -o output.txt
     ./page_owner_filter -n 0,1,2
     ./page_owner_filter -m stack -n 0

   Test 3: Invalid mode
   ./page_owner_filter -m invalid
   Error: Invalid mode 'invalid'
   Valid modes: stack, handle, stack_handle

   Test 4: Invalid nid with letters
   ./page_owner_filter -n 0,a,2
   Error: Invalid character 'a' in nid_list

   Test 5: Invalid nid with double comma
   ./page_owner_filter -n 0,,2
   Error: Invalid nid_list format

   Test 6: Invalid nid starting with comma
   ./page_owner_filter -n ,0,1
   Error: Invalid nid_list format

   Test 7: Invalid nid ending with comma
   ./page_owner_filter -n 0,1,
   Error: Invalid nid_list format

   Test 8: No filters specified
   ./page_owner_filter
   Error: At least one filter (-m or -n) must be specified

   Usage: ./page_owner_filter [OPTIONS]

   Options:
     -m, --mode MODE      : print_mode (stack, handle, or stack_handle)
     -n, --nid NID_LIST   : NUMA node IDs (comma-separated or ranges)
     -o, --output FILE    : output file (default: stdout)
     -h, --help           : show this help message

   Examples:
     ./page_owner_filter -m stack
     ./page_owner_filter -m handle
     ./page_owner_filter -m stack_handle
     ./page_owner_filter -m stack -o output.txt
     ./page_owner_filter -n 0,1,2
     ./page_owner_filter -m stack -n 0

   Test 9: Invalid nid - node 4 (out of range)
   ./page_owner_filter -n 4
   Error: Kernel rejected the filter command.
   Possible causes:
     - Kernel does not support per-fd filtering
     - NUMA node has no memory
     - Unknown reason

   Test 10: Invalid nid - large number
   ./page_owner_filter -n 65535
   write filter command: Numerical result out of range

   Test 11: Invalid mode AND invalid nid
   ./page_owner_filter -m wrong -n abc
   Error: Invalid mode 'wrong'
   Valid modes: stack, handle, stack_handle

   Test 12: Two invalid modes (try both)
   ./page_owner_filter -m wrong1 -m wrong2
   Error: Invalid mode 'wrong1'
   Valid modes: stack, handle, stack_handle

   Test 13: Valid mode - stack
   ./page_owner_filter -m stack | head -20
   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40000 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
    register_early_stack+0x2c/0x70
    init_page_owner+0x2c/0x460
    page_ext_init+0x204/0x298
    mm_core_init+0xdc/0x14c

   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40001 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
    register_early_stack+0x2c/0x70
    init_page_owner+0x2c/0x460
    page_ext_init+0x204/0x298
    mm_core_init+0xdc/0x14c

   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40002 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
    register_early_stack+0x2c/0x70
    init_page_owner+0x2c/0x460
    page_ext_init+0x204/0x298
    mm_core_init+0xdc/0x14c

   Test 14: Valid mode - handle
   ./page_owner_filter -m handle | head -20
   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40000 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
   handle: 1048577

   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40001 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
   handle: 1048577

   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40002 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
   handle: 1048577

   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40003 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
   handle: 1048577

   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40004 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000040(head|node=0|zone=0|lastcpupid=0x1ffff)
   handle: 1048577


   Test 15: Valid mode - stack_handle
   ./page_owner_filter -m stack_handle | head -20
   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40000 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
    register_early_stack+0x2c/0x70
    init_page_owner+0x2c/0x460
    page_ext_init+0x204/0x298
    mm_core_init+0xdc/0x14c
   handle: 1048577

   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40001 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
    register_early_stack+0x2c/0x70
    init_page_owner+0x2c/0x460
    page_ext_init+0x204/0x298
    mm_core_init+0xdc/0x14c
   handle: 1048577

   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40002 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
    register_early_stack+0x2c/0x70
    init_page_owner+0x2c/0x460

   Test 16: All modes
   ./page_owner_filter -m stack -m handle -m stack_handle | head -20
   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40000 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
    register_early_stack+0x2c/0x70
    init_page_owner+0x2c/0x460
    page_ext_init+0x204/0x298
    mm_core_init+0xdc/0x14c
   handle: 1048577

   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40001 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
    register_early_stack+0x2c/0x70
    init_page_owner+0x2c/0x460
    page_ext_init+0x204/0x298
    mm_core_init+0xdc/0x14c
   handle: 1048577

   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40002 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
    register_early_stack+0x2c/0x70
    init_page_owner+0x2c/0x460

   Test 17: Valid nid - single
   ./page_owner_filter -n 0 | head -20
   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40000 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
    register_early_stack+0x2c/0x70
    init_page_owner+0x2c/0x460
    page_ext_init+0x204/0x298
    mm_core_init+0xdc/0x14c

   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40001 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
    register_early_stack+0x2c/0x70
    init_page_owner+0x2c/0x460
    page_ext_init+0x204/0x298
    mm_core_init+0xdc/0x14c

   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40002 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
    register_early_stack+0x2c/0x70
    init_page_owner+0x2c/0x460
    page_ext_init+0x204/0x298
    mm_core_init+0xdc/0x14c
   Verify: should have node=0, should NOT have node=1,2,3
   ./page_owner_filter -n 0 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
     91327 node=0

   Test 18: Valid nid - multiple
   Verify: should have node=0,1,3, should NOT have node=2
   ./page_owner_filter -n 0,1,3 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
     91299 node=0
     43515 node=1
    110404 node=3

   Test 19: Valid nid - range
   Verify: should have node=2,3, should NOT have node=0,1
   ./page_owner_filter -n 2-3 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
     19391 node=2
    110287 node=3

   Test 20: Valid nid - range
   Verify: should have node=0,1,2,3
   ./page_owner_filter -n 2-3,0-1 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
     91562 node=0
     43527 node=1
     19495 node=2
    110286 node=3

   Test 21: Valid nid - range
   Verify: should have node=2, should NOT have node=0,1,3
   ./page_owner_filter -n 2-2 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
     19505 node=2

   Test 22: Invalid nid - range start must be <= end
   ./page_owner_filter -n 3-0
   Error: Invalid range 3-0 (start must be <= end)

   ./page_owner_filter -n 1-0,0-1
   Error: Invalid range 1-0 (start must be <= end)

   ./page_owner_filter -n 2-3,1-0,0-1
   Error: Invalid range 1-0 (start must be <= end)

   ./page_owner_filter -n 3,1-0,1
   Error: Invalid range 1-0 (start must be <= end)

   Test 23: Invalid nid - NUMA node 4 and above have no memory
   ./page_owner_filter -n 0-4
   Error: Kernel rejected the filter command.
   Possible causes:
     - Kernel does not support per-fd filtering
     - NUMA node has no memory
     - Unknown reason

   ./page_owner_filter -n 1,0-4
   Error: Kernel rejected the filter command.
   Possible causes:
     - Kernel does not support per-fd filtering
     - NUMA node has no memory
     - Unknown reason

   ./page_owner_filter -n 7-8
   Error: Kernel rejected the filter command.
   Possible causes:
     - Kernel does not support per-fd filtering
     - NUMA node has no memory
     - Unknown reason

   ./page_owner_filter -n 8-1
   Error: Invalid range 8-1 (start must be <= end)

   Test 24: Valid nid - range and comma mixed
   Verify: should have node=0,2,3, should NOT have node=1
   ./page_owner_filter -n 2-3,0| grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
     91741 node=0
     19389 node=2
    110286 node=3

   Test 25: Valid nid - range and comma mixed
   Verify: should have node=1,2,3, should NOT have node=0
   ./page_owner_filter -n 1,2-3| grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
     43462 node=1
     19402 node=2
    110288 node=3

   Test 26: Valid handle mode + nid filter
   ./page_owner_filter -m handle -n "0,1" | head -20
   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40000 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
   handle: 1048577

   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40001 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
   handle: 1048577

   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40002 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
   handle: 1048577

   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40003 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff)
   handle: 1048577

   Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns
   PFN 0x40004 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000040(head|node=0|zone=0|lastcpupid=0x1ffff)
   handle: 1048577

   Verify: should show stacks, and only node=0,1 (not 2,3)
   ./page_owner_filter -m handle -n "0,1" | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
     91677 node=0
     43458 node=1

   =========================================
   Tests completed. Please check output above.
   =========================================
   ```

Future Enhancements
===================

The per-fd filter infrastructure is designed to be extensible. Potential future filters could include:
- PID/TGID filtering
- Time range filtering (allocation timestamp windows)
- GFP flag filtering
- Migration type filtering

v8: https://lore.kernel.org/linux-mm/[email protected]/
v7: https://lore.kernel.org/linux-mm/[email protected]/
v6: https://lore.kernel.org/linux-mm/[email protected]/
v5: https://lore.kernel.org/linux-mm/[email protected]/
v4: https://lore.kernel.org/linux-mm/[email protected]/
v3: https://lore.kernel.org/linux-mm/[email protected]/
v2: https://lore.kernel.org/linux-mm/[email protected]/
v1: https://lore.kernel.org/linux-mm/[email protected]/

---

Zhen Ni (4):
mm/page_owner: add print_mode filter
mm/page_owner: add NUMA node filter
tools/mm: add page_owner_filter userspace tool
mm/page_owner: document page_owner filter

 Documentation/mm/page_owner.rst |  77 ++++++++-
 mm/page_owner.c                 | 186 +++++++++++++++++++-
 tools/mm/Makefile               |   4 +-
 tools/mm/page_owner_filter.c    | 292 ++++++++++++++++++++++++++++++++
 4 files changed, 549 insertions(+), 10 deletions(-)
 create mode 100644 tools/mm/page_owner_filter.c
Signature · 1 line
2.20.1
author
#1 lore raw

PATCH v9 1/4 mm/page_owner: add print_mode filter

Add a print_mode filter to page_owner that allows users to choose between printing stack traces, stack handles, or both, providing flexibility for different debugging and analysis scenarios.

The filter provides three modes via page_owner:
- Writing "mode=stack" prints stack traces for each page (default)
- Writing "mode=handle" prints only the handle number
- Writing "mode=stack_handle" prints both stack traces and handles

The default stack mode maintains backward compatibility with existing usage, displaying complete stack traces for each page allocation.

The handle mode dramatically reduces log size and improves performance by showing only the handle number instead of the full stack trace. Testing shows handle mode reduces output size by ~66% (84MB vs 244MB) and improves read performance by ~4.4x compared to full stack output. The mapping from handles to actual stack traces can be obtained via the show_stacks_handles interface.

The stack_handle mode prints both stack traces and handles, making it easier to identify pages with the same allocation pattern by comparing handle numbers instead of comparing large stack traces.

Example usage:
# Using the page_owner_filter tool (recommended)

  ./page_owner_filter -m stack          # Print only stack traces (default)
  ./page_owner_filter -m handle         # Print only handles
  ./page_owner_filter -m stack_handle   # Print both stack and handles

Sample output (handle mode):
Page allocated via order 0, migratetype Unmovable, gfp_mask 0x1100ca, pid 1, tgid 1 (systemd), ts 123456789 ns
PFN 0x1000 type Unmovable Block 1 type Unmovable
Flags 0x3fffe800000084(referenced|lru|active|private|node=0|zone=1)
handle: 17432583
...

This implementation uses per-file-descriptor filter state stored in file->private_data, allowing each opener to have independent filter configuration.

---
Changes in v9:
- Add spinlock_t lock to struct page_owner_filter_state for concurrent access protection

Changes in v8:
- Fix buffer overflow by adding bounds check between stack_depot_snprint() and scnprintf()
- Fix unsafe string handling: use memdup_user_nul() instead of kmalloc_objs + strncpy_from_user()
- Fix strsep() memory corruption by saving original pointer before strsep() call
- Change format specifier from %d to %u for depot_stack_handle_t

Changes in v7:
- per-file-descriptor implementation

Changes in v6:
- Remove unnecessary braces in if/else statement (coding style)
- Use stack array (char kbuf[33]) instead of kmalloc for input buffer

Changes in v5:
- No code changes

Changes in v4:
- Change from numeric (0/1) to string-based interface ("full_stack"/"stack_handle")
- Merge infrastructure patch into this patch

Changes in v3:
- No code changes

Changes in v2:
- Renamed from 'compact mode' to 'print_mode' for better clarity
- Use enum values (0=full_stack, 1=stack_handle) instead of boolean
- Update debugfs filename from 'compact' to 'print_mode'

v8: https://lore.kernel.org/linux-mm/[email protected]/
v7: https://lore.kernel.org/linux-mm/[email protected]/
v6: https://lore.kernel.org/linux-mm/[email protected]/
v5: https://lore.kernel.org/linux-mm/[email protected]/
v4: https://lore.kernel.org/linux-mm/[email protected]/
v3: https://lore.kernel.org/linux-mm/[email protected]/

    https://lore.kernel.org/linux-mm/[email protected]/
v2: https://lore.kernel.org/linux-mm/[email protected]/
    https://lore.kernel.org/linux-mm/[email protected]/
v1: https://lore.kernel.org/linux-mm/[email protected]/
    https://lore.kernel.org/linux-mm/[email protected]/
---
 mm/page_owner.c | 129 +++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 123 insertions(+), 6 deletions(-)
mm/page_owner.c +123 -6
index 8178e0be557f..7595735979bf 100644
@@ -54,6 +54,23 @@ struct stack_print_ctx {
 	u8 flags;
 };
 
+enum page_owner_print_mode {
+	PAGE_OWNER_PRINT_STACK,
+	PAGE_OWNER_PRINT_HANDLE,
+	PAGE_OWNER_PRINT_STACK_HANDLE,
+};
+
+static const char * const page_owner_print_mode_strings[] = {
+	[PAGE_OWNER_PRINT_STACK]	= "stack",
+	[PAGE_OWNER_PRINT_HANDLE]	= "handle",
+	[PAGE_OWNER_PRINT_STACK_HANDLE]	= "stack_handle",
+};
+
+struct page_owner_filter_state {
+	enum page_owner_print_mode print_mode;
+	spinlock_t lock;
+};
+
 static bool page_owner_enabled __initdata;
 DEFINE_STATIC_KEY_FALSE(page_owner_inited);
 
@@ -547,16 +564,23 @@ static inline int print_page_owner_memcg(char *kbuf, size_t count, int ret,
 static ssize_t
 print_page_owner(char __user *buf, size_t count, unsigned long pfn,
 		struct page *page, struct page_owner *page_owner,
-		depot_stack_handle_t handle)
+		depot_stack_handle_t handle,
+		struct page_owner_filter_state *state)
 {
 	int ret, pageblock_mt, page_mt;
 	char *kbuf;
+	enum page_owner_print_mode print_mode;
+	unsigned long flags;
 
 	count = min_t(size_t, count, PAGE_SIZE);
 	kbuf = kmalloc(count, GFP_KERNEL);
 	if (!kbuf)
 		return -ENOMEM;
 
+	spin_lock_irqsave(&state->lock, flags);
+	print_mode = state->print_mode;
+	spin_unlock_irqrestore(&state->lock, flags);
+
 	ret = scnprintf(kbuf, count,
 			"Page allocated via order %u, mask %#x(%pGg), pid %d, tgid %d (%s), ts %llu ns\n",
 			page_owner->order, page_owner->gfp_mask,
@@ -575,9 +599,18 @@ print_page_owner(char __user *buf, size_t count, unsigned long pfn,
 			migratetype_names[pageblock_mt],
 			&page->flags);
 
-	ret += stack_depot_snprint(handle, kbuf + ret, count - ret, 0);
-	if (ret >= count)
-		goto err;
+	if (print_mode != PAGE_OWNER_PRINT_HANDLE) {
+		ret += stack_depot_snprint(handle, kbuf + ret, count - ret, 0);
+		if (ret >= count)
+			goto err;
+	}
+
+	if (print_mode != PAGE_OWNER_PRINT_STACK) {
+		ret += scnprintf(kbuf + ret, count - ret, "handle: %u\n",
+				 handle);
+		if (ret >= count)
+			goto err;
+	}
 
 	if (page_owner->last_migrate_reason != -1) {
 		ret += scnprintf(kbuf + ret, count - ret,
@@ -664,6 +697,7 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
 	struct page_ext *page_ext;
 	struct page_owner *page_owner;
 	depot_stack_handle_t handle;
+	struct page_owner_filter_state *state = file->private_data;
 
 	if (!static_branch_unlikely(&page_owner_inited))
 		return -EINVAL;
@@ -746,7 +780,7 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
 		page_owner_tmp = *page_owner;
 		page_ext_put(page_ext);
 		return print_page_owner(buf, count, pfn, page,
-				&page_owner_tmp, handle);
+				&page_owner_tmp, handle, state);
 ext_put_continue:
 		page_ext_put(page_ext);
 	}
@@ -847,7 +881,90 @@ static void init_early_allocated_pages(void)
 		init_pages_in_zone(zone);
 }
 
+static int page_owner_open(struct inode *inode, struct file *file)
+{
+	struct page_owner_filter_state *state;
+
+	state = kzalloc_obj(*state);
+	if (!state)
+		return -ENOMEM;
+
+	spin_lock_init(&state->lock);
+	state->print_mode = PAGE_OWNER_PRINT_STACK;
+	file->private_data = state;
+	return 0;
+}
+
+static int page_owner_release(struct inode *inode, struct file *file)
+{
+	kfree(file->private_data);
+	return 0;
+}
+
+static ssize_t page_owner_write(struct file *file,
+				 const char __user *buf,
+				 size_t count, loff_t *ppos)
+{
+	char *kbuf;
+	char *orig;
+	char *token;
+	int ret;
+	size_t max_input_len;
+	struct page_owner_filter_state *state = file->private_data;
+	enum page_owner_print_mode new_print_mode;
+	unsigned long flags;
+
+	/*
+	 * Maximum input length for filter commands:
+	 * 32: print_mode command max length is 17 ("mode=stack_handle").
+	 */
+	max_input_len = 32;
+
+	if (count > max_input_len)
+		return -EINVAL;
+
+	kbuf = memdup_user_nul(buf, count);
+	if (IS_ERR(kbuf))
+		return PTR_ERR(kbuf);
+
+	orig = kbuf;
+
+	spin_lock_irqsave(&state->lock, flags);
+	new_print_mode = state->print_mode;
+	spin_unlock_irqrestore(&state->lock, flags);
+
+	while ((token = strsep(&kbuf, " \t\n")) != NULL) {
+		if (*token == '\0')
+			continue;
+
+		if (!strncmp(token, "mode=", 5)) {
+			ret = sysfs_match_string(page_owner_print_mode_strings,
+						token + 5);
+			if (ret < 0)
+				goto out_free;
+			new_print_mode = ret;
+		} else {
+			ret = -EINVAL;
+			goto out_free;
+		}
+	}
+
+	spin_lock_irqsave(&state->lock, flags);
+	state->print_mode = new_print_mode;
+	spin_unlock_irqrestore(&state->lock, flags);
+
+	ret = count;
+
+out_free:
+	kfree(orig);
+	return ret;
+}
+
 static const struct file_operations page_owner_fops = {
+	.owner		= THIS_MODULE,
+	.open		= page_owner_open,
+	.release	= page_owner_release,
+	.write		= page_owner_write,
 	.read		= read_page_owner,
 	.llseek		= lseek_page_owner,
 };
@@ -980,7 +1097,7 @@ static int __init pageowner_init(void)
 		return 0;
 	}
 
-	debugfs_create_file("page_owner", 0400, NULL, NULL, &page_owner_fops);
+	debugfs_create_file("page_owner", 0600, NULL, NULL, &page_owner_fops);
 	dir = debugfs_create_dir("page_owner_stacks", NULL);
 	debugfs_create_file("show_stacks", 0400, dir,
 			    (void *)(STACK_PRINT_FLAG_STACK |
Signature · 1 line
2.20.1
author
#2 lore raw

PATCH v9 2/4 mm/page_owner: add NUMA node filter

In reply to Zhen Ni

Add NUMA node filtering functionality to page_owner to allow filtering pages by specific NUMA node(s). This is useful for NUMA-aware memory allocation analysis and debugging.

The filter supports flexible input formats:
- Single node: nid=0
- Multiple nodes: nid=0,2,3
- Node range: nid=0-3
- Mixed format: nid=0,2-4,7

Example usage:
# Using the page_owner_filter tool (recommended) ./page_owner_filter -n 0-3 ./page_owner_filter -m stack_handle -n 0,2-4,7

The implementation uses per-file-descriptor filter state stored in file->private_data, allowing each opener to have independent filter configuration. It uses nodemask_t for efficient multi-node filtering and nodelist_parse() for flexible input parsing. Node validity is verified using nodes_subset() to reject nodes without memory.

---
Changes in v9:
- Add spinlock protection for NUMA filter state access
- Use memdesc_nid() instead of page_to_nid() to bypass PF_POISONED_CHECK()

Changes in v8:
- Add cond_resched() in page iteration loop to prevent RCU stalls
- Reject empty nid list to avoid enabling an empty filter
- Improve comment: "Commit all filter changes"

Changes in v7:
- per-file-descriptor implementation

Changes in v6:
- Add node validity check using nodes_subset
to reject invalid node numbers that don't exist in the system
- Move bool filter_by_nid declaration to top of block
- Use kmalloc_objs instead of kmalloc
- Remove 100 bytes overhead

Changes in v5:
- Optimize nodes_empty() check in page iteration loop
- Add __data_racy qualifier to nid_mask field

Changes in v4:
- Remove "-1" support, use empty string to clear filter
- Use strncpy_from_user() instead of copy_from_user()
- Add concurrency safety documentation for nid_mask access
- Rename fops to page_owner_nid_filter_fops for consistency

Changes in v3:
- Remove READ_ONCE/WRITE_ONCE for nodemask_t (fixes compilation errors)
* nodemask_t is a large structure (128 bytes) that triggers compile-time asserts
* Direct assignment is safe for this use case
- Add comment explaining input length calculation formula
* 6 bytes = ",NNNNN" (comma + 5-digit node number)
- Simplify "-1" check using kstrtoint() instead of dual strcmp()
- Move nodemask_t mask read outside PFN iteration loop for performance
* Avoids 128-byte structure copy on each iteration

Changes in v2:
- Use nodemask_t instead of int to support multiple nodes
- Implement nodelist_parse() to support flexible input formats
* Single node: "0", "2"
* Multiple nodes: "0,2,3"
* Ranges: "0-3"
* Mixed: "0,2-4,7"
- Use %*pbl format for output (e.g., "0-2", "0,2-4,7")
- Use dynamic memory allocation (kmalloc) to handle variable-length input
- Follow cpuset's max_write_len pattern: (100 + 6 * MAX_NUMNODES)

v8: https://lore.kernel.org/linux-mm/[email protected]/
v7: https://lore.kernel.org/linux-mm/[email protected]/
v6: https://lore.kernel.org/linux-mm/[email protected]/
v5: https://lore.kernel.org/linux-mm/[email protected]/
v4: https://lore.kernel.org/linux-mm/[email protected]/
v3: https://lore.kernel.org/linux-mm/[email protected]/
v2: https://lore.kernel.org/linux-mm/[email protected]/
v1: https://lore.kernel.org/linux-mm/[email protected]/

---
 mm/page_owner.c | 61 +++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 59 insertions(+), 2 deletions(-)
mm/page_owner.c +59 -2
index 7595735979bf..9e0fb679303f 100644
@@ -68,6 +68,8 @@ static const char * const page_owner_print_mode_strings[] = {
 
 struct page_owner_filter_state {
 	enum page_owner_print_mode print_mode;
+	nodemask_t nid_filter;
+	bool nid_filter_enabled;
 	spinlock_t lock;
 };
 
@@ -698,6 +700,7 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
 	struct page_owner *page_owner;
 	depot_stack_handle_t handle;
 	struct page_owner_filter_state *state = file->private_data;
+	unsigned long flags;
 
 	if (!static_branch_unlikely(&page_owner_inited))
 		return -EINVAL;
@@ -774,6 +777,26 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
 		if (!handle)
 			goto ext_put_continue;
 
+		/*
+		 * NUMA filter: if enabled, only output pages from specified nodes.
+		 * We cannot use page_to_nid() here because it calls
+		 * PF_POISONED_CHECK() which triggers VM_BUG_ON_PGFLAGS() when
+		 * the page is in an inconsistent state during concurrent allocation
+		 * or free. Since we're iterating pages without holding the zone
+		 * lock, we need to extract nid directly from page->flags
+		 * without the poisoned check.
+		 */
+		spin_lock_irqsave(&state->lock, flags);
+		if (state->nid_filter_enabled) {
+			int page_nid = memdesc_nid(page->flags);
+
+			if (!node_isset(page_nid, state->nid_filter)) {
+				spin_unlock_irqrestore(&state->lock, flags);
+				goto ext_put_continue;
+			}
+		}
+		spin_unlock_irqrestore(&state->lock, flags);
+
 		/* Record the next PFN to read in the file offset */
 		*ppos = pfn + 1;
 
@@ -783,6 +806,8 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
 				&page_owner_tmp, handle, state);
 ext_put_continue:
 		page_ext_put(page_ext);
+		if (need_resched())
+			cond_resched();
 	}
 
 	return 0;
@@ -891,6 +916,8 @@ static int page_owner_open(struct inode *inode, struct file *file)
 
 	spin_lock_init(&state->lock);
 	state->print_mode = PAGE_OWNER_PRINT_STACK;
+	nodes_clear(state->nid_filter);
+	state->nid_filter_enabled = false;
 	file->private_data = state;
 	return 0;
 }
@@ -912,13 +939,18 @@ static ssize_t page_owner_write(struct file *file,
 	size_t max_input_len;
 	struct page_owner_filter_state *state = file->private_data;
 	enum page_owner_print_mode new_print_mode;
+	nodemask_t new_nid_filter;
+	bool new_nid_filter_enabled;
 	unsigned long flags;
 
 	/*
 	 * Maximum input length for filter commands:
-	 * 32: print_mode command max length is 17 ("mode=stack_handle").
+	 * - 32: print_mode command max length is 17 ("mode=stack_handle")
+	 *        with sufficient buffer
+	 * - 6 * MAX_NUMNODES: worst case for nid list
+	 *   Worst case per node: ",NNNNN" (comma + 5-digit node number) = 6 bytes
 	 */
-	max_input_len = 32;
+	max_input_len = 32 + 6 * MAX_NUMNODES;
 
 	if (count > max_input_len)
 		return -EINVAL;
@@ -931,6 +963,8 @@ static ssize_t page_owner_write(struct file *file,
 
 	spin_lock_irqsave(&state->lock, flags);
 	new_print_mode = state->print_mode;
+	new_nid_filter = state->nid_filter;
+	new_nid_filter_enabled = state->nid_filter_enabled;
 	spin_unlock_irqrestore(&state->lock, flags);
 
 	while ((token = strsep(&kbuf, " \t\n")) != NULL) {
@@ -943,14 +977,37 @@ static ssize_t page_owner_write(struct file *file,
 			if (ret < 0)
 				goto out_free;
 			new_print_mode = ret;
+		} else if (!strncmp(token, "nid=", 4)) {
+			ret = nodelist_parse(token + 4, new_nid_filter);
+			if (ret < 0)
+				goto out_free;
+
+			if (nodes_empty(new_nid_filter)) {
+				ret = -EINVAL;
+				goto out_free;
+			}
+
+			/*
+			 * We want to filter memory allocations by numa nodes, so make sure
+			 * that the specified nodes have memory.
+			 */
+			if (!nodes_subset(new_nid_filter, node_states[N_MEMORY])) {
+				ret = -EINVAL;
+				goto out_free;
+			}
+
+			new_nid_filter_enabled = true;
 		} else {
 			ret = -EINVAL;
 			goto out_free;
 		}
 	}
 
+	/* Commit all filter changes */
 	spin_lock_irqsave(&state->lock, flags);
 	state->print_mode = new_print_mode;
+	state->nid_filter = new_nid_filter;
+	state->nid_filter_enabled = new_nid_filter_enabled;
 	spin_unlock_irqrestore(&state->lock, flags);
 
 	ret = count;
Signature · 1 line
2.20.1
author
#3 lore raw

PATCH v9 3/4 tools/mm: add page_owner_filter userspace tool

In reply to Zhen Ni

Add a userspace filtering tool for page_owner that supports per-fd filtering with print_mode and NUMA node filters.

Features:
- Three print modes: stack (default), handle, stack_handle
- NUMA node filtering with flexible formats (single: 0, multiple: 0,1,2,
range: 0-3, mixed: 0,2-3)
- Per-file-descriptor filter state for independent filtering

Usage examples:
# Filter by print mode
./page_owner_filter -m handle
./page_owner_filter -m stack_handle

# Filter by NUMA node
./page_owner_filter -n 0
./page_owner_filter -n 0-3

# Combined filters
./page_owner_filter -m stack -n 0,1,2
./page_owner_filter -m handle -n 0,2-3

The tool validates inputs before sending commands to the kernel and provides clear error messages when the kernel does not support per-fd filtering.

---
Changes in v9:
- Fix isdigit() usage: cast to unsigned char to avoid undefined behavior with non-ASCII input
- Optimize I/O performance: replace fprintf() + fflush() in loop with fwrite() + single fflush() after loop

Changes in v8:
- Add validation to reject multiple dashes in nid list (e.g., "1-2-3")
- Fix snprintf return value handling to prevent command overflow

Changes in v7:
- New patch for userspace tool

v8: https://lore.kernel.org/linux-mm/[email protected]/
v7: https://lore.kernel.org/linux-mm/[email protected]/

---
 tools/mm/Makefile            |   4 +-
 tools/mm/page_owner_filter.c | 292 +++++++++++++++++++++++++++++++++++
 2 files changed, 294 insertions(+), 2 deletions(-)
 create mode 100644 tools/mm/page_owner_filter.c
tools/mm/Makefile +2 -2
index f5725b5c23aa..858186a6eefd 100644
@@ -3,7 +3,7 @@
 #
 include ../scripts/Makefile.include
 
-BUILD_TARGETS=page-types slabinfo page_owner_sort thp_swap_allocator_test
+BUILD_TARGETS=page-types slabinfo page_owner_sort page_owner_filter thp_swap_allocator_test
 INSTALL_TARGETS = $(BUILD_TARGETS) thpmaps
 
 LIB_DIR = ../lib/api
@@ -23,7 +23,7 @@ $(LIBS):
 	$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
 
 clean:
-	$(RM) page-types slabinfo page_owner_sort thp_swap_allocator_test
+	$(RM) page-types slabinfo page_owner_sort page_owner_filter thp_swap_allocator_test
 	make -C $(LIB_DIR) clean
 
 sbindir ?= /usr/sbin
tools/mm/page_owner_filter.c new file +292 -0

292 lines not on this page · raw message

Signature · 1 line
2.20.1
author
#4 lore raw

PATCH v9 4/4 mm/page_owner: document page_owner filter

In reply to Zhen Ni

Add documentation for the page_owner_filter userspace tool and kernel-level filtering features.

---

Changes in v9:
- No changes

Changes in v8:
- Fix Sphinx double colon warning

Changes in v7:
- document for per-file-descriptor implementation

Changes in v6:
- No code changes

Changes in v5:
- No code changes

Changes in v4:
- Update print_mode documentation to reflect string-based interface
* Change from "0/1" to "full_stack"/"stack_handle"
* Add bracket notation example: "[full_stack] stack_handle"
- Update NUMA filter documentation
* Remove "-1" example
* Add empty string as clear method
- Fix indentation: use tabs instead of spaces in code examples

Changes in v3:
- New patch to document filter features as requested by Andrew Morton

v8: https://lore.kernel.org/linux-mm/[email protected]/
v7: https://lore.kernel.org/linux-mm/[email protected]/
v6: https://lore.kernel.org/linux-mm/[email protected]/
v5: https://lore.kernel.org/linux-mm/[email protected]/
v4: https://lore.kernel.org/linux-mm/[email protected]/
v3: https://lore.kernel.org/linux-mm/[email protected]/

---
 Documentation/mm/page_owner.rst | 77 ++++++++++++++++++++++++++++++++-
 1 file changed, 75 insertions(+), 2 deletions(-)
Documentation/mm/page_owner.rst +75 -2
index 6b12f3b007ec..383e59c42743 100644
@@ -65,7 +65,14 @@ un-tracking state.
 Usage
 =====
 
-1) Build user-space helper::
+1) Build user-space helpers::
+
+To filter page_owner output:
+
+	cd tools/mm
+	make page_owner_filter
+
+To sort and analyze page_owner output:
 
 	cd tools/mm
 	make page_owner_sort
@@ -74,7 +81,11 @@ Usage
 
 3) Do the job that you want to debug.
 
-4) Analyze information from page owner::
+4) (Optional) Filter page_owner output::
+
+	./page_owner_filter -m handle -n 0,1,2 > filtered_page_owner.txt
+
+5) Analyze information from page owner::
 
 	cat /sys/kernel/debug/page_owner_stacks/show_stacks > stacks.txt
 	cat stacks.txt
@@ -263,3 +274,65 @@ STANDARD FORMAT SPECIFIERS
 	f		free		whether the page has been released or not
 	st		stacktrace	stack trace of the page allocation
 	ator		allocator	memory allocator for pages
+
+Filtering page_owner output
+============================
+
+page_owner supports filtering output at the kernel level before reading,
+which reduces the amount of data that needs to be processed in userspace.
+
+The page_owner_filter tool provides a convenient interface for this filtering
+capability. It supports two types of filters:
+
+1. **print_mode filter**: Control what information is printed for each page
+	- ``stack``: Print full stack traces (default, compatible with existing usage)
+	- ``handle``: Print only stack handle numbers (much faster, smaller output)
+	- ``stack_handle``: Print both stack traces and handle numbers
+
+	The ``handle`` mode uses numeric identifiers instead of full stack traces.
+	The mapping from handles to actual stack traces can be obtained via the
+	show_stacks_handles interface.
+
+2. **NUMA node filter**: Filter pages by NUMA node ID
+	- Supports single node: ``-n 0``
+	- Multiple nodes: ``-n 0,1,2``
+	- Ranges: ``-n 0-3``
+	- Mixed format: ``-n 0,2-3,5``
+
+Usage examples::
+
+	# Filter by print mode
+	./page_owner_filter -m handle
+	./page_owner_filter -m stack_handle
+
+	# Filter by NUMA node
+	./page_owner_filter -n 0
+	./page_owner_filter -n 0-3
+
+	# Combined filters
+	./page_owner_filter -m stack -n 0,1,2
+	./page_owner_filter -m handle -n 0,2-3
+
+	# Save to file
+	./page_owner_filter -m handle -o filtered_output.txt
+
+The handle mode is particularly useful for monitoring and performance-critical
+scenarios as it dramatically reduces output size. Testing shows handle mode can
+reduce output size by ~66% (84MB vs 244MB) and improve read performance by ~4.4x
+compared to full stack output.
+
+The NUMA node filter is useful for NUMA-aware memory allocation analysis and debugging.
+
+Behind the scenes, page_owner_filter opens /sys/kernel/debug/page_owner and
+writes filter commands before reading the filtered output. The filtering uses
+per-file-descriptor state, allowing each open() to have independent filter settings.
+
+Each file descriptor maintains its own filter state, so you can have multiple
+independent filtering operations running concurrently. For example, in different
+terminals you can run different filters simultaneously::
+
+	# Terminal 1: Filter node 0
+	./page_owner_filter -n 0 > node0_output.txt
+
+	# Terminal 2: Filter node 1 (runs concurrently)
+	./page_owner_filter -n 1 > node1_output.txt
Signature · 1 line
2.20.1
author
#6 lore raw
Quoted 10 lines · Andrew Morton
Quoted 2 lines · On Mon, 25 May 2026 16:16:48 +0800 Zhen Ni <[email protected]> wrote:

This patch series introduces per-file-descriptor filtering capabilities to the page_owner feature.

Thanks again. AI review has found a bunch of new things to get worried about:

	https://sashiko.dev/#/patchset/[email protected]

Hi,

Can this lead to an out-of-bounds memory read?

The NUMA filter in page_owner (mm/page_owner.c:790-798) bypasses PF_POISONED_CHECK() to avoid triggering VM_BUG_ON during concurrent page allocation/free:

    int page_nid = memdesc_nid(page->flags);

When NODE_NOT_IN_PAGE_FLAGS is defined, memdesc_nid() performs unchecked array access:

    int memdesc_nid(memdesc_flags_t mdf)
    {
        return section_to_node_table[memdesc_section(mdf)];
    }

If page->flags is poisoned, memdesc_section() can return a garbage section_nr that causes out-of-bounds access.

## Lockless Access Safety Principle

The page_owner iterator runs without locks, meaning pages can be allocated or freed concurrently. The fundamental design principle should be:

"It's acceptable to skip a small number of abnormal pages, but panics must be prevented."

In lockless iteration, TOCTOU is unavoidable - even with reference counting or RCU, page->flags can still be modified concurrently during access. Zone locks prevent this but are prohibitively expensive.

## Proposed Solution: Add nid to struct page_owner

Record nid at allocation time when page state is stable, eliminating the need to extract it from page->flags during iteration:

### 1. Modify struct page_owner

    struct page_owner {
        unsigned short order;
        short last_migrate_reason;
        ...
        pid_t tgid;
        pid_t free_pid;
        pid_t free_tgid;
        int nid;  // NEW
    };

### 2. Record nid during allocation

    static inline void __update_page_owner_handle(struct page *page, ...)
    {
        int nid = page_to_nid(page);  // Safe in allocation context

        for_each_page_ext(page, 1 << order, page_ext, iter) {
            page_owner = get_page_owner(page_ext);
            page_owner->nid = nid;
            // ... other fields ...
        }
    }

### 3. Use saved nid in NUMA filter

    if (state->nid_filter_enabled) {
        int page_nid = page_owner->nid;  // Direct read, safe

        if (!node_isset(page_nid, state->nid_filter)) {
            spin_unlock_irqrestore(&state->lock, flags);
            goto ext_put_continue;
        }
    }

### 4. Update nid on page migration

    // In split_page_owner() when page migrates
    page_owner->nid = page_to_nid(&newfolio->page);

The remaining two issues can also be improved. If there are no additional comments, I will proceed with sending v10.

Thanks,
Zhen

#7 lore raw
Quoted 85 lines · zhen.ni
Quoted 10 lines · Andrew Morton
Quoted 2 lines · On Mon, 25 May 2026 16:16:48 +0800 Zhen Ni <[email protected]> wrote:

This patch series introduces per-file-descriptor filtering capabilities to the page_owner feature.

Thanks again. AI review has found a bunch of new things to get worried about:

	https://sashiko.dev/#/patchset/[email protected]

Hi,

Can this lead to an out-of-bounds memory read?

The NUMA filter in page_owner (mm/page_owner.c:790-798) bypasses PF_POISONED_CHECK() to avoid triggering VM_BUG_ON during concurrent page allocation/free:

     int page_nid = memdesc_nid(page->flags);

When NODE_NOT_IN_PAGE_FLAGS is defined, memdesc_nid() performs unchecked array access:

     int memdesc_nid(memdesc_flags_t mdf)
     {
         return section_to_node_table[memdesc_section(mdf)];
     }

If page->flags is poisoned, memdesc_section() can return a garbage section_nr that causes out-of-bounds access.

## Lockless Access Safety Principle

The page_owner iterator runs without locks, meaning pages can be allocated or freed concurrently. The fundamental design principle should be:

"It's acceptable to skip a small number of abnormal pages, but panics must be prevented."

In lockless iteration, TOCTOU is unavoidable - even with reference counting or RCU, page->flags can still be modified concurrently during access. Zone locks prevent this but are prohibitively expensive.

## Proposed Solution: Add nid to struct page_owner

Record nid at allocation time when page state is stable, eliminating the need to extract it from page->flags during iteration:

### 1. Modify struct page_owner

     struct page_owner {
         unsigned short order;
         short last_migrate_reason;
         ...
         pid_t tgid;
         pid_t free_pid;
         pid_t free_tgid;
         int nid;  // NEW
     };

### 2. Record nid during allocation

     static inline void __update_page_owner_handle(struct page *page, ...)
     {
         int nid = page_to_nid(page);  // Safe in allocation context

         for_each_page_ext(page, 1 << order, page_ext, iter) {
             page_owner = get_page_owner(page_ext);
             page_owner->nid = nid;
             // ... other fields ...
         }
     }

### 3. Use saved nid in NUMA filter

     if (state->nid_filter_enabled) {
         int page_nid = page_owner->nid;  // Direct read, safe

         if (!node_isset(page_nid, state->nid_filter)) {
             spin_unlock_irqrestore(&state->lock, flags);
             goto ext_put_continue;
         }
     }
Quoted 5 lines

### 4. Update nid on page migration

     // In split_page_owner() when page migrates
     page_owner->nid = page_to_nid(&newfolio->page);

This (presumably LLM) suggestion is a, let's say "lazy" solution to the problem, leading to more memory usage. I'd be surprised if it's not possible to read the nid in a way that avoids the hazards. If page_to_nid() can trigger a VM_BUG_ON(), then I'd add a version without that VM_BUG_ON(), handling the poisoned state gracefully - if it's poisoned, return e.g. NUMA_NO_NODE and skip the page, or something.

Quoted 6 lines

The remaining two issues can also be improved. If there are no additional comments, I will proceed with sending v10.

Thanks,
Zhen

author
#8 lore raw
Quoted 92 lines · Vlastimil Babka (SUSE)
Quoted 85 lines · zhen.ni
Quoted 10 lines · Andrew Morton
Quoted 2 lines · On Mon, 25 May 2026 16:16:48 +0800 Zhen Ni <[email protected]> wrote:

This patch series introduces per-file-descriptor filtering capabilities to the page_owner feature.

Thanks again. AI review has found a bunch of new things to get worried about:

	https://sashiko.dev/#/patchset/[email protected]

Hi,

Can this lead to an out-of-bounds memory read?

The NUMA filter in page_owner (mm/page_owner.c:790-798) bypasses PF_POISONED_CHECK() to avoid triggering VM_BUG_ON during concurrent page allocation/free:

      int page_nid = memdesc_nid(page->flags);

When NODE_NOT_IN_PAGE_FLAGS is defined, memdesc_nid() performs unchecked array access:

      int memdesc_nid(memdesc_flags_t mdf)
      {
          return section_to_node_table[memdesc_section(mdf)];
      }

If page->flags is poisoned, memdesc_section() can return a garbage section_nr that causes out-of-bounds access.

## Lockless Access Safety Principle

The page_owner iterator runs without locks, meaning pages can be allocated or freed concurrently. The fundamental design principle should be:

"It's acceptable to skip a small number of abnormal pages, but panics must be prevented."

In lockless iteration, TOCTOU is unavoidable - even with reference counting or RCU, page->flags can still be modified concurrently during access. Zone locks prevent this but are prohibitively expensive.

## Proposed Solution: Add nid to struct page_owner

Record nid at allocation time when page state is stable, eliminating the need to extract it from page->flags during iteration:

### 1. Modify struct page_owner

      struct page_owner {
          unsigned short order;
          short last_migrate_reason;
          ...
          pid_t tgid;
          pid_t free_pid;
          pid_t free_tgid;
          int nid;  // NEW
      };

### 2. Record nid during allocation

      static inline void __update_page_owner_handle(struct page *page, ...)
      {
          int nid = page_to_nid(page);  // Safe in allocation context

          for_each_page_ext(page, 1 << order, page_ext, iter) {
              page_owner = get_page_owner(page_ext);
              page_owner->nid = nid;
              // ... other fields ...
          }
      }

### 3. Use saved nid in NUMA filter

      if (state->nid_filter_enabled) {
          int page_nid = page_owner->nid;  // Direct read, safe

          if (!node_isset(page_nid, state->nid_filter)) {
              spin_unlock_irqrestore(&state->lock, flags);
              goto ext_put_continue;
          }
      }
Quoted 5 lines

### 4. Update nid on page migration

      // In split_page_owner() when page migrates
      page_owner->nid = page_to_nid(&newfolio->page);
Quoted 16 lines

This (presumably LLM) suggestion is a, let's say "lazy" solution to the problem, leading to more memory usage. I'd be surprised if it's not possible to read the nid in a way that avoids the hazards. If page_to_nid() can trigger a VM_BUG_ON(), then I'd add a version without that VM_BUG_ON(), handling the poisoned state gracefully - if it's poisoned, return e.g. NUMA_NO_NODE and skip the page, or something.

Quoted 6 lines

The remaining two issues can also be improved. If there are no additional comments, I will proceed with sending v10.

Thanks,
Zhen

Thank you for the review.

I'd like to clarify that the "nid" member approach was my own design after careful consideration of alternatives, not a suggestion from automated tools.:)

In fact, LLMs suggested approaches like "check then access" which I had already implemented and rejected in earlier versions due to TOCTOU issues. The key insight is that page_owner serves as a buffering layer for struct page, eliminating lockless access inconsistency entirely.

Think about it this way:
Even with extensive checks, page_owner->handle cannot guarantee the page won't be freed when printed. The time window between checking page->flags and accessing page->flags is unavoidable in lockless iteration.

By recording nid at allocation time (when page->flags is stable), page_owner becomes a "consistent snapshot" that can be safely accessed without locks. This is the same principle that makes page_owner work as a debug feature in the first place - it accepts a small inconsistency window in exchange for lockless access.

Alternative approaches (adding poison checks, bounds checking) cannot fully eliminate TOCTOU in lockless code - they just reduce the probability. The buffering approach is the way to provide both safety and lockless performance.

Thanks,
Zhen

#9 lore raw
Quoted 112 lines · zhen.ni
Quoted 92 lines · Vlastimil Babka (SUSE)
Quoted 85 lines · zhen.ni
Quoted 10 lines · Andrew Morton
Quoted 2 lines · On Mon, 25 May 2026 16:16:48 +0800 Zhen Ni <[email protected]> wrote:

This patch series introduces per-file-descriptor filtering capabilities to the page_owner feature.

Thanks again. AI review has found a bunch of new things to get worried about:

	https://sashiko.dev/#/patchset/[email protected]

Hi,

Can this lead to an out-of-bounds memory read?

The NUMA filter in page_owner (mm/page_owner.c:790-798) bypasses PF_POISONED_CHECK() to avoid triggering VM_BUG_ON during concurrent page allocation/free:

      int page_nid = memdesc_nid(page->flags);

When NODE_NOT_IN_PAGE_FLAGS is defined, memdesc_nid() performs unchecked array access:

      int memdesc_nid(memdesc_flags_t mdf)
      {
          return section_to_node_table[memdesc_section(mdf)];
      }

If page->flags is poisoned, memdesc_section() can return a garbage section_nr that causes out-of-bounds access.

## Lockless Access Safety Principle

The page_owner iterator runs without locks, meaning pages can be allocated or freed concurrently. The fundamental design principle should be:

"It's acceptable to skip a small number of abnormal pages, but panics must be prevented."

In lockless iteration, TOCTOU is unavoidable - even with reference counting or RCU, page->flags can still be modified concurrently during access. Zone locks prevent this but are prohibitively expensive.

## Proposed Solution: Add nid to struct page_owner

Record nid at allocation time when page state is stable, eliminating the need to extract it from page->flags during iteration:

### 1. Modify struct page_owner

      struct page_owner {
          unsigned short order;
          short last_migrate_reason;
          ...
          pid_t tgid;
          pid_t free_pid;
          pid_t free_tgid;
          int nid;  // NEW
      };

### 2. Record nid during allocation

      static inline void __update_page_owner_handle(struct page *page, ...)
      {
          int nid = page_to_nid(page);  // Safe in allocation context

          for_each_page_ext(page, 1 << order, page_ext, iter) {
              page_owner = get_page_owner(page_ext);
              page_owner->nid = nid;
              // ... other fields ...
          }
      }

### 3. Use saved nid in NUMA filter

      if (state->nid_filter_enabled) {
          int page_nid = page_owner->nid;  // Direct read, safe

          if (!node_isset(page_nid, state->nid_filter)) {
              spin_unlock_irqrestore(&state->lock, flags);
              goto ext_put_continue;
          }
      }
Quoted 5 lines

### 4. Update nid on page migration

      // In split_page_owner() when page migrates
      page_owner->nid = page_to_nid(&newfolio->page);
Quoted 16 lines

This (presumably LLM) suggestion is a, let's say "lazy" solution to the problem, leading to more memory usage. I'd be surprised if it's not possible to read the nid in a way that avoids the hazards. If page_to_nid() can trigger a VM_BUG_ON(), then I'd add a version without that VM_BUG_ON(), handling the poisoned state gracefully - if it's poisoned, return e.g. NUMA_NO_NODE and skip the page, or something.

Quoted 6 lines

The remaining two issues can also be improved. If there are no additional comments, I will proceed with sending v10.

Thanks,
Zhen

Quoted 5 lines

Thank you for the review.

I'd like to clarify that the "nid" member approach was my own design after careful consideration of alternatives, not a suggestion from automated tools.:)

Alright, good :)

Quoted 4 lines

In fact, LLMs suggested approaches like "check then access" which I had already implemented and rejected in earlier versions due to TOCTOU issues. The key insight is that page_owner serves as a buffering layer for struct page, eliminating lockless access inconsistency entirely.

Sure, but is the elimination necessary if it has a memory cost?

Quoted 5 lines

Think about it this way:
Even with extensive checks, page_owner->handle cannot guarantee the page won't be freed when printed. The time window between checking page->flags and accessing page->flags is unavoidable in lockless iteration.

Of course, but that's an argument saying that page owner as a whole can't be a perfect snapshot anyway. Then it follows that "nid" snapshot doesn't need to be perfect either.

Quoted 5 lines

By recording nid at allocation time (when page->flags is stable), page_owner becomes a "consistent snapshot" that can be safely accessed without locks. This is the same principle that makes page_owner work as a debug feature in the first place - it accepts a small inconsistency window in exchange for lockless access.

And that means we can accept some more small inconsistency for nid, without occupying more memory.

Quoted 4 lines

Alternative approaches (adding poison checks, bounds checking) cannot fully eliminate TOCTOU in lockless code - they just reduce the probability. The buffering approach is the way to provide both safety and lockless performance.

Please explain how the following "page_to_nid_robust()" (or similar name) code cannot eliminate the out of bounds accesses completely. The section/nid in page flags is possibly the most stable part of the whole struct page, it doesn't change as page is allocated, freed, or how it's used. The only problem is the poison.

memdesc_flags_t flags = READ_ONCE(page->flags);

// our flags variable is no longer subject to TOCTOU

if (flags.f == PAGE_POISON_PATTERN)

	return NUMA_NO_NODE;

return memdesc_nid(flags);

-

Quoted 2 lines

Thanks,
Zhen

author
#10 lore raw
Quoted 160 lines · Vlastimil Babka (SUSE)
Quoted 112 lines · zhen.ni
Quoted 92 lines · Vlastimil Babka (SUSE)
Quoted 85 lines · zhen.ni
Quoted 10 lines · Andrew Morton
Quoted 2 lines · On Mon, 25 May 2026 16:16:48 +0800 Zhen Ni <[email protected]> wrote:

This patch series introduces per-file-descriptor filtering capabilities to the page_owner feature.

Thanks again. AI review has found a bunch of new things to get worried about:

	https://sashiko.dev/#/patchset/[email protected]

Hi,

Can this lead to an out-of-bounds memory read?

The NUMA filter in page_owner (mm/page_owner.c:790-798) bypasses PF_POISONED_CHECK() to avoid triggering VM_BUG_ON during concurrent page allocation/free:

       int page_nid = memdesc_nid(page->flags);

When NODE_NOT_IN_PAGE_FLAGS is defined, memdesc_nid() performs unchecked array access:

       int memdesc_nid(memdesc_flags_t mdf)
       {
           return section_to_node_table[memdesc_section(mdf)];
       }

If page->flags is poisoned, memdesc_section() can return a garbage section_nr that causes out-of-bounds access.

## Lockless Access Safety Principle

The page_owner iterator runs without locks, meaning pages can be allocated or freed concurrently. The fundamental design principle should be:

"It's acceptable to skip a small number of abnormal pages, but panics must be prevented."

In lockless iteration, TOCTOU is unavoidable - even with reference counting or RCU, page->flags can still be modified concurrently during access. Zone locks prevent this but are prohibitively expensive.

## Proposed Solution: Add nid to struct page_owner

Record nid at allocation time when page state is stable, eliminating the need to extract it from page->flags during iteration:

### 1. Modify struct page_owner

       struct page_owner {
           unsigned short order;
           short last_migrate_reason;
           ...
           pid_t tgid;
           pid_t free_pid;
           pid_t free_tgid;
           int nid;  // NEW
       };

### 2. Record nid during allocation

       static inline void __update_page_owner_handle(struct page *page, ...)
       {
           int nid = page_to_nid(page);  // Safe in allocation context

           for_each_page_ext(page, 1 << order, page_ext, iter) {
               page_owner = get_page_owner(page_ext);
               page_owner->nid = nid;
               // ... other fields ...
           }
       }

### 3. Use saved nid in NUMA filter

       if (state->nid_filter_enabled) {
           int page_nid = page_owner->nid;  // Direct read, safe

           if (!node_isset(page_nid, state->nid_filter)) {
               spin_unlock_irqrestore(&state->lock, flags);
               goto ext_put_continue;
           }
       }
Quoted 5 lines

### 4. Update nid on page migration

       // In split_page_owner() when page migrates
       page_owner->nid = page_to_nid(&newfolio->page);
Quoted 16 lines

This (presumably LLM) suggestion is a, let's say "lazy" solution to the problem, leading to more memory usage. I'd be surprised if it's not possible to read the nid in a way that avoids the hazards. If page_to_nid() can trigger a VM_BUG_ON(), then I'd add a version without that VM_BUG_ON(), handling the poisoned state gracefully - if it's poisoned, return e.g. NUMA_NO_NODE and skip the page, or something.

Quoted 6 lines

The remaining two issues can also be improved. If there are no additional comments, I will proceed with sending v10.

Thanks,
Zhen

Quoted 5 lines

Thank you for the review.

I'd like to clarify that the "nid" member approach was my own design after careful consideration of alternatives, not a suggestion from automated tools.:)

Alright, good :)

Quoted 4 lines

In fact, LLMs suggested approaches like "check then access" which I had already implemented and rejected in earlier versions due to TOCTOU issues. The key insight is that page_owner serves as a buffering layer for struct page, eliminating lockless access inconsistency entirely.

Sure, but is the elimination necessary if it has a memory cost?

Quoted 5 lines

Think about it this way:
Even with extensive checks, page_owner->handle cannot guarantee the page won't be freed when printed. The time window between checking page->flags and accessing page->flags is unavoidable in lockless iteration.

Of course, but that's an argument saying that page owner as a whole can't be a perfect snapshot anyway. Then it follows that "nid" snapshot doesn't need to be perfect either.

Quoted 5 lines

By recording nid at allocation time (when page->flags is stable), page_owner becomes a "consistent snapshot" that can be safely accessed without locks. This is the same principle that makes page_owner work as a debug feature in the first place - it accepts a small inconsistency window in exchange for lockless access.

And that means we can accept some more small inconsistency for nid, without occupying more memory.

Quoted 4 lines

Alternative approaches (adding poison checks, bounds checking) cannot fully eliminate TOCTOU in lockless code - they just reduce the probability. The buffering approach is the way to provide both safety and lockless performance.

Please explain how the following "page_to_nid_robust()" (or similar name) code cannot eliminate the out of bounds accesses completely. The section/nid in page flags is possibly the most stable part of the whole struct page, it doesn't change as page is allocated, freed, or how it's used. The only problem is the poison.

memdesc_flags_t flags = READ_ONCE(page->flags);

Quoted 13 lines

// our flags variable is no longer subject to TOCTOU

if (flags.f == PAGE_POISON_PATTERN)

	return NUMA_NO_NODE;

return memdesc_nid(flags);

-

Quoted 2 lines

Thanks,
Zhen

Hi,

I agree that saving page->flags to a local variable can prevent TOCTOU. Both the nid member and page_to_nid_robust approaches are functionally equivalent, but the memory cost of the nid member is real.

Here's how I'm handling this logic:

    if (state->nid_filter_enabled) {
        int nid;
        memdesc_flags_t page_flags = READ_ONCE(page->flags);

        if (page_flags.f == PAGE_POISON_PATTERN) {
            spin_unlock_irqrestore(&state->lock, flags);
            goto ext_put_continue;
        }
        nid = memdesc_nid(page_flags);
        if (!node_isset(nid, state->nid_filter)) {
            spin_unlock_irqrestore(&state->lock, flags);
            goto ext_put_continue;
        }
    }

This might not be suited to be a separate public function - here we just need to check if the page is poisoned to prevent crashes. If it were a public function, it might need to check if the page exists before getting nid.

If you have further suggestions, please let me know.

Thanks,
Zhen