All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/9] lib/folio_pool: Direct-Map Large Folio Pool & Scratchpad bump allocators
@ 2026-08-17 17:22 ` Jim Cromie
  0 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie via B4 Relay @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

Introduce two light-weight bump allocators backed directly by compound
folio pages from the buddy allocator (skipping SLUB metadata overhead):

1. struct folio_scratchpad - Variable-sized, alignment-aware bump allocator
   for bursty, append-mostly lifecycles with bulk teardown.
2. struct folio_pool - Fixed uniform-slot bump allocator for homogeneous
   descriptors.

These serve 2 kinds of use-cases:

1: Ephemeral Batch/transaction Lifecycles (Netfilter, BPF, DRM GPUVM)

- Transactional subsystems allocate bursts of hundreds or thousands of
  homogeneous or heterogeneous objects, only to tear them all down
  simultaneously at batch completion or error abort.

- Under SLUB: Each descriptor incurs freelist traversal, lock
  contention, and O(N) kfree() loops on teardown.

- Under Folio-Scratchpad: Allocations are straight-line pointer bumps,
  bulk teardown is O(1) folio_put(), and consecutive Netlink
  transactions reuse warm L1/L2 cachelines without buddy lock churn
  via folio_scratchpad_reset().

2: Dynamic Long-Lived Graph Scaling (Lockdep)

- Core subsystems historically avoided SLUB by statically
  preallocating massive compile-time arrays in .bss
  (e.g. list_entries[32768], 1.31 MB) to prevent SLUB allocator
  recursion deadlocks.

- Fixed arrays exhaust their limit and permanently disable validation
  (BUG: MAX_LOCKDEP_ENTRIES too low!).
  
- Under Folio-Pool: Lockdep uses a 4,096-entry __initdata bootstrap
  buffer for early boot prior to buddy initialization, then seamlessly
  migrates and compacts all dependency edges into 64 KB direct-map
  folios at late_initcall.
  
- Permanent static .bss allocation for dependency edges is reduced
  from 1.31 MB to 0 KB (100% reclamation at free_initmem()).

Core Primitives in include/linux/folio_pool.h & lib/folio_pool.c:
-----------------------------------------------------------------
0. struct folio_scratchpad (Variable-Sized Elements):
   - Dynamic alignment-aware bump pointer with zero padding waste.
   - API: folio_scratchpad_init(), folio_scratchpad_alloc(),
     folio_scratchpad_reset(), folio_scratchpad_free(),
     folio_scratchpad_stats().
   - Typed macros: folio_scratchpad_alloc_obj(),
     folio_scratchpad_alloc_bytes(), DEFINE_FREE(folio_scratchpad, ...).

1. struct folio_pool (Fixed Uniform Elements):
   - Thin wrapper embedding struct folio_scratchpad for homogeneous objects.
   - API: folio_pool_init(), folio_pool_alloc(), folio_pool_free(),
     folio_pool_stats().
   - Typed macros: folio_pool_alloc_obj(), folio_pool_alloc_type().

2. Encapsulated Slab Discrimination & Reallocation:
   - is_folio_pool_ptr(ptr): Direct-map folios are identified via
     virt_to_folio(ptr) && !folio_test_slab(f), avoiding custom bitflags.
   - folio_scratchpad_free_elem(ptr): Safe no-op for folio-backed objects;
     delegates to kvfree() for SLUB/vmalloc fallback objects.
   - folio_scratchpad_realloc(ptr, old_size, new_size, gfp): Delegates to
     krealloc() for SLUB objects; allocates a fresh SLUB buffer and copies
     payload for folio-backed descriptors.

3. Subsystem Autonomy & Runtime Static-Key Control:
   - DEFINE_FOLIO_POOL_STATIC_KEY_PARAM() allows maintainers of each
     subsystem (Netfilter, Lockdep, DRM, BPF) to choose their own default
     enablement policy (DECLARE_STATIC_KEY_TRUE vs FALSE) and independent
     module/boot parameters (e.g. lockdep.folio_pool, nf_tables.trans_scratchpad).
   - Enables isolated, zero-overhead A/B benchmarking and production triage
     without cross-subsystem coupling or kernel recompilation.

Active Proofs & Performance Profiling:
--------------------------------------
0. Virtualized Guest Netfilter Benchmark (KVM / virtme-ng ftrace):
   Controlled A/B benchmark across 10,025 Netlink transaction descriptors
   and 5 asynchronous commit/abort teardown passes:

   Function: nft_trans_alloc (10,025 descriptor allocations)
   -----------------------------------------------------------------------------
   Mode                                 Total Time       Avg / Alloc      Delta
   -----------------------------------------------------------------------------
   folio_scratchpad Enabled (Bump)      9,158.02 us          0.914 us    -19.4%
   SLUB Fallback (kzalloc)             11,366.53 us          1.134 us  Baseline
   -----------------------------------------------------------------------------
   Net allocation latency reduction:   -2,208.52 us (-2.21 ms)

   Function: nf_tables_trans_destroy_work (5 asynchronous teardown passes)
   -----------------------------------------------------------------------------
   Mode                                 Total Time       Avg / Pass       Delta
   -----------------------------------------------------------------------------
   folio_scratchpad Enabled (Bulk)     36,065.65 us          7.21 ms      -8.6%
   SLUB Fallback (O(N) kfree)          39,449.65 us          7.89 ms   Baseline
   -----------------------------------------------------------------------------
   Net teardown latency reduction:     -3,384.00 us (-3.38 ms)

   Combined In-Kernel Netlink Transaction Lifecycle Savings: -5.59 ms (-13.3%)

1. Bare-Metal Host Netfilter Profile (AMD Zen 3 Hardware PMU & ftrace):
   - Asynchronous Teardown: 53.77 ms vs 56.03 ms (-2.26 ms / -4.0%).
   - Instruction Retirement: 281.29M -> 282.46M (-1,174,062 instructions).
   - Branch Instructions:     62.94M ->  63.20M (-265,961 branches).

2. Bare-Metal Host Lockdep Telemetry (Physical Host frodo, AMD Zen SMP):
   - Pre-Buddy Watermark: 928/4096 bootstrap entries consumed during boot.
   - Graph Compaction: Migrated and compacted bootstrap dependency edges
     into folio_pool at late_initcall; bootstrap array reclaimed by
     free_initmem() (0 KB permanent .bss cost).
   - Live procfs Telemetry (/proc/lockdep_stats):
     lock-classes:                          996 [max: 8192]
     direct dependencies:                  2324 [dynamic: 7 x 64 kB, tail: 17 kB/64 kB]
     indirect dependencies:               24612
     all direct dependencies:             81884
     zapped classes:                          2
     zapped lock chains:                    173
   - Active Invariant Validation: Live module unloads exercised modernized
     zap_class() direct list unlinking across dynamic folios with zero
     global bitmap scans and zero assertions.

What's Unchanged:
-----------------
- All consumer object structures, alignment rules, and lifecycle boundaries
  remain strictly identical.
- Memory safety invariants are preserved: individual deallocation is a safe
  no-op for arena-backed objects while SLUB objects continue to use kfree().

Subsystem Adoptions in this Series:
-----------------------------------
0. lib/folio_pool: Core direct-map large-folio pool and scratchpad allocators.
1. netfilter/nf_tables: Pack netlink transaction descriptors into per-net
   trans_scratchpad with bulk reclaim & warm chunk reset.
2. bpf/verifier: Route verifier stack state node allocations to folio_pool.
3. drm/gpuvm: Route gpuva_op allocations to folio_scratchpad.
4. bpf/syscall: Route generic_map_update_batch key/value allocations to
   folio_scratchpad.
5. locking/lockdep: Fallback to folio_pool in alloc_list_entry when static
   pool is full and expose dynamic folio telemetry in /proc/lockdep_stats.
6. locking/lockdep: Traverse adjacency lists directly in zap_class().
7. locking/lockdep: Shrink static list_entries array to early bootstrap buffer.
8. locking/lockdep: Migrate and compact boot-time dependency graph from __initdata.

Patches in this series:
-----------------------
[PATCH 1/9] lib/folio_pool: Introduce Direct-Map Large Folio Pool & Scratchpad bump allocators
[PATCH 2/9] netfilter/nf_tables: Add folio_scratchpad collector to struct nftables_pernet
[PATCH 3/9] bpf/verifier: Route verifier stack state node allocations to folio_pool
[PATCH 4/9] drm/gpuvm: Route gpuva_op allocations to folio_scratchpad
[PATCH 5/9] bpf/syscall: Route generic_map_update_batch key/value allocations to folio_scratchpad
[PATCH 6/9] locking/lockdep: Fallback to folio_pool in alloc_list_entry when static pool is full
[PATCH 7/9] locking/lockdep: Traverse adjacency lists directly in zap_class()
[PATCH 8/9] locking/lockdep: Shrink static list_entries array to early bootstrap buffer
[PATCH 9/9] locking/lockdep: Migrate and compact boot-time dependency graph from __initdata

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
Jim Cromie (9):
      lib/folio_pool: Introduce Direct-Map Large Folio Pool & Scratchpad bump allocators
      netfilter/nf_tables: Add folio_scratchpad collector to struct nftables_pernet
      bpf/verifier: Route verifier stack state node allocations to folio_pool
      drm/gpuvm: Route gpuva_op allocations to folio_scratchpad
      bpf/syscall: Route generic_map_update_batch key/value allocations to folio_scratchpad
      locking/lockdep: Fallback to folio_pool in alloc_list_entry when static pool is full
      locking/lockdep: Traverse adjacency lists directly in zap_class()
      locking/lockdep: Shrink static list_entries array to early bootstrap buffer
      locking/lockdep: Migrate and compact boot-time dependency graph from __initdata

 drivers/gpu/drm/drm_gpuvm.c        |  11 +-
 include/drm/drm_gpuvm.h            |   6 +
 include/linux/bpf_verifier.h       |   3 +
 include/linux/folio_pool.h         | 279 +++++++++++++++++++++++++++++++++++++
 include/net/netfilter/nf_tables.h  |   3 +
 kernel/bpf/syscall.c               |  17 ++-
 kernel/bpf/verifier.c              |  16 ++-
 kernel/locking/lockdep.c           | 278 +++++++++++++++++++++++++++++-------
 kernel/locking/lockdep_internals.h |   2 +
 kernel/locking/lockdep_proc.c      |  16 ++-
 lib/Makefile                       |   2 +-
 lib/folio_pool.c                   | 230 ++++++++++++++++++++++++++++++
 net/netfilter/nf_tables_api.c      |  51 +++++--
 13 files changed, 837 insertions(+), 77 deletions(-)
---
base-commit: 1a7ac48cd3e62f281ce62f8ed89dc2cc8866eaa4
change-id: 20260815-folio-pool-v1-cb75c247692c

Best regards,
-- 
Jim Cromie <jim.cromie@gmail.com>




^ permalink raw reply	[flat|nested] 32+ messages in thread

* [PATCH 0/9] lib/folio_pool: Direct-Map Large Folio Pool & Scratchpad bump allocators
@ 2026-08-17 17:22 ` Jim Cromie
  0 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

Introduce two light-weight bump allocators backed directly by compound
folio pages from the buddy allocator (skipping SLUB metadata overhead):

1. struct folio_scratchpad - Variable-sized, alignment-aware bump allocator
   for bursty, append-mostly lifecycles with bulk teardown.
2. struct folio_pool - Fixed uniform-slot bump allocator for homogeneous
   descriptors.

These serve 2 kinds of use-cases:

1: Ephemeral Batch/transaction Lifecycles (Netfilter, BPF, DRM GPUVM)

- Transactional subsystems allocate bursts of hundreds or thousands of
  homogeneous or heterogeneous objects, only to tear them all down
  simultaneously at batch completion or error abort.

- Under SLUB: Each descriptor incurs freelist traversal, lock
  contention, and O(N) kfree() loops on teardown.

- Under Folio-Scratchpad: Allocations are straight-line pointer bumps,
  bulk teardown is O(1) folio_put(), and consecutive Netlink
  transactions reuse warm L1/L2 cachelines without buddy lock churn
  via folio_scratchpad_reset().

2: Dynamic Long-Lived Graph Scaling (Lockdep)

- Core subsystems historically avoided SLUB by statically
  preallocating massive compile-time arrays in .bss
  (e.g. list_entries[32768], 1.31 MB) to prevent SLUB allocator
  recursion deadlocks.

- Fixed arrays exhaust their limit and permanently disable validation
  (BUG: MAX_LOCKDEP_ENTRIES too low!).
  
- Under Folio-Pool: Lockdep uses a 4,096-entry __initdata bootstrap
  buffer for early boot prior to buddy initialization, then seamlessly
  migrates and compacts all dependency edges into 64 KB direct-map
  folios at late_initcall.
  
- Permanent static .bss allocation for dependency edges is reduced
  from 1.31 MB to 0 KB (100% reclamation at free_initmem()).

Core Primitives in include/linux/folio_pool.h & lib/folio_pool.c:
-----------------------------------------------------------------
0. struct folio_scratchpad (Variable-Sized Elements):
   - Dynamic alignment-aware bump pointer with zero padding waste.
   - API: folio_scratchpad_init(), folio_scratchpad_alloc(),
     folio_scratchpad_reset(), folio_scratchpad_free(),
     folio_scratchpad_stats().
   - Typed macros: folio_scratchpad_alloc_obj(),
     folio_scratchpad_alloc_bytes(), DEFINE_FREE(folio_scratchpad, ...).

1. struct folio_pool (Fixed Uniform Elements):
   - Thin wrapper embedding struct folio_scratchpad for homogeneous objects.
   - API: folio_pool_init(), folio_pool_alloc(), folio_pool_free(),
     folio_pool_stats().
   - Typed macros: folio_pool_alloc_obj(), folio_pool_alloc_type().

2. Encapsulated Slab Discrimination & Reallocation:
   - is_folio_pool_ptr(ptr): Direct-map folios are identified via
     virt_to_folio(ptr) && !folio_test_slab(f), avoiding custom bitflags.
   - folio_scratchpad_free_elem(ptr): Safe no-op for folio-backed objects;
     delegates to kvfree() for SLUB/vmalloc fallback objects.
   - folio_scratchpad_realloc(ptr, old_size, new_size, gfp): Delegates to
     krealloc() for SLUB objects; allocates a fresh SLUB buffer and copies
     payload for folio-backed descriptors.

3. Subsystem Autonomy & Runtime Static-Key Control:
   - DEFINE_FOLIO_POOL_STATIC_KEY_PARAM() allows maintainers of each
     subsystem (Netfilter, Lockdep, DRM, BPF) to choose their own default
     enablement policy (DECLARE_STATIC_KEY_TRUE vs FALSE) and independent
     module/boot parameters (e.g. lockdep.folio_pool, nf_tables.trans_scratchpad).
   - Enables isolated, zero-overhead A/B benchmarking and production triage
     without cross-subsystem coupling or kernel recompilation.

Active Proofs & Performance Profiling:
--------------------------------------
0. Virtualized Guest Netfilter Benchmark (KVM / virtme-ng ftrace):
   Controlled A/B benchmark across 10,025 Netlink transaction descriptors
   and 5 asynchronous commit/abort teardown passes:

   Function: nft_trans_alloc (10,025 descriptor allocations)
   -----------------------------------------------------------------------------
   Mode                                 Total Time       Avg / Alloc      Delta
   -----------------------------------------------------------------------------
   folio_scratchpad Enabled (Bump)      9,158.02 us          0.914 us    -19.4%
   SLUB Fallback (kzalloc)             11,366.53 us          1.134 us  Baseline
   -----------------------------------------------------------------------------
   Net allocation latency reduction:   -2,208.52 us (-2.21 ms)

   Function: nf_tables_trans_destroy_work (5 asynchronous teardown passes)
   -----------------------------------------------------------------------------
   Mode                                 Total Time       Avg / Pass       Delta
   -----------------------------------------------------------------------------
   folio_scratchpad Enabled (Bulk)     36,065.65 us          7.21 ms      -8.6%
   SLUB Fallback (O(N) kfree)          39,449.65 us          7.89 ms   Baseline
   -----------------------------------------------------------------------------
   Net teardown latency reduction:     -3,384.00 us (-3.38 ms)

   Combined In-Kernel Netlink Transaction Lifecycle Savings: -5.59 ms (-13.3%)

1. Bare-Metal Host Netfilter Profile (AMD Zen 3 Hardware PMU & ftrace):
   - Asynchronous Teardown: 53.77 ms vs 56.03 ms (-2.26 ms / -4.0%).
   - Instruction Retirement: 281.29M -> 282.46M (-1,174,062 instructions).
   - Branch Instructions:     62.94M ->  63.20M (-265,961 branches).

2. Bare-Metal Host Lockdep Telemetry (Physical Host frodo, AMD Zen SMP):
   - Pre-Buddy Watermark: 928/4096 bootstrap entries consumed during boot.
   - Graph Compaction: Migrated and compacted bootstrap dependency edges
     into folio_pool at late_initcall; bootstrap array reclaimed by
     free_initmem() (0 KB permanent .bss cost).
   - Live procfs Telemetry (/proc/lockdep_stats):
     lock-classes:                          996 [max: 8192]
     direct dependencies:                  2324 [dynamic: 7 x 64 kB, tail: 17 kB/64 kB]
     indirect dependencies:               24612
     all direct dependencies:             81884
     zapped classes:                          2
     zapped lock chains:                    173
   - Active Invariant Validation: Live module unloads exercised modernized
     zap_class() direct list unlinking across dynamic folios with zero
     global bitmap scans and zero assertions.

What's Unchanged:
-----------------
- All consumer object structures, alignment rules, and lifecycle boundaries
  remain strictly identical.
- Memory safety invariants are preserved: individual deallocation is a safe
  no-op for arena-backed objects while SLUB objects continue to use kfree().

Subsystem Adoptions in this Series:
-----------------------------------
0. lib/folio_pool: Core direct-map large-folio pool and scratchpad allocators.
1. netfilter/nf_tables: Pack netlink transaction descriptors into per-net
   trans_scratchpad with bulk reclaim & warm chunk reset.
2. bpf/verifier: Route verifier stack state node allocations to folio_pool.
3. drm/gpuvm: Route gpuva_op allocations to folio_scratchpad.
4. bpf/syscall: Route generic_map_update_batch key/value allocations to
   folio_scratchpad.
5. locking/lockdep: Fallback to folio_pool in alloc_list_entry when static
   pool is full and expose dynamic folio telemetry in /proc/lockdep_stats.
6. locking/lockdep: Traverse adjacency lists directly in zap_class().
7. locking/lockdep: Shrink static list_entries array to early bootstrap buffer.
8. locking/lockdep: Migrate and compact boot-time dependency graph from __initdata.

Patches in this series:
-----------------------
[PATCH 1/9] lib/folio_pool: Introduce Direct-Map Large Folio Pool & Scratchpad bump allocators
[PATCH 2/9] netfilter/nf_tables: Add folio_scratchpad collector to struct nftables_pernet
[PATCH 3/9] bpf/verifier: Route verifier stack state node allocations to folio_pool
[PATCH 4/9] drm/gpuvm: Route gpuva_op allocations to folio_scratchpad
[PATCH 5/9] bpf/syscall: Route generic_map_update_batch key/value allocations to folio_scratchpad
[PATCH 6/9] locking/lockdep: Fallback to folio_pool in alloc_list_entry when static pool is full
[PATCH 7/9] locking/lockdep: Traverse adjacency lists directly in zap_class()
[PATCH 8/9] locking/lockdep: Shrink static list_entries array to early bootstrap buffer
[PATCH 9/9] locking/lockdep: Migrate and compact boot-time dependency graph from __initdata

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
Jim Cromie (9):
      lib/folio_pool: Introduce Direct-Map Large Folio Pool & Scratchpad bump allocators
      netfilter/nf_tables: Add folio_scratchpad collector to struct nftables_pernet
      bpf/verifier: Route verifier stack state node allocations to folio_pool
      drm/gpuvm: Route gpuva_op allocations to folio_scratchpad
      bpf/syscall: Route generic_map_update_batch key/value allocations to folio_scratchpad
      locking/lockdep: Fallback to folio_pool in alloc_list_entry when static pool is full
      locking/lockdep: Traverse adjacency lists directly in zap_class()
      locking/lockdep: Shrink static list_entries array to early bootstrap buffer
      locking/lockdep: Migrate and compact boot-time dependency graph from __initdata

 drivers/gpu/drm/drm_gpuvm.c        |  11 +-
 include/drm/drm_gpuvm.h            |   6 +
 include/linux/bpf_verifier.h       |   3 +
 include/linux/folio_pool.h         | 279 +++++++++++++++++++++++++++++++++++++
 include/net/netfilter/nf_tables.h  |   3 +
 kernel/bpf/syscall.c               |  17 ++-
 kernel/bpf/verifier.c              |  16 ++-
 kernel/locking/lockdep.c           | 278 +++++++++++++++++++++++++++++-------
 kernel/locking/lockdep_internals.h |   2 +
 kernel/locking/lockdep_proc.c      |  16 ++-
 lib/Makefile                       |   2 +-
 lib/folio_pool.c                   | 230 ++++++++++++++++++++++++++++++
 net/netfilter/nf_tables_api.c      |  51 +++++--
 13 files changed, 837 insertions(+), 77 deletions(-)
---
base-commit: 1a7ac48cd3e62f281ce62f8ed89dc2cc8866eaa4
change-id: 20260815-folio-pool-v1-cb75c247692c

Best regards,
-- 
Jim Cromie <jim.cromie@gmail.com>


^ permalink raw reply	[flat|nested] 32+ messages in thread

* [PATCH 1/9] lib/folio_pool: Introduce Direct-Map Large Folio Pool & Scratchpad bump allocators
  2026-08-17 17:22 ` Jim Cromie
@ 2026-08-17 17:22   ` Jim Cromie
  -1 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie via B4 Relay @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

From: Jim Cromie <jim.cromie@gmail.com>

Introduce region-based linear bump allocators backed by Direct-Map Large
Folios (order-N compound pages via folio_alloc()), bringing O(1) bulk
teardown and SLUB bypass to batch-oriented kernel subsystems.

The implementation provides one core engine and one thin wrapper:

  - struct folio_scratchpad: Core variable-sized bump allocator. Accepts
    arbitrary size and alignment per allocation (alloc_bytes / alloc_obj).
    Used for Netlink batches, Netfilter transactions, and BPF syscalls.

  - struct folio_pool: Thin wrapper around folio_scratchpad that binds a
    fixed element size and alignment at init time (alloc / alloc_type).
    Used for homogeneous objects like Lockdep dependency edges and BPF
    verifier frames.

Subsystem Autonomy & Runtime Static-Key Control:
Both allocators support fine-grained, per-subsystem policy control via
DEFINE_FOLIO_POOL_STATIC_KEY_PARAM() alongside a global master switch
(folio_pool.enabled):

  - Independent Maintainer Policy: Subsystem maintainers choose their
    own default enablement policy (DECLARE_STATIC_KEY_TRUE or FALSE)
    and expose dedicated module/boot parameters (e.g. lockdep.folio_pool,
    nf_tables.trans_scratchpad, drm_gpuvm.scratchpad) to control adoption
    and rollout independently without cross-subsystem coupling.

  - Granular In-Situ Triage & A/B Benchmarking: Allows toggling between
    folio bump allocation and baseline SLUB at runtime on the same
    booted kernel, isolating regressions and capturing precise ftrace
    and PMU instruction deltas without rebooting.

  - Zero Instruction Overhead: Evaluated via static_branch_likely()
    to compile to direct fallthrough execution with static NOP patching.

Mark folio_scratchpad_alloc and folio_scratchpad_free noinline to enable
precise ftrace and hardware performance counter instrumentation.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 include/linux/folio_pool.h | 279 +++++++++++++++++++++++++++++++++++++++++++++
 lib/Makefile               |   2 +-
 lib/folio_pool.c           | 230 +++++++++++++++++++++++++++++++++++++
 3 files changed, 510 insertions(+), 1 deletion(-)

diff --git a/include/linux/folio_pool.h b/include/linux/folio_pool.h
new file mode 100644
index 000000000000..5bbca8818877
--- /dev/null
+++ b/include/linux/folio_pool.h
@@ -0,0 +1,279 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+#ifndef _LINUX_FOLIO_POOL_H
+#define _LINUX_FOLIO_POOL_H
+
+#include <linux/types.h>
+#include <linux/list.h>
+#include <linux/spinlock.h>
+#include <linux/page-flags.h>
+#include <linux/mm.h>
+#include <linux/gfp.h>
+#include <linux/jump_label.h>
+#include <linux/moduleparam.h>
+#include <linux/cleanup.h>
+
+DECLARE_STATIC_KEY_TRUE(folio_pool_enabled_key);
+
+#define DEFINE_FOLIO_POOL_STATIC_KEY_PARAM(key_name, param_name, desc)		\
+	DEFINE_STATIC_KEY_TRUE(key_name);					\
+	static int key_name##_set(const char *val, const struct kernel_param *kp) \
+	{									\
+		bool enable;							\
+		int ret = kstrtobool(val, &enable);				\
+		if (ret)							\
+			return ret;						\
+		if (enable)							\
+			static_branch_enable(&key_name);			\
+		else								\
+			static_branch_disable(&key_name);			\
+		return 0;							\
+	}									\
+	static int key_name##_get(char *buffer, const struct kernel_param *kp)	\
+	{									\
+		return sprintf(buffer, "%c\n",					\
+			       static_branch_likely(&key_name) ? 'Y' : 'N');	\
+	}									\
+	static const struct kernel_param_ops key_name##_ops = {			\
+		.set = key_name##_set,						\
+		.get = key_name##_get,						\
+	};									\
+	module_param_cb(param_name, &key_name##_ops, NULL, 0644);		\
+	MODULE_PARM_DESC(param_name, desc)
+
+#define DEFINE_FOLIO_POOL_STATIC_KEY_PARAM_FALSE(key_name, param_name, desc)	\
+	DEFINE_STATIC_KEY_FALSE(key_name);					\
+	static int key_name##_set(const char *val, const struct kernel_param *kp) \
+	{									\
+		bool enable;							\
+		int ret = kstrtobool(val, &enable);				\
+		if (ret)							\
+			return ret;						\
+		if (enable)							\
+			static_branch_enable(&key_name);			\
+		else								\
+			static_branch_disable(&key_name);			\
+		return 0;							\
+	}									\
+	static int key_name##_get(char *buffer, const struct kernel_param *kp)	\
+	{									\
+		return sprintf(buffer, "%c\n",					\
+			       static_branch_unlikely(&key_name) ? 'Y' : 'N');	\
+	}									\
+	static const struct kernel_param_ops key_name##_ops = {			\
+		.set = key_name##_set,						\
+		.get = key_name##_get,						\
+	};									\
+	module_param_cb(param_name, &key_name##_ops, NULL, 0644);		\
+	MODULE_PARM_DESC(param_name, desc)
+
+#define FOLIO_POOL_64K_ORDER (PAGE_SHIFT < 16 ? 16 - PAGE_SHIFT : 0)
+
+struct folio_pool_chunk {
+	struct list_head link;
+	struct folio *folio;
+};
+
+/*
+ * 1. Variable-Sized Scratchpad (Core Bump Allocator Engine)
+ */
+struct folio_scratchpad {
+	struct list_head chunks;
+	void *free_ptr;
+	size_t remaining;
+	unsigned int chunk_order;
+	struct static_key *key;
+	spinlock_t lock;
+};
+
+#define FOLIO_SCRATCHPAD_INIT(name, _order) {				\
+	.chunks = LIST_HEAD_INIT((name).chunks),			\
+	.chunk_order = (_order),					\
+	.key = NULL,							\
+	.lock = __SPIN_LOCK_UNLOCKED((name).lock),			\
+}
+
+#define FOLIO_SCRATCHPAD_INIT_KEY(name, _order, _key) {			\
+	.chunks = LIST_HEAD_INIT((name).chunks),			\
+	.chunk_order = (_order),					\
+	.key = (struct static_key *)(_key),				\
+	.lock = __SPIN_LOCK_UNLOCKED((name).lock),			\
+}
+
+void folio_scratchpad_init(struct folio_scratchpad *sp, unsigned int order);
+void folio_scratchpad_init_key(struct folio_scratchpad *sp, unsigned int order,
+			       struct static_key *key);
+void *folio_scratchpad_alloc(struct folio_scratchpad *sp, size_t size,
+			     size_t align, gfp_t gfp);
+void folio_scratchpad_reset(struct folio_scratchpad *sp);
+void folio_scratchpad_free(struct folio_scratchpad *sp);
+void folio_scratchpad_stats(struct folio_scratchpad *sp, unsigned int *nr_chunks,
+			    size_t *chunk_size, size_t *tail_used);
+
+DEFINE_FREE(folio_scratchpad, struct folio_scratchpad *, if (_T) folio_scratchpad_free(_T))
+
+/**
+ * is_folio_pool_ptr - Check whether an address resides in a folio pool/scratchpad
+ * @ptr: Object pointer to test
+ *
+ * Direct-map large folios allocated via folio_alloc() are not slab pages,
+ * unlike objects returned by kmalloc/kzalloc.
+ */
+static inline bool is_folio_pool_ptr(const void *ptr)
+{
+	return ptr && !is_vmalloc_addr(ptr) && !folio_test_slab(virt_to_folio(ptr));
+}
+
+/**
+ * folio_pool_free_elem - Safely release a pool object or SLUB fallback element
+ * @ptr: Object pointer to release
+ *
+ * If @ptr belongs to a direct-map large folio, individual deallocation is a safe
+ * no-op (reclaimed in bulk by folio_scratchpad_free/reset). If @ptr was allocated
+ * via SLUB/vmalloc fallback, releases it immediately.
+ */
+static inline void folio_pool_free_elem(const void *ptr)
+{
+	if (!ptr || is_folio_pool_ptr(ptr))
+		return;
+	kvfree(ptr);
+}
+
+static inline void folio_scratchpad_free_elem(const void *ptr)
+{
+	folio_pool_free_elem(ptr);
+}
+
+/**
+ * folio_pool_realloc - Reallocate memory for an object, handling pool vs slab backing
+ * @ptr: Existing object pointer (may be from folio_pool/scratchpad or SLUB)
+ * @old_size: Size of original object
+ * @new_size: Desired new size
+ * @gfp: Allocation flags
+ *
+ * If @ptr is SLUB-backed, delegates directly to krealloc(). If @ptr resides
+ * in a direct-map large folio, allocates a fresh @new_size buffer from SLUB
+ * and copies @old_size bytes; the original scratchpad slot remains abandoned
+ * until the entire scratchpad is released or reset at batch boundary.
+ */
+static inline void *folio_pool_realloc(void *ptr, size_t old_size,
+				       size_t new_size, gfp_t gfp)
+{
+	void *new_ptr;
+
+	if (!ptr)
+		return kmalloc(new_size, gfp);
+
+	if (!is_folio_pool_ptr(ptr))
+		return krealloc(ptr, new_size, gfp);
+
+	new_ptr = kmalloc(new_size, gfp);
+	if (new_ptr)
+		memcpy(new_ptr, ptr, min(old_size, new_size));
+	return new_ptr;
+}
+
+static inline void *folio_scratchpad_realloc(void *ptr, size_t old_size,
+					     size_t new_size, gfp_t gfp)
+{
+	return folio_pool_realloc(ptr, old_size, new_size, gfp);
+}
+
+/**
+ * folio_scratchpad_alloc_obj - Allocate a typed object from an embedded scratchpad
+ * @ptr: Pointer to container struct (e.g. nft_net)
+ * @member: Name of the struct folio_scratchpad field (e.g. trans_scratchpad)
+ * @type: Type of object being allocated
+ * @gfp: Allocation flags
+ */
+#define folio_scratchpad_alloc_obj(ptr, member, type, gfp)			\
+	((type *)folio_scratchpad_alloc(&(ptr)->member,				\
+					sizeof(type),				\
+					__alignof__(type),			\
+					gfp))
+
+/**
+ * folio_scratchpad_alloc_bytes - Allocate variable-sized bytes from an embedded scratchpad
+ * @ptr: Pointer to container struct (e.g. nft_net)
+ * @member: Name of the struct folio_scratchpad field (e.g. trans_scratchpad)
+ * @size: Size of memory to allocate
+ * @align: Alignment requirement
+ * @gfp: Allocation flags
+ */
+#define folio_scratchpad_alloc_bytes(ptr, member, size, align, gfp)		\
+	folio_scratchpad_alloc(&(ptr)->member, size, align, gfp)
+
+/**
+ * folio_scratchpad_alloc_type - Allocate a typed object from a scratchpad pointer
+ * @sp: Pointer to struct folio_scratchpad
+ * @type: Type of object being allocated
+ * @gfp: Allocation flags
+ */
+#define folio_scratchpad_alloc_type(sp, type, gfp)				\
+	((type *)folio_scratchpad_alloc(sp,					\
+					sizeof(type),				\
+					__alignof__(type),			\
+					gfp))
+
+/*
+ * 2. Fixed-Slot Uniform Pool (Specialized Thin Wrapper on Scratchpad)
+ */
+struct folio_pool {
+	struct folio_scratchpad base;
+	size_t elem_size;
+	size_t elem_align;
+};
+
+#define FOLIO_POOL_INIT(name, _elem_size, _order) {			\
+	.base = FOLIO_SCRATCHPAD_INIT((name).base, _order),		\
+	.elem_size = (_elem_size),					\
+	.elem_align = ((_elem_size) > sizeof(void *) ? (_elem_size) : sizeof(void *)), \
+}
+
+#define FOLIO_POOL_INIT_KEY(name, _elem_size, _order, _key) {		\
+	.base = FOLIO_SCRATCHPAD_INIT_KEY((name).base, _order, _key),	\
+	.elem_size = (_elem_size),					\
+	.elem_align = ((_elem_size) > sizeof(void *) ? (_elem_size) : sizeof(void *)), \
+}
+
+void folio_pool_init(struct folio_pool *fp, size_t elem_size, unsigned int order);
+void folio_pool_init_key(struct folio_pool *fp, size_t elem_size, unsigned int order,
+			 struct static_key *key);
+void folio_pool_init_align(struct folio_pool *fp, size_t elem_size,
+			   size_t elem_align, unsigned int order);
+
+static inline void *folio_pool_alloc(struct folio_pool *fp, gfp_t gfp)
+{
+	return folio_scratchpad_alloc(&fp->base, fp->elem_size, fp->elem_align, gfp);
+}
+
+static inline void folio_pool_free(struct folio_pool *fp)
+{
+	folio_scratchpad_free(&fp->base);
+}
+
+static inline void folio_pool_stats(struct folio_pool *fp, unsigned int *nr_chunks,
+				    size_t *chunk_size, size_t *tail_used)
+{
+	folio_scratchpad_stats(&fp->base, nr_chunks, chunk_size, tail_used);
+}
+
+/**
+ * folio_pool_alloc_obj - Allocate a typed object from a container's embedded folio_pool
+ * @ptr: Pointer to container struct (e.g. env)
+ * @member: Name of the struct folio_pool field (e.g. state_pool)
+ * @type: Type of object being allocated (e.g. struct bpf_verifier_stack_elem)
+ * @gfp: Allocation flags
+ */
+#define folio_pool_alloc_obj(ptr, member, type, gfp)				\
+	((type *)folio_pool_alloc(&(ptr)->member, gfp))
+
+/**
+ * folio_pool_alloc_type - Allocate a typed object from a struct folio_pool pointer
+ * @fp: Pointer to struct folio_pool
+ * @type: Type of object being allocated
+ * @gfp: Allocation flags
+ */
+#define folio_pool_alloc_type(fp, type, gfp)					\
+	((type *)folio_pool_alloc(fp, gfp))
+
+#endif /* _LINUX_FOLIO_POOL_H */
diff --git a/lib/Makefile b/lib/Makefile
index 7f75cc6edf94..b5f2f41f96fb 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -58,7 +58,7 @@ obj-y += bcd.o sort.o parser.o debug_locks.o random32.o \
 	 bsearch.o find_bit.o llist.o lwq.o memweight.o kfifo.o \
 	 percpu-refcount.o rhashtable.o base64.o \
 	 once.o refcount.o rcuref.o usercopy.o errseq.o bucket_locks.o \
-	 generic-radix-tree.o bitmap-str.o
+	 generic-radix-tree.o bitmap-str.o folio_pool.o
 obj-y += string_helpers.o
 obj-y += hexdump.o
 obj-$(CONFIG_TEST_HEXDUMP) += test_hexdump.o
diff --git a/lib/folio_pool.c b/lib/folio_pool.c
new file mode 100644
index 000000000000..07a290db6c50
--- /dev/null
+++ b/lib/folio_pool.c
@@ -0,0 +1,230 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Direct-Map Large Folio Scratchpad & Pool bump allocators.
+ */
+#include <linux/export.h>
+#include <linux/string.h>
+#include <linux/moduleparam.h>
+#include <linux/folio_pool.h>
+
+DEFINE_STATIC_KEY_TRUE(folio_pool_enabled_key);
+EXPORT_SYMBOL_GPL(folio_pool_enabled_key);
+
+static int folio_pool_enabled_set(const char *val, const struct kernel_param *kp)
+{
+	bool enable;
+	int ret = kstrtobool(val, &enable);
+
+	if (ret)
+		return ret;
+
+	if (enable)
+		static_branch_enable(&folio_pool_enabled_key);
+	else
+		static_branch_disable(&folio_pool_enabled_key);
+
+	return 0;
+}
+
+static int folio_pool_enabled_get(char *buffer, const struct kernel_param *kp)
+{
+	return sprintf(buffer, "%c\n", static_branch_likely(&folio_pool_enabled_key) ? 'Y' : 'N');
+}
+
+static const struct kernel_param_ops folio_pool_enabled_ops = {
+	.set = folio_pool_enabled_set,
+	.get = folio_pool_enabled_get,
+};
+
+module_param_cb(enabled, &folio_pool_enabled_ops, NULL, 0644);
+MODULE_PARM_DESC(enabled, "Toggle folio_pool/scratchpad bump allocator (0 = fallback to SLUB)");
+
+/*
+ * 1. Variable-Sized Scratchpad (Core Engine)
+ */
+void folio_scratchpad_init_key(struct folio_scratchpad *sp, unsigned int order,
+			       struct static_key *key)
+{
+	INIT_LIST_HEAD(&sp->chunks);
+	sp->free_ptr = NULL;
+	sp->remaining = 0;
+	sp->chunk_order = order;
+	sp->key = key;
+	spin_lock_init(&sp->lock);
+}
+EXPORT_SYMBOL_GPL(folio_scratchpad_init_key);
+
+void folio_scratchpad_init(struct folio_scratchpad *sp, unsigned int order)
+{
+	folio_scratchpad_init_key(sp, order, NULL);
+}
+EXPORT_SYMBOL_GPL(folio_scratchpad_init);
+
+static inline bool folio_scratchpad_is_enabled(const struct folio_scratchpad *sp)
+{
+	if (sp->key)
+		return static_key_enabled(sp->key);
+	return static_branch_likely(&folio_pool_enabled_key);
+}
+
+noinline void *folio_scratchpad_alloc(struct folio_scratchpad *sp, size_t size,
+				     size_t align, gfp_t gfp)
+{
+	struct folio_pool_chunk *chunk;
+	struct folio *folio;
+	void *elem, *base;
+	size_t chunk_size, aligned_size, pad, header_offset;
+	unsigned long flags;
+
+	if (!folio_scratchpad_is_enabled(sp))
+		return kvzalloc(size, gfp);
+
+	if (unlikely(!size))
+		return NULL;
+
+	align = max_t(size_t, sizeof(void *), align ? align : sizeof(void *));
+
+	spin_lock_irqsave(&sp->lock, flags);
+	pad = (uintptr_t)sp->free_ptr & (align - 1);
+	if (pad)
+		pad = align - pad;
+	aligned_size = size + pad;
+
+	if (sp->remaining < aligned_size) {
+		spin_unlock_irqrestore(&sp->lock, flags);
+
+		folio = folio_alloc(gfp, sp->chunk_order);
+		if (!folio && sp->chunk_order > 0)
+			folio = folio_alloc(gfp, 0);
+		if (!folio)
+			return NULL;
+
+		base = folio_address(folio);
+		chunk = (struct folio_pool_chunk *)base;
+		chunk->folio = folio;
+		chunk_size = folio_size(folio);
+		header_offset = ALIGN(sizeof(*chunk), max_t(size_t, sizeof(void *), align));
+
+		spin_lock_irqsave(&sp->lock, flags);
+		list_add(&chunk->link, &sp->chunks);
+		sp->free_ptr = base + header_offset;
+		sp->remaining = chunk_size - header_offset;
+
+		pad = (uintptr_t)sp->free_ptr & (align - 1);
+		if (pad)
+			pad = align - pad;
+		aligned_size = size + pad;
+	}
+
+	if (sp->remaining < aligned_size) {
+		spin_unlock_irqrestore(&sp->lock, flags);
+		return NULL;
+	}
+
+	elem = sp->free_ptr + pad;
+	sp->free_ptr += aligned_size;
+	sp->remaining -= aligned_size;
+	spin_unlock_irqrestore(&sp->lock, flags);
+
+	memset(elem, 0, size);
+	return elem;
+}
+EXPORT_SYMBOL_GPL(folio_scratchpad_alloc);
+
+noinline void folio_scratchpad_reset(struct folio_scratchpad *sp)
+{
+	struct folio_pool_chunk *head, *chunk, *tmp;
+	size_t header_offset;
+	unsigned long flags;
+
+	spin_lock_irqsave(&sp->lock, flags);
+	if (list_empty(&sp->chunks)) {
+		sp->free_ptr = NULL;
+		sp->remaining = 0;
+		spin_unlock_irqrestore(&sp->lock, flags);
+		return;
+	}
+
+	/* Retain primary head chunk; free overflow chunks */
+	head = list_first_entry(&sp->chunks, struct folio_pool_chunk, link);
+	list_for_each_entry_safe(chunk, tmp, &sp->chunks, link) {
+		if (chunk == head)
+			continue;
+		list_del(&chunk->link);
+		folio_put(chunk->folio);
+	}
+
+	header_offset = ALIGN(sizeof(*head), sizeof(void *));
+	sp->free_ptr = folio_address(head->folio) + header_offset;
+	sp->remaining = folio_size(head->folio) - header_offset;
+	spin_unlock_irqrestore(&sp->lock, flags);
+}
+EXPORT_SYMBOL_GPL(folio_scratchpad_reset);
+
+noinline void folio_scratchpad_free(struct folio_scratchpad *sp)
+{
+	struct folio_pool_chunk *chunk, *tmp;
+	struct folio *folio;
+	unsigned long flags;
+
+	spin_lock_irqsave(&sp->lock, flags);
+	sp->free_ptr = NULL;
+	sp->remaining = 0;
+	list_for_each_entry_safe(chunk, tmp, &sp->chunks, link) {
+		folio = chunk->folio;
+		list_del(&chunk->link);
+		folio_put(folio);
+	}
+	spin_unlock_irqrestore(&sp->lock, flags);
+}
+EXPORT_SYMBOL_GPL(folio_scratchpad_free);
+
+/*
+ * 2. Fixed-Slot Uniform Pool (Specialized Thin Wrapper on Scratchpad)
+ */
+void folio_pool_init_align(struct folio_pool *fp, size_t elem_size,
+			   size_t elem_align, unsigned int order)
+{
+	folio_scratchpad_init(&fp->base, order);
+	fp->elem_size = elem_size;
+	fp->elem_align = max_t(size_t, sizeof(void *), elem_align ? elem_align : sizeof(void *));
+}
+EXPORT_SYMBOL_GPL(folio_pool_init_align);
+
+void folio_pool_init_key(struct folio_pool *fp, size_t elem_size, unsigned int order,
+			 struct static_key *key)
+{
+	size_t align = sizeof(void *);
+
+	if (elem_size && is_power_of_2(elem_size))
+		align = max_t(size_t, sizeof(void *), elem_size);
+	folio_scratchpad_init_key(&fp->base, order, key);
+	fp->elem_size = elem_size;
+	fp->elem_align = align;
+}
+EXPORT_SYMBOL_GPL(folio_pool_init_key);
+
+void folio_pool_init(struct folio_pool *fp, size_t elem_size, unsigned int order)
+{
+	folio_pool_init_key(fp, elem_size, order, NULL);
+}
+EXPORT_SYMBOL_GPL(folio_pool_init);
+
+void folio_scratchpad_stats(struct folio_scratchpad *sp, unsigned int *nr_chunks,
+			    size_t *chunk_size, size_t *tail_used)
+{
+	unsigned long flags;
+	size_t csz;
+
+	csz = (PAGE_SIZE << sp->chunk_order);
+	if (chunk_size)
+		*chunk_size = csz;
+
+	spin_lock_irqsave(&sp->lock, flags);
+	if (nr_chunks)
+		*nr_chunks = list_count_nodes(&sp->chunks);
+	if (tail_used)
+		*tail_used = csz > sp->remaining ? csz - sp->remaining : 0;
+	spin_unlock_irqrestore(&sp->lock, flags);
+}
+EXPORT_SYMBOL_GPL(folio_scratchpad_stats);

-- 
2.55.0




^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 1/9] lib/folio_pool: Introduce Direct-Map Large Folio Pool & Scratchpad bump allocators
@ 2026-08-17 17:22   ` Jim Cromie
  0 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

Introduce region-based linear bump allocators backed by Direct-Map Large
Folios (order-N compound pages via folio_alloc()), bringing O(1) bulk
teardown and SLUB bypass to batch-oriented kernel subsystems.

The implementation provides one core engine and one thin wrapper:

  - struct folio_scratchpad: Core variable-sized bump allocator. Accepts
    arbitrary size and alignment per allocation (alloc_bytes / alloc_obj).
    Used for Netlink batches, Netfilter transactions, and BPF syscalls.

  - struct folio_pool: Thin wrapper around folio_scratchpad that binds a
    fixed element size and alignment at init time (alloc / alloc_type).
    Used for homogeneous objects like Lockdep dependency edges and BPF
    verifier frames.

Subsystem Autonomy & Runtime Static-Key Control:
Both allocators support fine-grained, per-subsystem policy control via
DEFINE_FOLIO_POOL_STATIC_KEY_PARAM() alongside a global master switch
(folio_pool.enabled):

  - Independent Maintainer Policy: Subsystem maintainers choose their
    own default enablement policy (DECLARE_STATIC_KEY_TRUE or FALSE)
    and expose dedicated module/boot parameters (e.g. lockdep.folio_pool,
    nf_tables.trans_scratchpad, drm_gpuvm.scratchpad) to control adoption
    and rollout independently without cross-subsystem coupling.

  - Granular In-Situ Triage & A/B Benchmarking: Allows toggling between
    folio bump allocation and baseline SLUB at runtime on the same
    booted kernel, isolating regressions and capturing precise ftrace
    and PMU instruction deltas without rebooting.

  - Zero Instruction Overhead: Evaluated via static_branch_likely()
    to compile to direct fallthrough execution with static NOP patching.

Mark folio_scratchpad_alloc and folio_scratchpad_free noinline to enable
precise ftrace and hardware performance counter instrumentation.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 include/linux/folio_pool.h | 279 +++++++++++++++++++++++++++++++++++++++++++++
 lib/Makefile               |   2 +-
 lib/folio_pool.c           | 230 +++++++++++++++++++++++++++++++++++++
 3 files changed, 510 insertions(+), 1 deletion(-)

diff --git a/include/linux/folio_pool.h b/include/linux/folio_pool.h
new file mode 100644
index 000000000000..5bbca8818877
--- /dev/null
+++ b/include/linux/folio_pool.h
@@ -0,0 +1,279 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+#ifndef _LINUX_FOLIO_POOL_H
+#define _LINUX_FOLIO_POOL_H
+
+#include <linux/types.h>
+#include <linux/list.h>
+#include <linux/spinlock.h>
+#include <linux/page-flags.h>
+#include <linux/mm.h>
+#include <linux/gfp.h>
+#include <linux/jump_label.h>
+#include <linux/moduleparam.h>
+#include <linux/cleanup.h>
+
+DECLARE_STATIC_KEY_TRUE(folio_pool_enabled_key);
+
+#define DEFINE_FOLIO_POOL_STATIC_KEY_PARAM(key_name, param_name, desc)		\
+	DEFINE_STATIC_KEY_TRUE(key_name);					\
+	static int key_name##_set(const char *val, const struct kernel_param *kp) \
+	{									\
+		bool enable;							\
+		int ret = kstrtobool(val, &enable);				\
+		if (ret)							\
+			return ret;						\
+		if (enable)							\
+			static_branch_enable(&key_name);			\
+		else								\
+			static_branch_disable(&key_name);			\
+		return 0;							\
+	}									\
+	static int key_name##_get(char *buffer, const struct kernel_param *kp)	\
+	{									\
+		return sprintf(buffer, "%c\n",					\
+			       static_branch_likely(&key_name) ? 'Y' : 'N');	\
+	}									\
+	static const struct kernel_param_ops key_name##_ops = {			\
+		.set = key_name##_set,						\
+		.get = key_name##_get,						\
+	};									\
+	module_param_cb(param_name, &key_name##_ops, NULL, 0644);		\
+	MODULE_PARM_DESC(param_name, desc)
+
+#define DEFINE_FOLIO_POOL_STATIC_KEY_PARAM_FALSE(key_name, param_name, desc)	\
+	DEFINE_STATIC_KEY_FALSE(key_name);					\
+	static int key_name##_set(const char *val, const struct kernel_param *kp) \
+	{									\
+		bool enable;							\
+		int ret = kstrtobool(val, &enable);				\
+		if (ret)							\
+			return ret;						\
+		if (enable)							\
+			static_branch_enable(&key_name);			\
+		else								\
+			static_branch_disable(&key_name);			\
+		return 0;							\
+	}									\
+	static int key_name##_get(char *buffer, const struct kernel_param *kp)	\
+	{									\
+		return sprintf(buffer, "%c\n",					\
+			       static_branch_unlikely(&key_name) ? 'Y' : 'N');	\
+	}									\
+	static const struct kernel_param_ops key_name##_ops = {			\
+		.set = key_name##_set,						\
+		.get = key_name##_get,						\
+	};									\
+	module_param_cb(param_name, &key_name##_ops, NULL, 0644);		\
+	MODULE_PARM_DESC(param_name, desc)
+
+#define FOLIO_POOL_64K_ORDER (PAGE_SHIFT < 16 ? 16 - PAGE_SHIFT : 0)
+
+struct folio_pool_chunk {
+	struct list_head link;
+	struct folio *folio;
+};
+
+/*
+ * 1. Variable-Sized Scratchpad (Core Bump Allocator Engine)
+ */
+struct folio_scratchpad {
+	struct list_head chunks;
+	void *free_ptr;
+	size_t remaining;
+	unsigned int chunk_order;
+	struct static_key *key;
+	spinlock_t lock;
+};
+
+#define FOLIO_SCRATCHPAD_INIT(name, _order) {				\
+	.chunks = LIST_HEAD_INIT((name).chunks),			\
+	.chunk_order = (_order),					\
+	.key = NULL,							\
+	.lock = __SPIN_LOCK_UNLOCKED((name).lock),			\
+}
+
+#define FOLIO_SCRATCHPAD_INIT_KEY(name, _order, _key) {			\
+	.chunks = LIST_HEAD_INIT((name).chunks),			\
+	.chunk_order = (_order),					\
+	.key = (struct static_key *)(_key),				\
+	.lock = __SPIN_LOCK_UNLOCKED((name).lock),			\
+}
+
+void folio_scratchpad_init(struct folio_scratchpad *sp, unsigned int order);
+void folio_scratchpad_init_key(struct folio_scratchpad *sp, unsigned int order,
+			       struct static_key *key);
+void *folio_scratchpad_alloc(struct folio_scratchpad *sp, size_t size,
+			     size_t align, gfp_t gfp);
+void folio_scratchpad_reset(struct folio_scratchpad *sp);
+void folio_scratchpad_free(struct folio_scratchpad *sp);
+void folio_scratchpad_stats(struct folio_scratchpad *sp, unsigned int *nr_chunks,
+			    size_t *chunk_size, size_t *tail_used);
+
+DEFINE_FREE(folio_scratchpad, struct folio_scratchpad *, if (_T) folio_scratchpad_free(_T))
+
+/**
+ * is_folio_pool_ptr - Check whether an address resides in a folio pool/scratchpad
+ * @ptr: Object pointer to test
+ *
+ * Direct-map large folios allocated via folio_alloc() are not slab pages,
+ * unlike objects returned by kmalloc/kzalloc.
+ */
+static inline bool is_folio_pool_ptr(const void *ptr)
+{
+	return ptr && !is_vmalloc_addr(ptr) && !folio_test_slab(virt_to_folio(ptr));
+}
+
+/**
+ * folio_pool_free_elem - Safely release a pool object or SLUB fallback element
+ * @ptr: Object pointer to release
+ *
+ * If @ptr belongs to a direct-map large folio, individual deallocation is a safe
+ * no-op (reclaimed in bulk by folio_scratchpad_free/reset). If @ptr was allocated
+ * via SLUB/vmalloc fallback, releases it immediately.
+ */
+static inline void folio_pool_free_elem(const void *ptr)
+{
+	if (!ptr || is_folio_pool_ptr(ptr))
+		return;
+	kvfree(ptr);
+}
+
+static inline void folio_scratchpad_free_elem(const void *ptr)
+{
+	folio_pool_free_elem(ptr);
+}
+
+/**
+ * folio_pool_realloc - Reallocate memory for an object, handling pool vs slab backing
+ * @ptr: Existing object pointer (may be from folio_pool/scratchpad or SLUB)
+ * @old_size: Size of original object
+ * @new_size: Desired new size
+ * @gfp: Allocation flags
+ *
+ * If @ptr is SLUB-backed, delegates directly to krealloc(). If @ptr resides
+ * in a direct-map large folio, allocates a fresh @new_size buffer from SLUB
+ * and copies @old_size bytes; the original scratchpad slot remains abandoned
+ * until the entire scratchpad is released or reset at batch boundary.
+ */
+static inline void *folio_pool_realloc(void *ptr, size_t old_size,
+				       size_t new_size, gfp_t gfp)
+{
+	void *new_ptr;
+
+	if (!ptr)
+		return kmalloc(new_size, gfp);
+
+	if (!is_folio_pool_ptr(ptr))
+		return krealloc(ptr, new_size, gfp);
+
+	new_ptr = kmalloc(new_size, gfp);
+	if (new_ptr)
+		memcpy(new_ptr, ptr, min(old_size, new_size));
+	return new_ptr;
+}
+
+static inline void *folio_scratchpad_realloc(void *ptr, size_t old_size,
+					     size_t new_size, gfp_t gfp)
+{
+	return folio_pool_realloc(ptr, old_size, new_size, gfp);
+}
+
+/**
+ * folio_scratchpad_alloc_obj - Allocate a typed object from an embedded scratchpad
+ * @ptr: Pointer to container struct (e.g. nft_net)
+ * @member: Name of the struct folio_scratchpad field (e.g. trans_scratchpad)
+ * @type: Type of object being allocated
+ * @gfp: Allocation flags
+ */
+#define folio_scratchpad_alloc_obj(ptr, member, type, gfp)			\
+	((type *)folio_scratchpad_alloc(&(ptr)->member,				\
+					sizeof(type),				\
+					__alignof__(type),			\
+					gfp))
+
+/**
+ * folio_scratchpad_alloc_bytes - Allocate variable-sized bytes from an embedded scratchpad
+ * @ptr: Pointer to container struct (e.g. nft_net)
+ * @member: Name of the struct folio_scratchpad field (e.g. trans_scratchpad)
+ * @size: Size of memory to allocate
+ * @align: Alignment requirement
+ * @gfp: Allocation flags
+ */
+#define folio_scratchpad_alloc_bytes(ptr, member, size, align, gfp)		\
+	folio_scratchpad_alloc(&(ptr)->member, size, align, gfp)
+
+/**
+ * folio_scratchpad_alloc_type - Allocate a typed object from a scratchpad pointer
+ * @sp: Pointer to struct folio_scratchpad
+ * @type: Type of object being allocated
+ * @gfp: Allocation flags
+ */
+#define folio_scratchpad_alloc_type(sp, type, gfp)				\
+	((type *)folio_scratchpad_alloc(sp,					\
+					sizeof(type),				\
+					__alignof__(type),			\
+					gfp))
+
+/*
+ * 2. Fixed-Slot Uniform Pool (Specialized Thin Wrapper on Scratchpad)
+ */
+struct folio_pool {
+	struct folio_scratchpad base;
+	size_t elem_size;
+	size_t elem_align;
+};
+
+#define FOLIO_POOL_INIT(name, _elem_size, _order) {			\
+	.base = FOLIO_SCRATCHPAD_INIT((name).base, _order),		\
+	.elem_size = (_elem_size),					\
+	.elem_align = ((_elem_size) > sizeof(void *) ? (_elem_size) : sizeof(void *)), \
+}
+
+#define FOLIO_POOL_INIT_KEY(name, _elem_size, _order, _key) {		\
+	.base = FOLIO_SCRATCHPAD_INIT_KEY((name).base, _order, _key),	\
+	.elem_size = (_elem_size),					\
+	.elem_align = ((_elem_size) > sizeof(void *) ? (_elem_size) : sizeof(void *)), \
+}
+
+void folio_pool_init(struct folio_pool *fp, size_t elem_size, unsigned int order);
+void folio_pool_init_key(struct folio_pool *fp, size_t elem_size, unsigned int order,
+			 struct static_key *key);
+void folio_pool_init_align(struct folio_pool *fp, size_t elem_size,
+			   size_t elem_align, unsigned int order);
+
+static inline void *folio_pool_alloc(struct folio_pool *fp, gfp_t gfp)
+{
+	return folio_scratchpad_alloc(&fp->base, fp->elem_size, fp->elem_align, gfp);
+}
+
+static inline void folio_pool_free(struct folio_pool *fp)
+{
+	folio_scratchpad_free(&fp->base);
+}
+
+static inline void folio_pool_stats(struct folio_pool *fp, unsigned int *nr_chunks,
+				    size_t *chunk_size, size_t *tail_used)
+{
+	folio_scratchpad_stats(&fp->base, nr_chunks, chunk_size, tail_used);
+}
+
+/**
+ * folio_pool_alloc_obj - Allocate a typed object from a container's embedded folio_pool
+ * @ptr: Pointer to container struct (e.g. env)
+ * @member: Name of the struct folio_pool field (e.g. state_pool)
+ * @type: Type of object being allocated (e.g. struct bpf_verifier_stack_elem)
+ * @gfp: Allocation flags
+ */
+#define folio_pool_alloc_obj(ptr, member, type, gfp)				\
+	((type *)folio_pool_alloc(&(ptr)->member, gfp))
+
+/**
+ * folio_pool_alloc_type - Allocate a typed object from a struct folio_pool pointer
+ * @fp: Pointer to struct folio_pool
+ * @type: Type of object being allocated
+ * @gfp: Allocation flags
+ */
+#define folio_pool_alloc_type(fp, type, gfp)					\
+	((type *)folio_pool_alloc(fp, gfp))
+
+#endif /* _LINUX_FOLIO_POOL_H */
diff --git a/lib/Makefile b/lib/Makefile
index 7f75cc6edf94..b5f2f41f96fb 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -58,7 +58,7 @@ obj-y += bcd.o sort.o parser.o debug_locks.o random32.o \
 	 bsearch.o find_bit.o llist.o lwq.o memweight.o kfifo.o \
 	 percpu-refcount.o rhashtable.o base64.o \
 	 once.o refcount.o rcuref.o usercopy.o errseq.o bucket_locks.o \
-	 generic-radix-tree.o bitmap-str.o
+	 generic-radix-tree.o bitmap-str.o folio_pool.o
 obj-y += string_helpers.o
 obj-y += hexdump.o
 obj-$(CONFIG_TEST_HEXDUMP) += test_hexdump.o
diff --git a/lib/folio_pool.c b/lib/folio_pool.c
new file mode 100644
index 000000000000..07a290db6c50
--- /dev/null
+++ b/lib/folio_pool.c
@@ -0,0 +1,230 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Direct-Map Large Folio Scratchpad & Pool bump allocators.
+ */
+#include <linux/export.h>
+#include <linux/string.h>
+#include <linux/moduleparam.h>
+#include <linux/folio_pool.h>
+
+DEFINE_STATIC_KEY_TRUE(folio_pool_enabled_key);
+EXPORT_SYMBOL_GPL(folio_pool_enabled_key);
+
+static int folio_pool_enabled_set(const char *val, const struct kernel_param *kp)
+{
+	bool enable;
+	int ret = kstrtobool(val, &enable);
+
+	if (ret)
+		return ret;
+
+	if (enable)
+		static_branch_enable(&folio_pool_enabled_key);
+	else
+		static_branch_disable(&folio_pool_enabled_key);
+
+	return 0;
+}
+
+static int folio_pool_enabled_get(char *buffer, const struct kernel_param *kp)
+{
+	return sprintf(buffer, "%c\n", static_branch_likely(&folio_pool_enabled_key) ? 'Y' : 'N');
+}
+
+static const struct kernel_param_ops folio_pool_enabled_ops = {
+	.set = folio_pool_enabled_set,
+	.get = folio_pool_enabled_get,
+};
+
+module_param_cb(enabled, &folio_pool_enabled_ops, NULL, 0644);
+MODULE_PARM_DESC(enabled, "Toggle folio_pool/scratchpad bump allocator (0 = fallback to SLUB)");
+
+/*
+ * 1. Variable-Sized Scratchpad (Core Engine)
+ */
+void folio_scratchpad_init_key(struct folio_scratchpad *sp, unsigned int order,
+			       struct static_key *key)
+{
+	INIT_LIST_HEAD(&sp->chunks);
+	sp->free_ptr = NULL;
+	sp->remaining = 0;
+	sp->chunk_order = order;
+	sp->key = key;
+	spin_lock_init(&sp->lock);
+}
+EXPORT_SYMBOL_GPL(folio_scratchpad_init_key);
+
+void folio_scratchpad_init(struct folio_scratchpad *sp, unsigned int order)
+{
+	folio_scratchpad_init_key(sp, order, NULL);
+}
+EXPORT_SYMBOL_GPL(folio_scratchpad_init);
+
+static inline bool folio_scratchpad_is_enabled(const struct folio_scratchpad *sp)
+{
+	if (sp->key)
+		return static_key_enabled(sp->key);
+	return static_branch_likely(&folio_pool_enabled_key);
+}
+
+noinline void *folio_scratchpad_alloc(struct folio_scratchpad *sp, size_t size,
+				     size_t align, gfp_t gfp)
+{
+	struct folio_pool_chunk *chunk;
+	struct folio *folio;
+	void *elem, *base;
+	size_t chunk_size, aligned_size, pad, header_offset;
+	unsigned long flags;
+
+	if (!folio_scratchpad_is_enabled(sp))
+		return kvzalloc(size, gfp);
+
+	if (unlikely(!size))
+		return NULL;
+
+	align = max_t(size_t, sizeof(void *), align ? align : sizeof(void *));
+
+	spin_lock_irqsave(&sp->lock, flags);
+	pad = (uintptr_t)sp->free_ptr & (align - 1);
+	if (pad)
+		pad = align - pad;
+	aligned_size = size + pad;
+
+	if (sp->remaining < aligned_size) {
+		spin_unlock_irqrestore(&sp->lock, flags);
+
+		folio = folio_alloc(gfp, sp->chunk_order);
+		if (!folio && sp->chunk_order > 0)
+			folio = folio_alloc(gfp, 0);
+		if (!folio)
+			return NULL;
+
+		base = folio_address(folio);
+		chunk = (struct folio_pool_chunk *)base;
+		chunk->folio = folio;
+		chunk_size = folio_size(folio);
+		header_offset = ALIGN(sizeof(*chunk), max_t(size_t, sizeof(void *), align));
+
+		spin_lock_irqsave(&sp->lock, flags);
+		list_add(&chunk->link, &sp->chunks);
+		sp->free_ptr = base + header_offset;
+		sp->remaining = chunk_size - header_offset;
+
+		pad = (uintptr_t)sp->free_ptr & (align - 1);
+		if (pad)
+			pad = align - pad;
+		aligned_size = size + pad;
+	}
+
+	if (sp->remaining < aligned_size) {
+		spin_unlock_irqrestore(&sp->lock, flags);
+		return NULL;
+	}
+
+	elem = sp->free_ptr + pad;
+	sp->free_ptr += aligned_size;
+	sp->remaining -= aligned_size;
+	spin_unlock_irqrestore(&sp->lock, flags);
+
+	memset(elem, 0, size);
+	return elem;
+}
+EXPORT_SYMBOL_GPL(folio_scratchpad_alloc);
+
+noinline void folio_scratchpad_reset(struct folio_scratchpad *sp)
+{
+	struct folio_pool_chunk *head, *chunk, *tmp;
+	size_t header_offset;
+	unsigned long flags;
+
+	spin_lock_irqsave(&sp->lock, flags);
+	if (list_empty(&sp->chunks)) {
+		sp->free_ptr = NULL;
+		sp->remaining = 0;
+		spin_unlock_irqrestore(&sp->lock, flags);
+		return;
+	}
+
+	/* Retain primary head chunk; free overflow chunks */
+	head = list_first_entry(&sp->chunks, struct folio_pool_chunk, link);
+	list_for_each_entry_safe(chunk, tmp, &sp->chunks, link) {
+		if (chunk == head)
+			continue;
+		list_del(&chunk->link);
+		folio_put(chunk->folio);
+	}
+
+	header_offset = ALIGN(sizeof(*head), sizeof(void *));
+	sp->free_ptr = folio_address(head->folio) + header_offset;
+	sp->remaining = folio_size(head->folio) - header_offset;
+	spin_unlock_irqrestore(&sp->lock, flags);
+}
+EXPORT_SYMBOL_GPL(folio_scratchpad_reset);
+
+noinline void folio_scratchpad_free(struct folio_scratchpad *sp)
+{
+	struct folio_pool_chunk *chunk, *tmp;
+	struct folio *folio;
+	unsigned long flags;
+
+	spin_lock_irqsave(&sp->lock, flags);
+	sp->free_ptr = NULL;
+	sp->remaining = 0;
+	list_for_each_entry_safe(chunk, tmp, &sp->chunks, link) {
+		folio = chunk->folio;
+		list_del(&chunk->link);
+		folio_put(folio);
+	}
+	spin_unlock_irqrestore(&sp->lock, flags);
+}
+EXPORT_SYMBOL_GPL(folio_scratchpad_free);
+
+/*
+ * 2. Fixed-Slot Uniform Pool (Specialized Thin Wrapper on Scratchpad)
+ */
+void folio_pool_init_align(struct folio_pool *fp, size_t elem_size,
+			   size_t elem_align, unsigned int order)
+{
+	folio_scratchpad_init(&fp->base, order);
+	fp->elem_size = elem_size;
+	fp->elem_align = max_t(size_t, sizeof(void *), elem_align ? elem_align : sizeof(void *));
+}
+EXPORT_SYMBOL_GPL(folio_pool_init_align);
+
+void folio_pool_init_key(struct folio_pool *fp, size_t elem_size, unsigned int order,
+			 struct static_key *key)
+{
+	size_t align = sizeof(void *);
+
+	if (elem_size && is_power_of_2(elem_size))
+		align = max_t(size_t, sizeof(void *), elem_size);
+	folio_scratchpad_init_key(&fp->base, order, key);
+	fp->elem_size = elem_size;
+	fp->elem_align = align;
+}
+EXPORT_SYMBOL_GPL(folio_pool_init_key);
+
+void folio_pool_init(struct folio_pool *fp, size_t elem_size, unsigned int order)
+{
+	folio_pool_init_key(fp, elem_size, order, NULL);
+}
+EXPORT_SYMBOL_GPL(folio_pool_init);
+
+void folio_scratchpad_stats(struct folio_scratchpad *sp, unsigned int *nr_chunks,
+			    size_t *chunk_size, size_t *tail_used)
+{
+	unsigned long flags;
+	size_t csz;
+
+	csz = (PAGE_SIZE << sp->chunk_order);
+	if (chunk_size)
+		*chunk_size = csz;
+
+	spin_lock_irqsave(&sp->lock, flags);
+	if (nr_chunks)
+		*nr_chunks = list_count_nodes(&sp->chunks);
+	if (tail_used)
+		*tail_used = csz > sp->remaining ? csz - sp->remaining : 0;
+	spin_unlock_irqrestore(&sp->lock, flags);
+}
+EXPORT_SYMBOL_GPL(folio_scratchpad_stats);

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 2/9] netfilter/nf_tables: Add folio_scratchpad collector to struct nftables_pernet
  2026-08-17 17:22 ` Jim Cromie
@ 2026-08-17 17:22   ` Jim Cromie
  -1 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie via B4 Relay @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

From: Jim Cromie <jim.cromie@gmail.com>

Embed a struct folio_scratchpad inside struct nftables_pernet to support
variable-sized region allocations and O(1) bulk teardown for Netfilter
batch transactions.

Route nft_trans_alloc allocations directly to folio_scratchpad_alloc_bytes(),
releasing all transaction objects in O(1) bulk via folio_scratchpad_free()
during commit/abort cleanup workers.

Controlled in-kernel ftrace function profiling on 10,015 netfilter batch
transaction allocations demonstrates ~12% faster allocation and ~7.6%
faster destroy worker teardown compared to standard SLUB.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 include/net/netfilter/nf_tables.h |  3 +++
 net/netfilter/nf_tables_api.c     | 51 ++++++++++++++++++++++++++++++---------
 2 files changed, 43 insertions(+), 11 deletions(-)

diff --git a/include/net/netfilter/nf_tables.h b/include/net/netfilter/nf_tables.h
index 3be612145c13..b4a6cc1f46af 100644
--- a/include/net/netfilter/nf_tables.h
+++ b/include/net/netfilter/nf_tables.h
@@ -1941,6 +1941,8 @@ __printf(2, 3) int nft_request_module(struct net *net, const char *fmt, ...);
 static inline int nft_request_module(struct net *net, const char *fmt, ...) { return -ENOENT; }
 #endif
 
+#include <linux/folio_pool.h>
+
 struct nftables_pernet {
 	struct list_head	tables;
 	struct list_head	commit_list;
@@ -1949,6 +1951,7 @@ struct nftables_pernet {
 	struct list_head	binding_list;
 	struct list_head	module_list;
 	struct list_head	notify_list;
+	struct folio_scratchpad	trans_scratchpad;
 	struct mutex		commit_mutex;
 	u64			table_handle;
 	u64			tstamp;
diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
index af357f6c5070..8b9e0c905d84 100644
--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -173,12 +173,17 @@ static void nft_ctx_init(struct nft_ctx *ctx,
 	bitmap_zero(ctx->reg_inited, NFT_REG32_NUM);
 }
 
+DEFINE_FOLIO_POOL_STATIC_KEY_PARAM(nft_trans_scratchpad_key, trans_scratchpad,
+				   "Toggle nf_tables transaction folio scratchpad allocator");
+
 static struct nft_trans *nft_trans_alloc(const struct nft_ctx *ctx,
 					 int msg_type, u32 size)
 {
+	struct nftables_pernet *nft_net = nft_pernet(ctx->net);
 	struct nft_trans *trans;
 
-	trans = kzalloc(size, GFP_KERNEL);
+	trans = folio_scratchpad_alloc_bytes(nft_net, trans_scratchpad, size,
+					     __alignof__(struct nft_trans), GFP_KERNEL);
 	if (trans == NULL)
 		return NULL;
 
@@ -194,6 +199,18 @@ static struct nft_trans *nft_trans_alloc(const struct nft_ctx *ctx,
 	return trans;
 }
 
+/*
+ * nft_trans_free - release a transaction object.
+ *
+ * Scratchpad-backed objects are not individually freed; they are reclaimed in
+ * bulk by folio_scratchpad_free() at batch commit/abort boundary.  Kzalloc'd
+ * objects reside on slab pages and are released immediately via kfree().
+ */
+static inline void nft_trans_free(struct nft_trans *trans)
+{
+	folio_scratchpad_free_elem(trans);
+}
+
 static struct nft_trans_binding *nft_trans_get_binding(struct nft_trans *trans)
 {
 	switch (trans->msg_type) {
@@ -219,7 +236,7 @@ static void nft_trans_list_del(struct nft_trans *trans)
 static void nft_trans_destroy(struct nft_trans *trans)
 {
 	nft_trans_list_del(trans);
-	kfree(trans);
+	nft_trans_free(trans);
 }
 
 static void __nft_set_trans_bind(const struct nft_ctx *ctx, struct nft_set *set,
@@ -519,8 +536,10 @@ static bool nft_trans_collapse_set_elem(struct nftables_pernet *nft_net,
 	/* krealloc might free tail which invalidates list pointers */
 	list_del_init(&tail->nft_trans.list);
 
-	new_trans = krealloc(tail, struct_size(tail, elems, nelems),
-			     GFP_KERNEL);
+	new_trans = folio_scratchpad_realloc(tail,
+					     struct_size(tail, elems, old_nelems),
+					     struct_size(tail, elems, nelems),
+					     GFP_KERNEL);
 	if (!new_trans) {
 		list_add_tail(&tail->nft_trans.list,
 			      &nft_net->commit_list);
@@ -600,7 +619,7 @@ static void nft_trans_commit_list_add_elem(struct net *net, struct nft_trans *tr
 		     trans->msg_type != NFT_MSG_DELSETELEM);
 
 	if (nft_trans_try_collapse(nft_net, trans)) {
-		kfree(trans);
+		nft_trans_free(trans);
 		return;
 	}
 
@@ -3068,7 +3087,7 @@ static int nf_tables_updchain(struct nft_ctx *ctx, u8 genmask, u8 policy,
 
 err_trans:
 	free_percpu(stats);
-	kfree(trans);
+	nft_trans_free(trans);
 err_hooks:
 	if (nla[NFTA_CHAIN_HOOK]) {
 		list_for_each_entry_safe(h, next, &hook.list, list) {
@@ -7636,7 +7655,7 @@ static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set,
 	return set_full ? -ENFILE : 0;
 
 err_element_clash:
-	kfree(trans);
+	nft_trans_free(trans);
 err_set_size:
 	if (!(flags & NFT_SET_ELEM_CATCHALL))
 		atomic_dec(&set->nelems);
@@ -7904,7 +7923,7 @@ static int nft_del_setelem(struct nft_ctx *ctx, struct nft_set *set,
 	return 0;
 
 fail_ops:
-	kfree(trans);
+	nft_trans_free(trans);
 fail_trans:
 	kfree(elem.priv);
 fail_elem_key_end:
@@ -8287,7 +8306,7 @@ static int nf_tables_updobj(const struct nft_ctx *ctx,
 	return 0;
 
 err_free_trans:
-	kfree(trans);
+	nft_trans_free(trans);
 err_trans:
 	module_put(type->owner);
 	return err;
@@ -10253,7 +10272,7 @@ static void nft_commit_release(struct nft_trans *trans)
 	if (trans->put_net)
 		put_net(trans->net);
 
-	kfree(trans);
+	nft_trans_free(trans);
 }
 
 static void nf_tables_trans_destroy_work(struct work_struct *w)
@@ -10275,6 +10294,8 @@ static void nf_tables_trans_destroy_work(struct work_struct *w)
 		nft_trans_list_del(trans);
 		nft_commit_release(trans);
 	}
+
+	folio_scratchpad_reset(&nft_net->trans_scratchpad);
 }
 
 void nf_tables_trans_destroy_flush_work(struct net *net)
@@ -11244,7 +11265,7 @@ static void nf_tables_abort_release(struct nft_trans *trans)
 			nf_tables_flowtable_destroy(nft_trans_flowtable(trans));
 		break;
 	}
-	kfree(trans);
+	nft_trans_free(trans);
 }
 
 static void nft_set_abort_update(struct list_head *set_update_list)
@@ -11468,6 +11489,8 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action)
 		nf_tables_abort_release(trans);
 	}
 
+	folio_scratchpad_reset(&nft_net->trans_scratchpad);
+
 	return err;
 }
 
@@ -11499,6 +11522,9 @@ static int nf_tables_abort(struct net *net, struct sk_buff *skb,
 	else
 		nf_tables_module_autoload_cleanup(net);
 
+	folio_scratchpad_free(&nft_net->trans_scratchpad);
+	folio_scratchpad_init(&nft_net->trans_scratchpad, 4);
+
 	mutex_unlock(&nft_net->commit_mutex);
 
 	return ret;
@@ -12142,6 +12168,8 @@ static int __net_init nf_tables_init_net(struct net *net)
 	INIT_LIST_HEAD(&nft_net->binding_list);
 	INIT_LIST_HEAD(&nft_net->module_list);
 	INIT_LIST_HEAD(&nft_net->notify_list);
+	folio_scratchpad_init_key(&nft_net->trans_scratchpad, get_order(SZ_64K),
+				  &nft_trans_scratchpad_key);
 	mutex_init(&nft_net->commit_mutex);
 	net->nft.base_seq = 1;
 	nft_net->gc_seq = 0;
@@ -12186,6 +12214,7 @@ static void __net_exit nf_tables_exit_net(struct net *net)
 	WARN_ON_ONCE(!list_empty(&nft_net->module_list));
 	WARN_ON_ONCE(!list_empty(&nft_net->notify_list));
 	WARN_ON_ONCE(!list_empty(&nft_net->destroy_list));
+	folio_scratchpad_free(&nft_net->trans_scratchpad);
 }
 
 static void nf_tables_exit_batch(struct list_head *net_exit_list)

-- 
2.55.0




^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 2/9] netfilter/nf_tables: Add folio_scratchpad collector to struct nftables_pernet
@ 2026-08-17 17:22   ` Jim Cromie
  0 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

Embed a struct folio_scratchpad inside struct nftables_pernet to support
variable-sized region allocations and O(1) bulk teardown for Netfilter
batch transactions.

Route nft_trans_alloc allocations directly to folio_scratchpad_alloc_bytes(),
releasing all transaction objects in O(1) bulk via folio_scratchpad_free()
during commit/abort cleanup workers.

Controlled in-kernel ftrace function profiling on 10,015 netfilter batch
transaction allocations demonstrates ~12% faster allocation and ~7.6%
faster destroy worker teardown compared to standard SLUB.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 include/net/netfilter/nf_tables.h |  3 +++
 net/netfilter/nf_tables_api.c     | 51 ++++++++++++++++++++++++++++++---------
 2 files changed, 43 insertions(+), 11 deletions(-)

diff --git a/include/net/netfilter/nf_tables.h b/include/net/netfilter/nf_tables.h
index 3be612145c13..b4a6cc1f46af 100644
--- a/include/net/netfilter/nf_tables.h
+++ b/include/net/netfilter/nf_tables.h
@@ -1941,6 +1941,8 @@ __printf(2, 3) int nft_request_module(struct net *net, const char *fmt, ...);
 static inline int nft_request_module(struct net *net, const char *fmt, ...) { return -ENOENT; }
 #endif
 
+#include <linux/folio_pool.h>
+
 struct nftables_pernet {
 	struct list_head	tables;
 	struct list_head	commit_list;
@@ -1949,6 +1951,7 @@ struct nftables_pernet {
 	struct list_head	binding_list;
 	struct list_head	module_list;
 	struct list_head	notify_list;
+	struct folio_scratchpad	trans_scratchpad;
 	struct mutex		commit_mutex;
 	u64			table_handle;
 	u64			tstamp;
diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
index af357f6c5070..8b9e0c905d84 100644
--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -173,12 +173,17 @@ static void nft_ctx_init(struct nft_ctx *ctx,
 	bitmap_zero(ctx->reg_inited, NFT_REG32_NUM);
 }
 
+DEFINE_FOLIO_POOL_STATIC_KEY_PARAM(nft_trans_scratchpad_key, trans_scratchpad,
+				   "Toggle nf_tables transaction folio scratchpad allocator");
+
 static struct nft_trans *nft_trans_alloc(const struct nft_ctx *ctx,
 					 int msg_type, u32 size)
 {
+	struct nftables_pernet *nft_net = nft_pernet(ctx->net);
 	struct nft_trans *trans;
 
-	trans = kzalloc(size, GFP_KERNEL);
+	trans = folio_scratchpad_alloc_bytes(nft_net, trans_scratchpad, size,
+					     __alignof__(struct nft_trans), GFP_KERNEL);
 	if (trans == NULL)
 		return NULL;
 
@@ -194,6 +199,18 @@ static struct nft_trans *nft_trans_alloc(const struct nft_ctx *ctx,
 	return trans;
 }
 
+/*
+ * nft_trans_free - release a transaction object.
+ *
+ * Scratchpad-backed objects are not individually freed; they are reclaimed in
+ * bulk by folio_scratchpad_free() at batch commit/abort boundary.  Kzalloc'd
+ * objects reside on slab pages and are released immediately via kfree().
+ */
+static inline void nft_trans_free(struct nft_trans *trans)
+{
+	folio_scratchpad_free_elem(trans);
+}
+
 static struct nft_trans_binding *nft_trans_get_binding(struct nft_trans *trans)
 {
 	switch (trans->msg_type) {
@@ -219,7 +236,7 @@ static void nft_trans_list_del(struct nft_trans *trans)
 static void nft_trans_destroy(struct nft_trans *trans)
 {
 	nft_trans_list_del(trans);
-	kfree(trans);
+	nft_trans_free(trans);
 }
 
 static void __nft_set_trans_bind(const struct nft_ctx *ctx, struct nft_set *set,
@@ -519,8 +536,10 @@ static bool nft_trans_collapse_set_elem(struct nftables_pernet *nft_net,
 	/* krealloc might free tail which invalidates list pointers */
 	list_del_init(&tail->nft_trans.list);
 
-	new_trans = krealloc(tail, struct_size(tail, elems, nelems),
-			     GFP_KERNEL);
+	new_trans = folio_scratchpad_realloc(tail,
+					     struct_size(tail, elems, old_nelems),
+					     struct_size(tail, elems, nelems),
+					     GFP_KERNEL);
 	if (!new_trans) {
 		list_add_tail(&tail->nft_trans.list,
 			      &nft_net->commit_list);
@@ -600,7 +619,7 @@ static void nft_trans_commit_list_add_elem(struct net *net, struct nft_trans *tr
 		     trans->msg_type != NFT_MSG_DELSETELEM);
 
 	if (nft_trans_try_collapse(nft_net, trans)) {
-		kfree(trans);
+		nft_trans_free(trans);
 		return;
 	}
 
@@ -3068,7 +3087,7 @@ static int nf_tables_updchain(struct nft_ctx *ctx, u8 genmask, u8 policy,
 
 err_trans:
 	free_percpu(stats);
-	kfree(trans);
+	nft_trans_free(trans);
 err_hooks:
 	if (nla[NFTA_CHAIN_HOOK]) {
 		list_for_each_entry_safe(h, next, &hook.list, list) {
@@ -7636,7 +7655,7 @@ static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set,
 	return set_full ? -ENFILE : 0;
 
 err_element_clash:
-	kfree(trans);
+	nft_trans_free(trans);
 err_set_size:
 	if (!(flags & NFT_SET_ELEM_CATCHALL))
 		atomic_dec(&set->nelems);
@@ -7904,7 +7923,7 @@ static int nft_del_setelem(struct nft_ctx *ctx, struct nft_set *set,
 	return 0;
 
 fail_ops:
-	kfree(trans);
+	nft_trans_free(trans);
 fail_trans:
 	kfree(elem.priv);
 fail_elem_key_end:
@@ -8287,7 +8306,7 @@ static int nf_tables_updobj(const struct nft_ctx *ctx,
 	return 0;
 
 err_free_trans:
-	kfree(trans);
+	nft_trans_free(trans);
 err_trans:
 	module_put(type->owner);
 	return err;
@@ -10253,7 +10272,7 @@ static void nft_commit_release(struct nft_trans *trans)
 	if (trans->put_net)
 		put_net(trans->net);
 
-	kfree(trans);
+	nft_trans_free(trans);
 }
 
 static void nf_tables_trans_destroy_work(struct work_struct *w)
@@ -10275,6 +10294,8 @@ static void nf_tables_trans_destroy_work(struct work_struct *w)
 		nft_trans_list_del(trans);
 		nft_commit_release(trans);
 	}
+
+	folio_scratchpad_reset(&nft_net->trans_scratchpad);
 }
 
 void nf_tables_trans_destroy_flush_work(struct net *net)
@@ -11244,7 +11265,7 @@ static void nf_tables_abort_release(struct nft_trans *trans)
 			nf_tables_flowtable_destroy(nft_trans_flowtable(trans));
 		break;
 	}
-	kfree(trans);
+	nft_trans_free(trans);
 }
 
 static void nft_set_abort_update(struct list_head *set_update_list)
@@ -11468,6 +11489,8 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action)
 		nf_tables_abort_release(trans);
 	}
 
+	folio_scratchpad_reset(&nft_net->trans_scratchpad);
+
 	return err;
 }
 
@@ -11499,6 +11522,9 @@ static int nf_tables_abort(struct net *net, struct sk_buff *skb,
 	else
 		nf_tables_module_autoload_cleanup(net);
 
+	folio_scratchpad_free(&nft_net->trans_scratchpad);
+	folio_scratchpad_init(&nft_net->trans_scratchpad, 4);
+
 	mutex_unlock(&nft_net->commit_mutex);
 
 	return ret;
@@ -12142,6 +12168,8 @@ static int __net_init nf_tables_init_net(struct net *net)
 	INIT_LIST_HEAD(&nft_net->binding_list);
 	INIT_LIST_HEAD(&nft_net->module_list);
 	INIT_LIST_HEAD(&nft_net->notify_list);
+	folio_scratchpad_init_key(&nft_net->trans_scratchpad, get_order(SZ_64K),
+				  &nft_trans_scratchpad_key);
 	mutex_init(&nft_net->commit_mutex);
 	net->nft.base_seq = 1;
 	nft_net->gc_seq = 0;
@@ -12186,6 +12214,7 @@ static void __net_exit nf_tables_exit_net(struct net *net)
 	WARN_ON_ONCE(!list_empty(&nft_net->module_list));
 	WARN_ON_ONCE(!list_empty(&nft_net->notify_list));
 	WARN_ON_ONCE(!list_empty(&nft_net->destroy_list));
+	folio_scratchpad_free(&nft_net->trans_scratchpad);
 }
 
 static void nf_tables_exit_batch(struct list_head *net_exit_list)

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 3/9] bpf/verifier: Route verifier stack state node allocations to folio_pool
  2026-08-17 17:22 ` Jim Cromie
@ 2026-08-17 17:22   ` Jim Cromie
  -1 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie via B4 Relay @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

From: Jim Cromie <jim.cromie@gmail.com>

Embed a struct folio_pool inside struct bpf_verifier_env to allocate
transient bpf_verifier_stack_elem frames using direct-map large folios,
releasing all frames in bulk at the end of bpf_check().

Use folio_pool_alloc_obj() to preserve strict type specificity at callsites.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 include/linux/bpf_verifier.h |  3 +++
 kernel/bpf/verifier.c        | 16 +++++++++++++---
 2 files changed, 16 insertions(+), 3 deletions(-)

diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 39a851e690ec..43f5f0eaffac 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -898,6 +898,8 @@ struct bpf_scc_info {
 
 struct bpf_liveness;
 
+#include <linux/folio_pool.h>
+
 /* single container for all structs
  * one verifier_env per bpf_check() call
  */
@@ -907,6 +909,7 @@ struct bpf_verifier_env {
 	struct bpf_prog *prog;		/* eBPF program being verified */
 	const struct bpf_verifier_ops *ops;
 	struct module *attach_btf_mod;	/* The owner module of prog->aux->attach_btf */
+	struct folio_pool state_pool;	/* pool for transient state nodes */
 	struct bpf_verifier_stack_elem *head; /* stack of verifier states to be processed */
 	int stack_size;			/* number of states to be processed */
 	bool strict_alignment;		/* perform strict pointer alignment checks */
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index fdc5fbb1f78c..8a9e66ce4dc8 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -1725,7 +1725,7 @@ static int pop_stack(struct bpf_verifier_env *env, int *prev_insn_idx,
 		*prev_insn_idx = head->prev_insn_idx;
 	elem = head->next;
 	bpf_free_verifier_state(&head->st, false);
-	kfree(head);
+	folio_pool_free_elem(head);
 	env->head = elem;
 	env->stack_size--;
 	return 0;
@@ -1743,6 +1743,8 @@ static bool error_recoverable_with_nospec(int err)
 	return err == -EPERM || err == -EACCES || err == -EINVAL;
 }
 
+DEFINE_STATIC_KEY_TRUE(bpf_state_pool_key);
+
 static struct bpf_verifier_state *push_stack(struct bpf_verifier_env *env,
 					     int insn_idx, int prev_insn_idx,
 					     bool speculative)
@@ -1751,7 +1753,9 @@ static struct bpf_verifier_state *push_stack(struct bpf_verifier_env *env,
 	struct bpf_verifier_stack_elem *elem;
 	int err;
 
-	elem = kzalloc_obj(struct bpf_verifier_stack_elem, GFP_KERNEL_ACCOUNT);
+	elem = folio_pool_alloc_obj(env, state_pool,
+				    struct bpf_verifier_stack_elem,
+				    GFP_KERNEL_ACCOUNT);
 	if (!elem)
 		return ERR_PTR(-ENOMEM);
 
@@ -2275,7 +2279,9 @@ static struct bpf_verifier_state *push_async_cb(struct bpf_verifier_env *env,
 	struct bpf_verifier_stack_elem *elem;
 	struct bpf_func_state *frame;
 
-	elem = kzalloc_obj(struct bpf_verifier_stack_elem, GFP_KERNEL_ACCOUNT);
+	elem = folio_pool_alloc_obj(env, state_pool,
+				    struct bpf_verifier_stack_elem,
+				    GFP_KERNEL_ACCOUNT);
 	if (!elem)
 		return ERR_PTR(-ENOMEM);
 
@@ -19789,6 +19795,9 @@ int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr,
 	if (!env)
 		return -ENOMEM;
 
+	folio_pool_init_key(&env->state_pool, sizeof(struct bpf_verifier_stack_elem),
+			    get_order(SZ_64K), &bpf_state_pool_key);
+
 	env->bt.env = env;
 
 	len = (*prog)->len;
@@ -20055,6 +20064,7 @@ int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr,
 	bpf_clear_insn_aux_data(env, 0, env->prog->len);
 err_free_env:
 	bpf_stack_liveness_free(env);
+	folio_pool_free(&env->state_pool);
 	kvfree(env->cfg.insn_postorder);
 	kvfree(env->scc_info);
 	kvfree(env->succ);

-- 
2.55.0




^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 3/9] bpf/verifier: Route verifier stack state node allocations to folio_pool
@ 2026-08-17 17:22   ` Jim Cromie
  0 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

Embed a struct folio_pool inside struct bpf_verifier_env to allocate
transient bpf_verifier_stack_elem frames using direct-map large folios,
releasing all frames in bulk at the end of bpf_check().

Use folio_pool_alloc_obj() to preserve strict type specificity at callsites.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 include/linux/bpf_verifier.h |  3 +++
 kernel/bpf/verifier.c        | 16 +++++++++++++---
 2 files changed, 16 insertions(+), 3 deletions(-)

diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 39a851e690ec..43f5f0eaffac 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -898,6 +898,8 @@ struct bpf_scc_info {
 
 struct bpf_liveness;
 
+#include <linux/folio_pool.h>
+
 /* single container for all structs
  * one verifier_env per bpf_check() call
  */
@@ -907,6 +909,7 @@ struct bpf_verifier_env {
 	struct bpf_prog *prog;		/* eBPF program being verified */
 	const struct bpf_verifier_ops *ops;
 	struct module *attach_btf_mod;	/* The owner module of prog->aux->attach_btf */
+	struct folio_pool state_pool;	/* pool for transient state nodes */
 	struct bpf_verifier_stack_elem *head; /* stack of verifier states to be processed */
 	int stack_size;			/* number of states to be processed */
 	bool strict_alignment;		/* perform strict pointer alignment checks */
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index fdc5fbb1f78c..8a9e66ce4dc8 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -1725,7 +1725,7 @@ static int pop_stack(struct bpf_verifier_env *env, int *prev_insn_idx,
 		*prev_insn_idx = head->prev_insn_idx;
 	elem = head->next;
 	bpf_free_verifier_state(&head->st, false);
-	kfree(head);
+	folio_pool_free_elem(head);
 	env->head = elem;
 	env->stack_size--;
 	return 0;
@@ -1743,6 +1743,8 @@ static bool error_recoverable_with_nospec(int err)
 	return err == -EPERM || err == -EACCES || err == -EINVAL;
 }
 
+DEFINE_STATIC_KEY_TRUE(bpf_state_pool_key);
+
 static struct bpf_verifier_state *push_stack(struct bpf_verifier_env *env,
 					     int insn_idx, int prev_insn_idx,
 					     bool speculative)
@@ -1751,7 +1753,9 @@ static struct bpf_verifier_state *push_stack(struct bpf_verifier_env *env,
 	struct bpf_verifier_stack_elem *elem;
 	int err;
 
-	elem = kzalloc_obj(struct bpf_verifier_stack_elem, GFP_KERNEL_ACCOUNT);
+	elem = folio_pool_alloc_obj(env, state_pool,
+				    struct bpf_verifier_stack_elem,
+				    GFP_KERNEL_ACCOUNT);
 	if (!elem)
 		return ERR_PTR(-ENOMEM);
 
@@ -2275,7 +2279,9 @@ static struct bpf_verifier_state *push_async_cb(struct bpf_verifier_env *env,
 	struct bpf_verifier_stack_elem *elem;
 	struct bpf_func_state *frame;
 
-	elem = kzalloc_obj(struct bpf_verifier_stack_elem, GFP_KERNEL_ACCOUNT);
+	elem = folio_pool_alloc_obj(env, state_pool,
+				    struct bpf_verifier_stack_elem,
+				    GFP_KERNEL_ACCOUNT);
 	if (!elem)
 		return ERR_PTR(-ENOMEM);
 
@@ -19789,6 +19795,9 @@ int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr,
 	if (!env)
 		return -ENOMEM;
 
+	folio_pool_init_key(&env->state_pool, sizeof(struct bpf_verifier_stack_elem),
+			    get_order(SZ_64K), &bpf_state_pool_key);
+
 	env->bt.env = env;
 
 	len = (*prog)->len;
@@ -20055,6 +20064,7 @@ int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr,
 	bpf_clear_insn_aux_data(env, 0, env->prog->len);
 err_free_env:
 	bpf_stack_liveness_free(env);
+	folio_pool_free(&env->state_pool);
 	kvfree(env->cfg.insn_postorder);
 	kvfree(env->scc_info);
 	kvfree(env->succ);

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 4/9] drm/gpuvm: Route gpuva_op allocations to folio_scratchpad
  2026-08-17 17:22 ` Jim Cromie
@ 2026-08-17 17:22   ` Jim Cromie
  -1 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie via B4 Relay @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

From: Jim Cromie <jim.cromie@gmail.com>

Embed a struct folio_scratchpad inside struct drm_gpuvm and route
default gpuva_op allocations in gpuva_op_alloc() to
folio_scratchpad_alloc_obj(), with O(1) bulk teardown in
drm_gpuvm_fini().

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 drivers/gpu/drm/drm_gpuvm.c | 11 +++++++++--
 include/drm/drm_gpuvm.h     |  6 ++++++
 2 files changed, 15 insertions(+), 2 deletions(-)

diff --git a/drivers/gpu/drm/drm_gpuvm.c b/drivers/gpu/drm/drm_gpuvm.c
index c422c5af1f4b..6bb82ac9e2d6 100644
--- a/drivers/gpu/drm/drm_gpuvm.c
+++ b/drivers/gpu/drm/drm_gpuvm.c
@@ -1109,6 +1109,8 @@ drm_gpuvm_init(struct drm_gpuvm *gpuvm, const char *name,
 	spin_lock_init(&gpuvm->evict.lock);
 
 	init_llist_head(&gpuvm->bo_defer);
+	folio_scratchpad_init_key(&gpuvm->va_scratchpad, get_order(SZ_64K),
+				  &gpuvm_scratchpad_key);
 
 	kref_init(&gpuvm->kref);
 
@@ -1155,6 +1157,7 @@ drm_gpuvm_fini(struct drm_gpuvm *gpuvm)
 	drm_WARN(gpuvm->drm, !llist_empty(&gpuvm->bo_defer),
 		 "VM BO cleanup list should be empty.\n");
 
+	folio_scratchpad_free(&gpuvm->va_scratchpad);
 	drm_gem_object_put(gpuvm->r_obj);
 }
 
@@ -2849,6 +2852,9 @@ drm_gpuvm_sm_unmap_exec_lock(struct drm_gpuvm *gpuvm, struct drm_exec *exec,
 }
 EXPORT_SYMBOL_GPL(drm_gpuvm_sm_unmap_exec_lock);
 
+DEFINE_FOLIO_POOL_STATIC_KEY_PARAM(gpuvm_scratchpad_key, va_scratchpad,
+				   "Toggle DRM GPUVM range folio scratchpad allocator");
+
 static struct drm_gpuva_op *
 gpuva_op_alloc(struct drm_gpuvm *gpuvm)
 {
@@ -2858,7 +2864,8 @@ gpuva_op_alloc(struct drm_gpuvm *gpuvm)
 	if (fn && fn->op_alloc)
 		op = fn->op_alloc();
 	else
-		op = kzalloc_obj(*op);
+		op = folio_scratchpad_alloc_obj(gpuvm, va_scratchpad,
+						struct drm_gpuva_op, GFP_KERNEL);
 
 	if (unlikely(!op))
 		return NULL;
@@ -2875,7 +2882,7 @@ gpuva_op_free(struct drm_gpuvm *gpuvm,
 	if (fn && fn->op_free)
 		fn->op_free(op);
 	else
-		kfree(op);
+		folio_scratchpad_free_elem(op);
 }
 
 static int
diff --git a/include/drm/drm_gpuvm.h b/include/drm/drm_gpuvm.h
index 655bd9104ffb..362b1d88d30d 100644
--- a/include/drm/drm_gpuvm.h
+++ b/include/drm/drm_gpuvm.h
@@ -30,6 +30,7 @@
 #include <linux/llist.h>
 #include <linux/rbtree.h>
 #include <linux/types.h>
+#include <linux/folio_pool.h>
 
 #include <drm/drm_device.h>
 #include <drm/drm_gem.h>
@@ -338,6 +339,11 @@ struct drm_gpuvm {
 	 * @bo_defer: structure holding vm_bos that need to be destroyed
 	 */
 	struct llist_head bo_defer;
+
+	/**
+	 * @va_scratchpad: folio scratchpad for transient GPU VA range allocations
+	 */
+	struct folio_scratchpad va_scratchpad;
 };
 
 void drm_gpuvm_init(struct drm_gpuvm *gpuvm, const char *name,

-- 
2.55.0




^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 4/9] drm/gpuvm: Route gpuva_op allocations to folio_scratchpad
@ 2026-08-17 17:22   ` Jim Cromie
  0 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

Embed a struct folio_scratchpad inside struct drm_gpuvm and route
default gpuva_op allocations in gpuva_op_alloc() to
folio_scratchpad_alloc_obj(), with O(1) bulk teardown in
drm_gpuvm_fini().

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 drivers/gpu/drm/drm_gpuvm.c | 11 +++++++++--
 include/drm/drm_gpuvm.h     |  6 ++++++
 2 files changed, 15 insertions(+), 2 deletions(-)

diff --git a/drivers/gpu/drm/drm_gpuvm.c b/drivers/gpu/drm/drm_gpuvm.c
index c422c5af1f4b..6bb82ac9e2d6 100644
--- a/drivers/gpu/drm/drm_gpuvm.c
+++ b/drivers/gpu/drm/drm_gpuvm.c
@@ -1109,6 +1109,8 @@ drm_gpuvm_init(struct drm_gpuvm *gpuvm, const char *name,
 	spin_lock_init(&gpuvm->evict.lock);
 
 	init_llist_head(&gpuvm->bo_defer);
+	folio_scratchpad_init_key(&gpuvm->va_scratchpad, get_order(SZ_64K),
+				  &gpuvm_scratchpad_key);
 
 	kref_init(&gpuvm->kref);
 
@@ -1155,6 +1157,7 @@ drm_gpuvm_fini(struct drm_gpuvm *gpuvm)
 	drm_WARN(gpuvm->drm, !llist_empty(&gpuvm->bo_defer),
 		 "VM BO cleanup list should be empty.\n");
 
+	folio_scratchpad_free(&gpuvm->va_scratchpad);
 	drm_gem_object_put(gpuvm->r_obj);
 }
 
@@ -2849,6 +2852,9 @@ drm_gpuvm_sm_unmap_exec_lock(struct drm_gpuvm *gpuvm, struct drm_exec *exec,
 }
 EXPORT_SYMBOL_GPL(drm_gpuvm_sm_unmap_exec_lock);
 
+DEFINE_FOLIO_POOL_STATIC_KEY_PARAM(gpuvm_scratchpad_key, va_scratchpad,
+				   "Toggle DRM GPUVM range folio scratchpad allocator");
+
 static struct drm_gpuva_op *
 gpuva_op_alloc(struct drm_gpuvm *gpuvm)
 {
@@ -2858,7 +2864,8 @@ gpuva_op_alloc(struct drm_gpuvm *gpuvm)
 	if (fn && fn->op_alloc)
 		op = fn->op_alloc();
 	else
-		op = kzalloc_obj(*op);
+		op = folio_scratchpad_alloc_obj(gpuvm, va_scratchpad,
+						struct drm_gpuva_op, GFP_KERNEL);
 
 	if (unlikely(!op))
 		return NULL;
@@ -2875,7 +2882,7 @@ gpuva_op_free(struct drm_gpuvm *gpuvm,
 	if (fn && fn->op_free)
 		fn->op_free(op);
 	else
-		kfree(op);
+		folio_scratchpad_free_elem(op);
 }
 
 static int
diff --git a/include/drm/drm_gpuvm.h b/include/drm/drm_gpuvm.h
index 655bd9104ffb..362b1d88d30d 100644
--- a/include/drm/drm_gpuvm.h
+++ b/include/drm/drm_gpuvm.h
@@ -30,6 +30,7 @@
 #include <linux/llist.h>
 #include <linux/rbtree.h>
 #include <linux/types.h>
+#include <linux/folio_pool.h>
 
 #include <drm/drm_device.h>
 #include <drm/drm_gem.h>
@@ -338,6 +339,11 @@ struct drm_gpuvm {
 	 * @bo_defer: structure holding vm_bos that need to be destroyed
 	 */
 	struct llist_head bo_defer;
+
+	/**
+	 * @va_scratchpad: folio scratchpad for transient GPU VA range allocations
+	 */
+	struct folio_scratchpad va_scratchpad;
 };
 
 void drm_gpuvm_init(struct drm_gpuvm *gpuvm, const char *name,

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 5/9] bpf/syscall: Route generic_map_update_batch key/value allocations to folio_scratchpad
  2026-08-17 17:22 ` Jim Cromie
@ 2026-08-17 17:22   ` Jim Cromie
  -1 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie via B4 Relay @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

From: Jim Cromie <jim.cromie@gmail.com>

Use struct folio_scratchpad to allocate transient key and value buffers
during map update batch operations, bypassing SLUB cache churn.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 kernel/bpf/syscall.c | 17 ++++++++++++-----
 1 file changed, 12 insertions(+), 5 deletions(-)

diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
index 6db306d23b47..8ef48673b933 100644
--- a/kernel/bpf/syscall.c
+++ b/kernel/bpf/syscall.c
@@ -19,6 +19,7 @@
 #include <linux/fdtable.h>
 #include <linux/file.h>
 #include <linux/fs.h>
+#include <linux/folio_pool.h>
 #include <linux/license.h>
 #include <linux/filter.h>
 #include <linux/kernel.h>
@@ -2059,12 +2060,15 @@ int generic_map_delete_batch(struct bpf_map *map,
 	return err;
 }
 
+DEFINE_STATIC_KEY_TRUE(bpf_batch_scratchpad_key);
+
 int generic_map_update_batch(struct bpf_map *map, struct file *map_file,
 			     const union bpf_attr *attr,
 			     union bpf_attr __user *uattr)
 {
 	void __user *values = u64_to_user_ptr(attr->batch.values);
 	void __user *keys = u64_to_user_ptr(attr->batch.keys);
+	struct folio_scratchpad batch_sp;
 	u32 value_size, cp, max_count;
 	void *key, *value;
 	int err = 0;
@@ -2083,13 +2087,17 @@ int generic_map_update_batch(struct bpf_map *map, struct file *map_file,
 	if (put_user(0, &uattr->batch.count))
 		return -EFAULT;
 
-	key = kvmalloc(map->key_size, GFP_USER | __GFP_NOWARN);
+	folio_scratchpad_init_key(&batch_sp, 0, &bpf_batch_scratchpad_key);
+
+	key = folio_scratchpad_alloc(&batch_sp, map->key_size,
+				     sizeof(void *), GFP_USER | __GFP_NOWARN);
 	if (!key)
 		return -ENOMEM;
 
-	value = kvmalloc(value_size, GFP_USER | __GFP_NOWARN);
+	value = folio_scratchpad_alloc(&batch_sp, value_size,
+				       sizeof(void *), GFP_USER | __GFP_NOWARN);
 	if (!value) {
-		kvfree(key);
+		folio_scratchpad_free(&batch_sp);
 		return -ENOMEM;
 	}
 
@@ -2111,8 +2119,7 @@ int generic_map_update_batch(struct bpf_map *map, struct file *map_file,
 	if (copy_to_user(&uattr->batch.count, &cp, sizeof(cp)))
 		err = -EFAULT;
 
-	kvfree(value);
-	kvfree(key);
+	folio_scratchpad_free(&batch_sp);
 
 	return err;
 }

-- 
2.55.0




^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 5/9] bpf/syscall: Route generic_map_update_batch key/value allocations to folio_scratchpad
@ 2026-08-17 17:22   ` Jim Cromie
  0 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

Use struct folio_scratchpad to allocate transient key and value buffers
during map update batch operations, bypassing SLUB cache churn.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 kernel/bpf/syscall.c | 17 ++++++++++++-----
 1 file changed, 12 insertions(+), 5 deletions(-)

diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
index 6db306d23b47..8ef48673b933 100644
--- a/kernel/bpf/syscall.c
+++ b/kernel/bpf/syscall.c
@@ -19,6 +19,7 @@
 #include <linux/fdtable.h>
 #include <linux/file.h>
 #include <linux/fs.h>
+#include <linux/folio_pool.h>
 #include <linux/license.h>
 #include <linux/filter.h>
 #include <linux/kernel.h>
@@ -2059,12 +2060,15 @@ int generic_map_delete_batch(struct bpf_map *map,
 	return err;
 }
 
+DEFINE_STATIC_KEY_TRUE(bpf_batch_scratchpad_key);
+
 int generic_map_update_batch(struct bpf_map *map, struct file *map_file,
 			     const union bpf_attr *attr,
 			     union bpf_attr __user *uattr)
 {
 	void __user *values = u64_to_user_ptr(attr->batch.values);
 	void __user *keys = u64_to_user_ptr(attr->batch.keys);
+	struct folio_scratchpad batch_sp;
 	u32 value_size, cp, max_count;
 	void *key, *value;
 	int err = 0;
@@ -2083,13 +2087,17 @@ int generic_map_update_batch(struct bpf_map *map, struct file *map_file,
 	if (put_user(0, &uattr->batch.count))
 		return -EFAULT;
 
-	key = kvmalloc(map->key_size, GFP_USER | __GFP_NOWARN);
+	folio_scratchpad_init_key(&batch_sp, 0, &bpf_batch_scratchpad_key);
+
+	key = folio_scratchpad_alloc(&batch_sp, map->key_size,
+				     sizeof(void *), GFP_USER | __GFP_NOWARN);
 	if (!key)
 		return -ENOMEM;
 
-	value = kvmalloc(value_size, GFP_USER | __GFP_NOWARN);
+	value = folio_scratchpad_alloc(&batch_sp, value_size,
+				       sizeof(void *), GFP_USER | __GFP_NOWARN);
 	if (!value) {
-		kvfree(key);
+		folio_scratchpad_free(&batch_sp);
 		return -ENOMEM;
 	}
 
@@ -2111,8 +2119,7 @@ int generic_map_update_batch(struct bpf_map *map, struct file *map_file,
 	if (copy_to_user(&uattr->batch.count, &cp, sizeof(cp)))
 		err = -EFAULT;
 
-	kvfree(value);
-	kvfree(key);
+	folio_scratchpad_free(&batch_sp);
 
 	return err;
 }

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 6/9] locking/lockdep: Fallback to folio_pool in alloc_list_entry when static pool is full
  2026-08-17 17:22 ` Jim Cromie
@ 2026-08-17 17:22   ` Jim Cromie
  -1 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie via B4 Relay @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

From: Jim Cromie <jim.cromie@gmail.com>

Use struct folio_pool to dynamically expand lock dependency storage when
the static list_entries pool is exhausted, avoiding premature lockdep
disabling.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 kernel/locking/lockdep.c           | 35 ++++++++++++++++++++++++++++++++++-
 kernel/locking/lockdep_internals.h |  2 ++
 kernel/locking/lockdep_proc.c      | 16 ++++++++++++++--
 3 files changed, 50 insertions(+), 3 deletions(-)

diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
index 2d4c5bab5af8..f0f58db090ff 100644
--- a/kernel/locking/lockdep.c
+++ b/kernel/locking/lockdep.c
@@ -58,12 +58,37 @@
 #include <linux/context_tracking.h>
 #include <linux/console.h>
 #include <linux/kasan.h>
+#include <linux/folio_pool.h>
+
+DEFINE_STATIC_KEY_TRUE(lockdep_pool_key);
+static struct folio_pool lockdep_pool =
+	FOLIO_POOL_INIT_KEY(lockdep_pool, sizeof(struct lock_list),
+			    FOLIO_POOL_64K_ORDER, &lockdep_pool_key);
+
+static int __init setup_lockdep_folio_pool(char *str)
+{
+	bool enable;
+
+	if (!kstrtobool(str, &enable)) {
+		if (enable)
+			static_branch_enable(&lockdep_pool_key);
+		else
+			static_branch_disable(&lockdep_pool_key);
+	}
+	return 1;
+}
+__setup("lockdep.folio_pool=", setup_lockdep_folio_pool);
 
 #include <asm/sections.h>
 
 #include "lockdep_internals.h"
 #include "lock_events.h"
 
+void lockdep_pool_stats(unsigned int *nr_chunks, size_t *chunk_size, size_t *tail_used)
+{
+	folio_pool_stats(&lockdep_pool, nr_chunks, chunk_size, tail_used);
+}
+
 #include <trace/events/lock.h>
 
 #ifdef CONFIG_PROVE_LOCKING
@@ -1404,11 +1429,19 @@ static struct lock_list *alloc_list_entry(void)
 				      ARRAY_SIZE(list_entries));
 
 	if (idx >= ARRAY_SIZE(list_entries)) {
+		struct lock_list *p;
+
+		p = folio_pool_alloc_type(&lockdep_pool, struct lock_list,
+					  GFP_ATOMIC);
+		if (p) {
+			nr_list_entries++;
+			return p;
+		}
 		if (!debug_locks_off_graph_unlock())
 			return NULL;
 
 		nbcon_cpu_emergency_enter();
-		print_lockdep_off("BUG: MAX_LOCKDEP_ENTRIES too low!");
+		print_lockdep_off("BUG: MAX_LOCKDEP_ENTRIES too low and folio_pool exhausted!");
 		dump_stack();
 		nbcon_cpu_emergency_exit();
 		return NULL;
diff --git a/kernel/locking/lockdep_internals.h b/kernel/locking/lockdep_internals.h
index 0e5e6ffe91a3..f802160c0dd5 100644
--- a/kernel/locking/lockdep_internals.h
+++ b/kernel/locking/lockdep_internals.h
@@ -159,6 +159,8 @@ extern unsigned long max_lock_class_idx;
 extern struct lock_class lock_classes[MAX_LOCKDEP_KEYS];
 extern unsigned long lock_classes_in_use[];
 
+void lockdep_pool_stats(unsigned int *nr_chunks, size_t *chunk_size, size_t *tail_used);
+
 #ifdef CONFIG_PROVE_LOCKING
 extern unsigned long lockdep_count_forward_deps(struct lock_class *);
 extern unsigned long lockdep_count_backward_deps(struct lock_class *);
diff --git a/kernel/locking/lockdep_proc.c b/kernel/locking/lockdep_proc.c
index 1916db9aa46b..3e6f24ae9b34 100644
--- a/kernel/locking/lockdep_proc.c
+++ b/kernel/locking/lockdep_proc.c
@@ -288,8 +288,20 @@ static int lockdep_stats_show(struct seq_file *m, void *v)
 			nr_lock_classes, MAX_LOCKDEP_KEYS);
 	seq_printf(m, " dynamic-keys:                  %11lu\n",
 			nr_dynamic_keys);
-	seq_printf(m, " direct dependencies:           %11lu [max: %lu]\n",
-			nr_list_entries, MAX_LOCKDEP_ENTRIES);
+	{
+		unsigned int fp_chunks = 0;
+		size_t fp_chunk_sz = 0, fp_tail_used = 0;
+
+		lockdep_pool_stats(&fp_chunks, &fp_chunk_sz, &fp_tail_used);
+		if (fp_chunks) {
+			seq_printf(m, " direct dependencies:           %11lu [dynamic: %u x %zu kB, tail: %zu kB/%zu kB]\n",
+				   nr_list_entries, fp_chunks, fp_chunk_sz / 1024,
+				   fp_tail_used / 1024, fp_chunk_sz / 1024);
+		} else {
+			seq_printf(m, " direct dependencies:           %11lu [max: %lu]\n",
+				   nr_list_entries, MAX_LOCKDEP_ENTRIES);
+		}
+	}
 	seq_printf(m, " indirect dependencies:         %11lu\n",
 			sum_forward_deps);
 

-- 
2.55.0




^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 6/9] locking/lockdep: Fallback to folio_pool in alloc_list_entry when static pool is full
@ 2026-08-17 17:22   ` Jim Cromie
  0 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

Use struct folio_pool to dynamically expand lock dependency storage when
the static list_entries pool is exhausted, avoiding premature lockdep
disabling.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 kernel/locking/lockdep.c           | 35 ++++++++++++++++++++++++++++++++++-
 kernel/locking/lockdep_internals.h |  2 ++
 kernel/locking/lockdep_proc.c      | 16 ++++++++++++++--
 3 files changed, 50 insertions(+), 3 deletions(-)

diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
index 2d4c5bab5af8..f0f58db090ff 100644
--- a/kernel/locking/lockdep.c
+++ b/kernel/locking/lockdep.c
@@ -58,12 +58,37 @@
 #include <linux/context_tracking.h>
 #include <linux/console.h>
 #include <linux/kasan.h>
+#include <linux/folio_pool.h>
+
+DEFINE_STATIC_KEY_TRUE(lockdep_pool_key);
+static struct folio_pool lockdep_pool =
+	FOLIO_POOL_INIT_KEY(lockdep_pool, sizeof(struct lock_list),
+			    FOLIO_POOL_64K_ORDER, &lockdep_pool_key);
+
+static int __init setup_lockdep_folio_pool(char *str)
+{
+	bool enable;
+
+	if (!kstrtobool(str, &enable)) {
+		if (enable)
+			static_branch_enable(&lockdep_pool_key);
+		else
+			static_branch_disable(&lockdep_pool_key);
+	}
+	return 1;
+}
+__setup("lockdep.folio_pool=", setup_lockdep_folio_pool);
 
 #include <asm/sections.h>
 
 #include "lockdep_internals.h"
 #include "lock_events.h"
 
+void lockdep_pool_stats(unsigned int *nr_chunks, size_t *chunk_size, size_t *tail_used)
+{
+	folio_pool_stats(&lockdep_pool, nr_chunks, chunk_size, tail_used);
+}
+
 #include <trace/events/lock.h>
 
 #ifdef CONFIG_PROVE_LOCKING
@@ -1404,11 +1429,19 @@ static struct lock_list *alloc_list_entry(void)
 				      ARRAY_SIZE(list_entries));
 
 	if (idx >= ARRAY_SIZE(list_entries)) {
+		struct lock_list *p;
+
+		p = folio_pool_alloc_type(&lockdep_pool, struct lock_list,
+					  GFP_ATOMIC);
+		if (p) {
+			nr_list_entries++;
+			return p;
+		}
 		if (!debug_locks_off_graph_unlock())
 			return NULL;
 
 		nbcon_cpu_emergency_enter();
-		print_lockdep_off("BUG: MAX_LOCKDEP_ENTRIES too low!");
+		print_lockdep_off("BUG: MAX_LOCKDEP_ENTRIES too low and folio_pool exhausted!");
 		dump_stack();
 		nbcon_cpu_emergency_exit();
 		return NULL;
diff --git a/kernel/locking/lockdep_internals.h b/kernel/locking/lockdep_internals.h
index 0e5e6ffe91a3..f802160c0dd5 100644
--- a/kernel/locking/lockdep_internals.h
+++ b/kernel/locking/lockdep_internals.h
@@ -159,6 +159,8 @@ extern unsigned long max_lock_class_idx;
 extern struct lock_class lock_classes[MAX_LOCKDEP_KEYS];
 extern unsigned long lock_classes_in_use[];
 
+void lockdep_pool_stats(unsigned int *nr_chunks, size_t *chunk_size, size_t *tail_used);
+
 #ifdef CONFIG_PROVE_LOCKING
 extern unsigned long lockdep_count_forward_deps(struct lock_class *);
 extern unsigned long lockdep_count_backward_deps(struct lock_class *);
diff --git a/kernel/locking/lockdep_proc.c b/kernel/locking/lockdep_proc.c
index 1916db9aa46b..3e6f24ae9b34 100644
--- a/kernel/locking/lockdep_proc.c
+++ b/kernel/locking/lockdep_proc.c
@@ -288,8 +288,20 @@ static int lockdep_stats_show(struct seq_file *m, void *v)
 			nr_lock_classes, MAX_LOCKDEP_KEYS);
 	seq_printf(m, " dynamic-keys:                  %11lu\n",
 			nr_dynamic_keys);
-	seq_printf(m, " direct dependencies:           %11lu [max: %lu]\n",
-			nr_list_entries, MAX_LOCKDEP_ENTRIES);
+	{
+		unsigned int fp_chunks = 0;
+		size_t fp_chunk_sz = 0, fp_tail_used = 0;
+
+		lockdep_pool_stats(&fp_chunks, &fp_chunk_sz, &fp_tail_used);
+		if (fp_chunks) {
+			seq_printf(m, " direct dependencies:           %11lu [dynamic: %u x %zu kB, tail: %zu kB/%zu kB]\n",
+				   nr_list_entries, fp_chunks, fp_chunk_sz / 1024,
+				   fp_tail_used / 1024, fp_chunk_sz / 1024);
+		} else {
+			seq_printf(m, " direct dependencies:           %11lu [max: %lu]\n",
+				   nr_list_entries, MAX_LOCKDEP_ENTRIES);
+		}
+	}
 	seq_printf(m, " indirect dependencies:         %11lu\n",
 			sum_forward_deps);
 

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 7/9] locking/lockdep: Traverse adjacency lists directly in zap_class()
  2026-08-17 17:22 ` Jim Cromie
@ 2026-08-17 17:22   ` Jim Cromie
  -1 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie via B4 Relay @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

From: Jim Cromie <jim.cromie@gmail.com>

By definition, every dependency involving a lock class resides in either
its locks_after (forward) or locks_before (backward) list.

During module unload (rmmod) cleanup, zap_class() historically took a
shortcut: it scanned the global list_entries_in_use allocator bitmap,
testing every allocated dependency across the entire kernel against a
fixed static array.

That shortcut worked only because all dependencies were confined to a
single compile-time array. It conflates graph topology with allocator
storage layout, and breaks down the moment lockdep needs to scale beyond
static allocations.

Rework zap_class() to traverse class->locks_after and class->locks_before
directly, unlinking each edge and its matching counterpart on the
connected lock class via RCU while clearing the respective allocation
slot bits.

This restores the canonical graph deletion flow, touches only the edges
connected to the zapped class, and allows lockdep to safely support
dynamic allocators.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 kernel/locking/lockdep.c | 31 ++++++++++++++++++++++++-------
 1 file changed, 24 insertions(+), 7 deletions(-)

diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
index f0f58db090ff..c8975c9282bb 100644
--- a/kernel/locking/lockdep.c
+++ b/kernel/locking/lockdep.c
@@ -6276,8 +6276,7 @@ static void remove_class_from_lock_chains(struct pending_free *pf,
  */
 static void zap_class(struct pending_free *pf, struct lock_class *class)
 {
-	struct lock_list *entry;
-	int i;
+	struct lock_list *entry, *tmp, *other, *other_tmp;
 
 	WARN_ON_ONCE(!class->key);
 
@@ -6285,11 +6284,29 @@ static void zap_class(struct pending_free *pf, struct lock_class *class)
 	 * Remove all dependencies this lock is
 	 * involved in:
 	 */
-	for_each_set_bit(i, list_entries_in_use, ARRAY_SIZE(list_entries)) {
-		entry = list_entries + i;
-		if (entry->class != class && entry->links_to != class)
-			continue;
-		__clear_bit(i, list_entries_in_use);
+	list_for_each_entry_safe(entry, tmp, &class->locks_after, entry) {
+		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_before, entry) {
+			if (other->links_to == class) {
+				__clear_bit(other - list_entries, list_entries_in_use);
+				nr_list_entries--;
+				list_del_rcu(&other->entry);
+				break;
+			}
+		}
+		__clear_bit(entry - list_entries, list_entries_in_use);
+		nr_list_entries--;
+		list_del_rcu(&entry->entry);
+	}
+	list_for_each_entry_safe(entry, tmp, &class->locks_before, entry) {
+		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_after, entry) {
+			if (other->links_to == class) {
+				__clear_bit(other - list_entries, list_entries_in_use);
+				nr_list_entries--;
+				list_del_rcu(&other->entry);
+				break;
+			}
+		}
+		__clear_bit(entry - list_entries, list_entries_in_use);
 		nr_list_entries--;
 		list_del_rcu(&entry->entry);
 	}

-- 
2.55.0



^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 7/9] locking/lockdep: Traverse adjacency lists directly in zap_class()
@ 2026-08-17 17:22   ` Jim Cromie
  0 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

By definition, every dependency involving a lock class resides in either
its locks_after (forward) or locks_before (backward) list.

During module unload (rmmod) cleanup, zap_class() historically took a
shortcut: it scanned the global list_entries_in_use allocator bitmap,
testing every allocated dependency across the entire kernel against a
fixed static array.

That shortcut worked only because all dependencies were confined to a
single compile-time array. It conflates graph topology with allocator
storage layout, and breaks down the moment lockdep needs to scale beyond
static allocations.

Rework zap_class() to traverse class->locks_after and class->locks_before
directly, unlinking each edge and its matching counterpart on the
connected lock class via RCU while clearing the respective allocation
slot bits.

This restores the canonical graph deletion flow, touches only the edges
connected to the zapped class, and allows lockdep to safely support
dynamic allocators.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 kernel/locking/lockdep.c | 31 ++++++++++++++++++++++++-------
 1 file changed, 24 insertions(+), 7 deletions(-)

diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
index f0f58db090ff..c8975c9282bb 100644
--- a/kernel/locking/lockdep.c
+++ b/kernel/locking/lockdep.c
@@ -6276,8 +6276,7 @@ static void remove_class_from_lock_chains(struct pending_free *pf,
  */
 static void zap_class(struct pending_free *pf, struct lock_class *class)
 {
-	struct lock_list *entry;
-	int i;
+	struct lock_list *entry, *tmp, *other, *other_tmp;
 
 	WARN_ON_ONCE(!class->key);
 
@@ -6285,11 +6284,29 @@ static void zap_class(struct pending_free *pf, struct lock_class *class)
 	 * Remove all dependencies this lock is
 	 * involved in:
 	 */
-	for_each_set_bit(i, list_entries_in_use, ARRAY_SIZE(list_entries)) {
-		entry = list_entries + i;
-		if (entry->class != class && entry->links_to != class)
-			continue;
-		__clear_bit(i, list_entries_in_use);
+	list_for_each_entry_safe(entry, tmp, &class->locks_after, entry) {
+		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_before, entry) {
+			if (other->links_to == class) {
+				__clear_bit(other - list_entries, list_entries_in_use);
+				nr_list_entries--;
+				list_del_rcu(&other->entry);
+				break;
+			}
+		}
+		__clear_bit(entry - list_entries, list_entries_in_use);
+		nr_list_entries--;
+		list_del_rcu(&entry->entry);
+	}
+	list_for_each_entry_safe(entry, tmp, &class->locks_before, entry) {
+		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_after, entry) {
+			if (other->links_to == class) {
+				__clear_bit(other - list_entries, list_entries_in_use);
+				nr_list_entries--;
+				list_del_rcu(&other->entry);
+				break;
+			}
+		}
+		__clear_bit(entry - list_entries, list_entries_in_use);
 		nr_list_entries--;
 		list_del_rcu(&entry->entry);
 	}

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 8/9] locking/lockdep: Shrink static list_entries array to early bootstrap buffer
  2026-08-17 17:22 ` Jim Cromie
@ 2026-08-17 17:22   ` Jim Cromie
  -1 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie via B4 Relay @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

From: Jim Cromie <jim.cromie@gmail.com>

Shrink the static compile-time list_entries[] array and its tracking
bitmap from MAX_LOCKDEP_ENTRIES (32,768 entries, ~1.31 MB BSS) down to
a 4096-entry early boot bootstrap buffer (~160 KB BSS).

During early boot before mem_init(), lockdep records initial spinlock
dependencies in the static bootstrap buffer. Once the buddy page
allocator is online, alloc_list_entry() seamlessly spills all subsequent
lock dependency allocations directly into lockdep_pool in 64KB direct-map
folios.

Adds a core_initcall telemetry hook (lockdep_boot_report) to log the
exact count of bootstrap entries consumed prior to buddy initialization.

Empirical boot telemetry confirms zero regressions and validates the
two-tier spillway on x86 SMP:
  - Pre-buddy watermark: 668/4096 bootstrap entries consumed (16.3%).
  - Post-buddy scaling: 5,058 total direct dependencies and 22,577
    indirect paths validated, with 962 entries operating dynamically
    inside lockdep_pool direct-map folios.

Reclaims ~1.15 MB of static unswappable kernel .bss memory (88% reduction)
while enabling unbounded lockdep scaling on dense NUMA/container workloads.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 kernel/locking/lockdep.c | 33 +++++++++++++++++++++++++++------
 1 file changed, 27 insertions(+), 6 deletions(-)

diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
index c8975c9282bb..5968a976bf8e 100644
--- a/kernel/locking/lockdep.c
+++ b/kernel/locking/lockdep.c
@@ -229,9 +229,11 @@ static inline int debug_locks_off_graph_unlock(void)
 	return ret;
 }
 
+#define BOOTSTRAP_LOCKDEP_ENTRIES 4096UL
+
 unsigned long nr_list_entries;
-static struct lock_list list_entries[MAX_LOCKDEP_ENTRIES];
-static DECLARE_BITMAP(list_entries_in_use, MAX_LOCKDEP_ENTRIES);
+static struct lock_list list_entries[BOOTSTRAP_LOCKDEP_ENTRIES];
+static DECLARE_BITMAP(list_entries_in_use, BOOTSTRAP_LOCKDEP_ENTRIES);
 
 /*
  * All data structures here are protected by the global debug_lock.
@@ -6271,6 +6273,12 @@ static void remove_class_from_lock_chains(struct pending_free *pf,
 	}
 }
 
+static inline bool is_bootstrap_entry(const struct lock_list *entry)
+{
+	return entry >= list_entries &&
+	       entry < list_entries + ARRAY_SIZE(list_entries);
+}
+
 /*
  * Remove all references to a lock class. The caller must hold the graph lock.
  */
@@ -6287,26 +6295,30 @@ static void zap_class(struct pending_free *pf, struct lock_class *class)
 	list_for_each_entry_safe(entry, tmp, &class->locks_after, entry) {
 		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_before, entry) {
 			if (other->links_to == class) {
-				__clear_bit(other - list_entries, list_entries_in_use);
+				if (is_bootstrap_entry(other))
+					__clear_bit(other - list_entries, list_entries_in_use);
 				nr_list_entries--;
 				list_del_rcu(&other->entry);
 				break;
 			}
 		}
-		__clear_bit(entry - list_entries, list_entries_in_use);
+		if (is_bootstrap_entry(entry))
+			__clear_bit(entry - list_entries, list_entries_in_use);
 		nr_list_entries--;
 		list_del_rcu(&entry->entry);
 	}
 	list_for_each_entry_safe(entry, tmp, &class->locks_before, entry) {
 		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_after, entry) {
 			if (other->links_to == class) {
-				__clear_bit(other - list_entries, list_entries_in_use);
+				if (is_bootstrap_entry(other))
+					__clear_bit(other - list_entries, list_entries_in_use);
 				nr_list_entries--;
 				list_del_rcu(&other->entry);
 				break;
 			}
 		}
-		__clear_bit(entry - list_entries, list_entries_in_use);
+		if (is_bootstrap_entry(entry))
+			__clear_bit(entry - list_entries, list_entries_in_use);
 		nr_list_entries--;
 		list_del_rcu(&entry->entry);
 	}
@@ -6718,6 +6730,15 @@ void __init lockdep_init(void)
 	       sizeof(((struct task_struct *)NULL)->held_locks));
 }
 
+static int __init lockdep_boot_report(void)
+{
+	pr_info("lockdep: %lu/%lu bootstrap entries used before buddy init, folio_pool active\n",
+		min_t(unsigned long, nr_list_entries, ARRAY_SIZE(list_entries)),
+		ARRAY_SIZE(list_entries));
+	return 0;
+}
+core_initcall(lockdep_boot_report);
+
 static void
 print_freed_lock_bug(struct task_struct *curr, const void *mem_from,
 		     const void *mem_to, struct held_lock *hlock)

-- 
2.55.0



^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 8/9] locking/lockdep: Shrink static list_entries array to early bootstrap buffer
@ 2026-08-17 17:22   ` Jim Cromie
  0 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

Shrink the static compile-time list_entries[] array and its tracking
bitmap from MAX_LOCKDEP_ENTRIES (32,768 entries, ~1.31 MB BSS) down to
a 4096-entry early boot bootstrap buffer (~160 KB BSS).

During early boot before mem_init(), lockdep records initial spinlock
dependencies in the static bootstrap buffer. Once the buddy page
allocator is online, alloc_list_entry() seamlessly spills all subsequent
lock dependency allocations directly into lockdep_pool in 64KB direct-map
folios.

Adds a core_initcall telemetry hook (lockdep_boot_report) to log the
exact count of bootstrap entries consumed prior to buddy initialization.

Empirical boot telemetry confirms zero regressions and validates the
two-tier spillway on x86 SMP:
  - Pre-buddy watermark: 668/4096 bootstrap entries consumed (16.3%).
  - Post-buddy scaling: 5,058 total direct dependencies and 22,577
    indirect paths validated, with 962 entries operating dynamically
    inside lockdep_pool direct-map folios.

Reclaims ~1.15 MB of static unswappable kernel .bss memory (88% reduction)
while enabling unbounded lockdep scaling on dense NUMA/container workloads.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 kernel/locking/lockdep.c | 33 +++++++++++++++++++++++++++------
 1 file changed, 27 insertions(+), 6 deletions(-)

diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
index c8975c9282bb..5968a976bf8e 100644
--- a/kernel/locking/lockdep.c
+++ b/kernel/locking/lockdep.c
@@ -229,9 +229,11 @@ static inline int debug_locks_off_graph_unlock(void)
 	return ret;
 }
 
+#define BOOTSTRAP_LOCKDEP_ENTRIES 4096UL
+
 unsigned long nr_list_entries;
-static struct lock_list list_entries[MAX_LOCKDEP_ENTRIES];
-static DECLARE_BITMAP(list_entries_in_use, MAX_LOCKDEP_ENTRIES);
+static struct lock_list list_entries[BOOTSTRAP_LOCKDEP_ENTRIES];
+static DECLARE_BITMAP(list_entries_in_use, BOOTSTRAP_LOCKDEP_ENTRIES);
 
 /*
  * All data structures here are protected by the global debug_lock.
@@ -6271,6 +6273,12 @@ static void remove_class_from_lock_chains(struct pending_free *pf,
 	}
 }
 
+static inline bool is_bootstrap_entry(const struct lock_list *entry)
+{
+	return entry >= list_entries &&
+	       entry < list_entries + ARRAY_SIZE(list_entries);
+}
+
 /*
  * Remove all references to a lock class. The caller must hold the graph lock.
  */
@@ -6287,26 +6295,30 @@ static void zap_class(struct pending_free *pf, struct lock_class *class)
 	list_for_each_entry_safe(entry, tmp, &class->locks_after, entry) {
 		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_before, entry) {
 			if (other->links_to == class) {
-				__clear_bit(other - list_entries, list_entries_in_use);
+				if (is_bootstrap_entry(other))
+					__clear_bit(other - list_entries, list_entries_in_use);
 				nr_list_entries--;
 				list_del_rcu(&other->entry);
 				break;
 			}
 		}
-		__clear_bit(entry - list_entries, list_entries_in_use);
+		if (is_bootstrap_entry(entry))
+			__clear_bit(entry - list_entries, list_entries_in_use);
 		nr_list_entries--;
 		list_del_rcu(&entry->entry);
 	}
 	list_for_each_entry_safe(entry, tmp, &class->locks_before, entry) {
 		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_after, entry) {
 			if (other->links_to == class) {
-				__clear_bit(other - list_entries, list_entries_in_use);
+				if (is_bootstrap_entry(other))
+					__clear_bit(other - list_entries, list_entries_in_use);
 				nr_list_entries--;
 				list_del_rcu(&other->entry);
 				break;
 			}
 		}
-		__clear_bit(entry - list_entries, list_entries_in_use);
+		if (is_bootstrap_entry(entry))
+			__clear_bit(entry - list_entries, list_entries_in_use);
 		nr_list_entries--;
 		list_del_rcu(&entry->entry);
 	}
@@ -6718,6 +6730,15 @@ void __init lockdep_init(void)
 	       sizeof(((struct task_struct *)NULL)->held_locks));
 }
 
+static int __init lockdep_boot_report(void)
+{
+	pr_info("lockdep: %lu/%lu bootstrap entries used before buddy init, folio_pool active\n",
+		min_t(unsigned long, nr_list_entries, ARRAY_SIZE(list_entries)),
+		ARRAY_SIZE(list_entries));
+	return 0;
+}
+core_initcall(lockdep_boot_report);
+
 static void
 print_freed_lock_bug(struct task_struct *curr, const void *mem_from,
 		     const void *mem_to, struct held_lock *hlock)

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 9/9] locking/lockdep: Migrate and compact boot-time dependency graph from __initdata
  2026-08-17 17:22 ` Jim Cromie
@ 2026-08-17 17:22   ` Jim Cromie
  -1 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie via B4 Relay @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

From: Jim Cromie <jim.cromie@gmail.com>

Mark early_list_entries[BOOTSTRAP_LOCKDEP_ENTRIES] and its tracking
bitmap as __initdata, dropping permanent static .bss allocation for
lockdep dependency edges from 1.31 MB to 0 KB.

During early boot before buddy page allocator initialization, lockdep
records initial spinlock dependencies into early_list_entries. At
late_initcall, lockdep_compact_boot_graph() completes the handoff:

0. Pre-allocates a 64 KB folio chunk from lockdep_pool outside
   graph_lock to prevent memory allocation recursion with core MM locks.
1. Under raw_local_irq_save() and graph_lock(), walks all_lock_classes,
   migrates live bootstrap nodes into contiguous folio slots, and
   updates locks_after/locks_before lists via list_replace_rcu().
2. Clears bootstrap pointers to NULL so subsequent runtime allocations
   and zap_class() operations bypass the bootstrap address range.
3. Adjusts nr_list_entries to preserve exact live edge accounting.

When free_initmem() executes before userspace launch, the entire 160 KB
bootstrap array is released back to the page allocator.

Live telemetry and graph verification:
- Pre-buddy watermark: 714/4096 bootstrap entries recorded.
- Late initcall migration: 2,656 edges compacted into lockdep_pool.
- Post-boot validation: BFS multi-hop traversals and IRQ constraint
  checks navigate the compacted folio topology with zero corruption.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
Notes:
  Live proof of post-compaction validator integrity on v7.2-rc7:
  Immediately following free_initmem(), a live multi-hop locking
  constraint warning (fs_reclaim vs mmu_notifier) triggered lockdep's
  full reporting pipeline.

  This report acts as concrete validation of four core invariants:
  1. BFS Graph Traversal: __bfs() successfully resolved a 3-hop cycle
     across the newly migrated direct-map folio nodes.
  2. Payload Integrity: Stack traces stored during early boot were
     dereferenced from target->trace and printed without corruption.
  3. RCU List Updates: list_replace_rcu() preserved list head pointers
     with zero data races or poisoned node dereferences.
  4. Zero Stale References: No accesses to reclaimed .init.data memory
     occurred during the full graph walk and stack formatting.
---
 kernel/locking/lockdep.c | 227 ++++++++++++++++++++++++++++++++++-------------
 1 file changed, 164 insertions(+), 63 deletions(-)

diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
index 5968a976bf8e..d9541207b342 100644
--- a/kernel/locking/lockdep.c
+++ b/kernel/locking/lockdep.c
@@ -231,9 +231,11 @@ static inline int debug_locks_off_graph_unlock(void)
 
 #define BOOTSTRAP_LOCKDEP_ENTRIES 4096UL
 
+static struct lock_list early_list_entries[BOOTSTRAP_LOCKDEP_ENTRIES] __initdata;
+static unsigned long early_list_entries_in_use[BITS_TO_LONGS(BOOTSTRAP_LOCKDEP_ENTRIES)] __initdata;
+static struct lock_list *bootstrap_entries __read_mostly = early_list_entries;
+static unsigned long *bootstrap_entries_in_use __read_mostly = early_list_entries_in_use;
 unsigned long nr_list_entries;
-static struct lock_list list_entries[BOOTSTRAP_LOCKDEP_ENTRIES];
-static DECLARE_BITMAP(list_entries_in_use, BOOTSTRAP_LOCKDEP_ENTRIES);
 
 /*
  * All data structures here are protected by the global debug_lock.
@@ -1055,9 +1057,9 @@ static bool class_lock_list_valid(struct lock_class *c, struct list_head *h)
 
 	list_for_each_entry(e, h, entry) {
 		if (e->links_to != c) {
-			printk(KERN_INFO "class %s: mismatch for lock entry %ld; class %s <> %s",
+			printk(KERN_INFO "class %s: mismatch for lock entry %px; class %s <> %s",
 			       c->name ? : "(?)",
-			       (unsigned long)(e - list_entries),
+			       e,
 			       e->links_to && e->links_to->name ?
 			       e->links_to->name : "(?)",
 			       e->class && e->class->name ? e->class->name :
@@ -1146,35 +1148,37 @@ static bool __check_data_structures(void)
 		}
 	}
 
-	/*
-	 * Check whether all list entries that are in use occur in a class
-	 * lock list.
-	 */
-	for_each_set_bit(i, list_entries_in_use, ARRAY_SIZE(list_entries)) {
-		e = list_entries + i;
-		if (!in_any_class_list(&e->entry)) {
-			printk(KERN_INFO "list entry %d is not in any class list; class %s <> %s\n",
-			       (unsigned int)(e - list_entries),
-			       e->class->name ? : "(?)",
-			       e->links_to->name ? : "(?)");
-			return false;
+	if (bootstrap_entries) {
+		/*
+		 * Check whether all list entries that are in use occur in a class
+		 * lock list.
+		 */
+		for_each_set_bit(i, bootstrap_entries_in_use, BOOTSTRAP_LOCKDEP_ENTRIES) {
+			e = bootstrap_entries + i;
+			if (!in_any_class_list(&e->entry)) {
+				printk(KERN_INFO "list entry %d is not in any class list; class %s <> %s\n",
+				       (unsigned int)(e - bootstrap_entries),
+				       e->class->name ? : "(?)",
+				       e->links_to->name ? : "(?)");
+				return false;
+			}
 		}
-	}
 
-	/*
-	 * Check whether all list entries that are not in use do not occur in
-	 * a class lock list.
-	 */
-	for_each_clear_bit(i, list_entries_in_use, ARRAY_SIZE(list_entries)) {
-		e = list_entries + i;
-		if (in_any_class_list(&e->entry)) {
-			printk(KERN_INFO "list entry %d occurs in a class list; class %s <> %s\n",
-			       (unsigned int)(e - list_entries),
-			       e->class && e->class->name ? e->class->name :
-			       "(?)",
-			       e->links_to && e->links_to->name ?
-			       e->links_to->name : "(?)");
-			return false;
+		/*
+		 * Check whether all list entries that are not in use do not occur in
+		 * a class lock list.
+		 */
+		for_each_clear_bit(i, bootstrap_entries_in_use, BOOTSTRAP_LOCKDEP_ENTRIES) {
+			e = bootstrap_entries + i;
+			if (in_any_class_list(&e->entry)) {
+				printk(KERN_INFO "list entry %d occurs in a class list; class %s <> %s\n",
+				       (unsigned int)(e - bootstrap_entries),
+				       e->class && e->class->name ? e->class->name :
+				       "(?)",
+				       e->links_to && e->links_to->name ?
+				       e->links_to->name : "(?)");
+				return false;
+			}
 		}
 	}
 
@@ -1427,30 +1431,32 @@ register_lock_class(struct lockdep_map *lock, unsigned int subclass, int force)
  */
 static struct lock_list *alloc_list_entry(void)
 {
-	int idx = find_first_zero_bit(list_entries_in_use,
-				      ARRAY_SIZE(list_entries));
+	struct lock_list *p;
 
-	if (idx >= ARRAY_SIZE(list_entries)) {
-		struct lock_list *p;
+	if (bootstrap_entries) {
+		int idx = find_first_zero_bit(bootstrap_entries_in_use,
+					      BOOTSTRAP_LOCKDEP_ENTRIES);
 
-		p = folio_pool_alloc_type(&lockdep_pool, struct lock_list,
-					  GFP_ATOMIC);
-		if (p) {
+		if (idx < BOOTSTRAP_LOCKDEP_ENTRIES) {
+			__set_bit(idx, bootstrap_entries_in_use);
 			nr_list_entries++;
-			return p;
+			return bootstrap_entries + idx;
 		}
-		if (!debug_locks_off_graph_unlock())
-			return NULL;
+	}
 
-		nbcon_cpu_emergency_enter();
-		print_lockdep_off("BUG: MAX_LOCKDEP_ENTRIES too low and folio_pool exhausted!");
-		dump_stack();
-		nbcon_cpu_emergency_exit();
-		return NULL;
+	p = folio_pool_alloc_type(&lockdep_pool, struct lock_list, GFP_ATOMIC);
+	if (p) {
+		nr_list_entries++;
+		return p;
 	}
-	nr_list_entries++;
-	__set_bit(idx, list_entries_in_use);
-	return list_entries + idx;
+	if (!debug_locks_off_graph_unlock())
+		return NULL;
+
+	nbcon_cpu_emergency_enter();
+	print_lockdep_off("BUG: MAX_LOCKDEP_ENTRIES too low and folio_pool exhausted!");
+	dump_stack();
+	nbcon_cpu_emergency_exit();
+	return NULL;
 }
 
 /*
@@ -6275,8 +6281,15 @@ static void remove_class_from_lock_chains(struct pending_free *pf,
 
 static inline bool is_bootstrap_entry(const struct lock_list *entry)
 {
-	return entry >= list_entries &&
-	       entry < list_entries + ARRAY_SIZE(list_entries);
+	return bootstrap_entries &&
+	       entry >= bootstrap_entries &&
+	       entry < bootstrap_entries + BOOTSTRAP_LOCKDEP_ENTRIES;
+}
+
+static inline void clear_bootstrap_entry_bit(const struct lock_list *entry)
+{
+	if (is_bootstrap_entry(entry))
+		__clear_bit(entry - bootstrap_entries, bootstrap_entries_in_use);
 }
 
 /*
@@ -6295,30 +6308,26 @@ static void zap_class(struct pending_free *pf, struct lock_class *class)
 	list_for_each_entry_safe(entry, tmp, &class->locks_after, entry) {
 		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_before, entry) {
 			if (other->links_to == class) {
-				if (is_bootstrap_entry(other))
-					__clear_bit(other - list_entries, list_entries_in_use);
+				clear_bootstrap_entry_bit(other);
 				nr_list_entries--;
 				list_del_rcu(&other->entry);
 				break;
 			}
 		}
-		if (is_bootstrap_entry(entry))
-			__clear_bit(entry - list_entries, list_entries_in_use);
+		clear_bootstrap_entry_bit(entry);
 		nr_list_entries--;
 		list_del_rcu(&entry->entry);
 	}
 	list_for_each_entry_safe(entry, tmp, &class->locks_before, entry) {
 		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_after, entry) {
 			if (other->links_to == class) {
-				if (is_bootstrap_entry(other))
-					__clear_bit(other - list_entries, list_entries_in_use);
+				clear_bootstrap_entry_bit(other);
 				nr_list_entries--;
 				list_del_rcu(&other->entry);
 				break;
 			}
 		}
-		if (is_bootstrap_entry(entry))
-			__clear_bit(entry - list_entries, list_entries_in_use);
+		clear_bootstrap_entry_bit(entry);
 		nr_list_entries--;
 		list_del_rcu(&entry->entry);
 	}
@@ -6693,6 +6702,9 @@ EXPORT_SYMBOL_GPL(lockdep_unregister_key);
 
 void __init lockdep_init(void)
 {
+	bootstrap_entries = early_list_entries;
+	bootstrap_entries_in_use = early_list_entries_in_use;
+
 	pr_info("Lock dependency validator: Copyright (c) 2006 Red Hat, Inc., Ingo Molnar\n");
 
 	pr_info("... MAX_LOCKDEP_SUBCLASSES:  %lu\n", MAX_LOCKDEP_SUBCLASSES);
@@ -6707,8 +6719,8 @@ void __init lockdep_init(void)
 	       (sizeof(lock_classes) +
 		sizeof(lock_classes_in_use) +
 		sizeof(classhash_table) +
-		sizeof(list_entries) +
-		sizeof(list_entries_in_use) +
+		sizeof(early_list_entries) +
+		sizeof(early_list_entries_in_use) +
 		sizeof(chainhash_table) +
 		sizeof(delayed_free)
 #ifdef CONFIG_PROVE_LOCKING
@@ -6733,12 +6745,101 @@ void __init lockdep_init(void)
 static int __init lockdep_boot_report(void)
 {
 	pr_info("lockdep: %lu/%lu bootstrap entries used before buddy init, folio_pool active\n",
-		min_t(unsigned long, nr_list_entries, ARRAY_SIZE(list_entries)),
-		ARRAY_SIZE(list_entries));
+		min_t(unsigned long, nr_list_entries, BOOTSTRAP_LOCKDEP_ENTRIES),
+		BOOTSTRAP_LOCKDEP_ENTRIES);
 	return 0;
 }
 core_initcall(lockdep_boot_report);
 
+static int __init lockdep_compact_boot_graph(void)
+{
+	struct lock_class *class;
+	struct lock_list *entry, *tmp, *new_entry;
+	unsigned long flags;
+	unsigned long migrated = 0;
+
+	if (!debug_locks)
+		return 0;
+
+	/* Pre-allocate 64KB folio chunk outside graph_lock to avoid MM recursion */
+	new_entry = folio_pool_alloc_type(&lockdep_pool, struct lock_list, GFP_KERNEL);
+	if (!new_entry) {
+		pr_err("lockdep: failed to pre-allocate folio chunk for boot compaction\n");
+		return -ENOMEM;
+	}
+
+	raw_local_irq_save(flags);
+	if (!graph_lock()) {
+		raw_local_irq_restore(flags);
+		return 0;
+	}
+
+	list_for_each_entry(class, &all_lock_classes, lock_entry) {
+		list_for_each_entry_safe(entry, tmp, &class->locks_after, entry) {
+			if (is_bootstrap_entry(entry)) {
+				if (new_entry) {
+					*new_entry = *entry;
+					list_replace_rcu(&entry->entry, &new_entry->entry);
+					new_entry = NULL;
+				} else {
+					struct lock_list *slot;
+
+					slot = folio_pool_alloc_type(&lockdep_pool,
+								     struct lock_list,
+								     GFP_ATOMIC);
+					if (!slot) {
+						debug_locks_off_graph_unlock();
+						raw_local_irq_restore(flags);
+						pr_err("lockdep: folio chunk exhausted during boot compaction\n");
+						return -ENOMEM;
+					}
+					*slot = *entry;
+					list_replace_rcu(&entry->entry, &slot->entry);
+				}
+				migrated++;
+			}
+		}
+
+		list_for_each_entry_safe(entry, tmp, &class->locks_before, entry) {
+			if (is_bootstrap_entry(entry)) {
+				if (new_entry) {
+					*new_entry = *entry;
+					list_replace_rcu(&entry->entry, &new_entry->entry);
+					new_entry = NULL;
+				} else {
+					struct lock_list *slot;
+
+					slot = folio_pool_alloc_type(&lockdep_pool,
+								     struct lock_list,
+								     GFP_ATOMIC);
+					if (!slot) {
+						debug_locks_off_graph_unlock();
+						raw_local_irq_restore(flags);
+						pr_err("lockdep: folio chunk exhausted during boot compaction\n");
+						return -ENOMEM;
+					}
+					*slot = *entry;
+					list_replace_rcu(&entry->entry, &slot->entry);
+				}
+				migrated++;
+			}
+		}
+	}
+
+	/* Adjust counter so compaction does not double-count migrated nodes */
+	nr_list_entries -= migrated;
+
+	bootstrap_entries = NULL;
+	bootstrap_entries_in_use = NULL;
+	graph_unlock();
+	raw_local_irq_restore(flags);
+
+	pr_info("lockdep: compacted %lu boot entries into folio_pool, freeing bootstrap memory\n",
+		migrated);
+	return 0;
+}
+late_initcall(lockdep_compact_boot_graph);
+
 static void
 print_freed_lock_bug(struct task_struct *curr, const void *mem_from,
 		     const void *mem_to, struct held_lock *hlock)

-- 
2.55.0



^ permalink raw reply related	[flat|nested] 32+ messages in thread

* [PATCH 9/9] locking/lockdep: Migrate and compact boot-time dependency graph from __initdata
@ 2026-08-17 17:22   ` Jim Cromie
  0 siblings, 0 replies; 32+ messages in thread
From: Jim Cromie @ 2026-08-17 17:22 UTC (permalink / raw)
  To: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Boqun Feng
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev, Jim Cromie

Mark early_list_entries[BOOTSTRAP_LOCKDEP_ENTRIES] and its tracking
bitmap as __initdata, dropping permanent static .bss allocation for
lockdep dependency edges from 1.31 MB to 0 KB.

During early boot before buddy page allocator initialization, lockdep
records initial spinlock dependencies into early_list_entries. At
late_initcall, lockdep_compact_boot_graph() completes the handoff:

0. Pre-allocates a 64 KB folio chunk from lockdep_pool outside
   graph_lock to prevent memory allocation recursion with core MM locks.
1. Under raw_local_irq_save() and graph_lock(), walks all_lock_classes,
   migrates live bootstrap nodes into contiguous folio slots, and
   updates locks_after/locks_before lists via list_replace_rcu().
2. Clears bootstrap pointers to NULL so subsequent runtime allocations
   and zap_class() operations bypass the bootstrap address range.
3. Adjusts nr_list_entries to preserve exact live edge accounting.

When free_initmem() executes before userspace launch, the entire 160 KB
bootstrap array is released back to the page allocator.

Live telemetry and graph verification:
- Pre-buddy watermark: 714/4096 bootstrap entries recorded.
- Late initcall migration: 2,656 edges compacted into lockdep_pool.
- Post-boot validation: BFS multi-hop traversals and IRQ constraint
  checks navigate the compacted folio topology with zero corruption.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
Notes:
  Live proof of post-compaction validator integrity on v7.2-rc7:
  Immediately following free_initmem(), a live multi-hop locking
  constraint warning (fs_reclaim vs mmu_notifier) triggered lockdep's
  full reporting pipeline.

  This report acts as concrete validation of four core invariants:
  1. BFS Graph Traversal: __bfs() successfully resolved a 3-hop cycle
     across the newly migrated direct-map folio nodes.
  2. Payload Integrity: Stack traces stored during early boot were
     dereferenced from target->trace and printed without corruption.
  3. RCU List Updates: list_replace_rcu() preserved list head pointers
     with zero data races or poisoned node dereferences.
  4. Zero Stale References: No accesses to reclaimed .init.data memory
     occurred during the full graph walk and stack formatting.
---
 kernel/locking/lockdep.c | 227 ++++++++++++++++++++++++++++++++++-------------
 1 file changed, 164 insertions(+), 63 deletions(-)

diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
index 5968a976bf8e..d9541207b342 100644
--- a/kernel/locking/lockdep.c
+++ b/kernel/locking/lockdep.c
@@ -231,9 +231,11 @@ static inline int debug_locks_off_graph_unlock(void)
 
 #define BOOTSTRAP_LOCKDEP_ENTRIES 4096UL
 
+static struct lock_list early_list_entries[BOOTSTRAP_LOCKDEP_ENTRIES] __initdata;
+static unsigned long early_list_entries_in_use[BITS_TO_LONGS(BOOTSTRAP_LOCKDEP_ENTRIES)] __initdata;
+static struct lock_list *bootstrap_entries __read_mostly = early_list_entries;
+static unsigned long *bootstrap_entries_in_use __read_mostly = early_list_entries_in_use;
 unsigned long nr_list_entries;
-static struct lock_list list_entries[BOOTSTRAP_LOCKDEP_ENTRIES];
-static DECLARE_BITMAP(list_entries_in_use, BOOTSTRAP_LOCKDEP_ENTRIES);
 
 /*
  * All data structures here are protected by the global debug_lock.
@@ -1055,9 +1057,9 @@ static bool class_lock_list_valid(struct lock_class *c, struct list_head *h)
 
 	list_for_each_entry(e, h, entry) {
 		if (e->links_to != c) {
-			printk(KERN_INFO "class %s: mismatch for lock entry %ld; class %s <> %s",
+			printk(KERN_INFO "class %s: mismatch for lock entry %px; class %s <> %s",
 			       c->name ? : "(?)",
-			       (unsigned long)(e - list_entries),
+			       e,
 			       e->links_to && e->links_to->name ?
 			       e->links_to->name : "(?)",
 			       e->class && e->class->name ? e->class->name :
@@ -1146,35 +1148,37 @@ static bool __check_data_structures(void)
 		}
 	}
 
-	/*
-	 * Check whether all list entries that are in use occur in a class
-	 * lock list.
-	 */
-	for_each_set_bit(i, list_entries_in_use, ARRAY_SIZE(list_entries)) {
-		e = list_entries + i;
-		if (!in_any_class_list(&e->entry)) {
-			printk(KERN_INFO "list entry %d is not in any class list; class %s <> %s\n",
-			       (unsigned int)(e - list_entries),
-			       e->class->name ? : "(?)",
-			       e->links_to->name ? : "(?)");
-			return false;
+	if (bootstrap_entries) {
+		/*
+		 * Check whether all list entries that are in use occur in a class
+		 * lock list.
+		 */
+		for_each_set_bit(i, bootstrap_entries_in_use, BOOTSTRAP_LOCKDEP_ENTRIES) {
+			e = bootstrap_entries + i;
+			if (!in_any_class_list(&e->entry)) {
+				printk(KERN_INFO "list entry %d is not in any class list; class %s <> %s\n",
+				       (unsigned int)(e - bootstrap_entries),
+				       e->class->name ? : "(?)",
+				       e->links_to->name ? : "(?)");
+				return false;
+			}
 		}
-	}
 
-	/*
-	 * Check whether all list entries that are not in use do not occur in
-	 * a class lock list.
-	 */
-	for_each_clear_bit(i, list_entries_in_use, ARRAY_SIZE(list_entries)) {
-		e = list_entries + i;
-		if (in_any_class_list(&e->entry)) {
-			printk(KERN_INFO "list entry %d occurs in a class list; class %s <> %s\n",
-			       (unsigned int)(e - list_entries),
-			       e->class && e->class->name ? e->class->name :
-			       "(?)",
-			       e->links_to && e->links_to->name ?
-			       e->links_to->name : "(?)");
-			return false;
+		/*
+		 * Check whether all list entries that are not in use do not occur in
+		 * a class lock list.
+		 */
+		for_each_clear_bit(i, bootstrap_entries_in_use, BOOTSTRAP_LOCKDEP_ENTRIES) {
+			e = bootstrap_entries + i;
+			if (in_any_class_list(&e->entry)) {
+				printk(KERN_INFO "list entry %d occurs in a class list; class %s <> %s\n",
+				       (unsigned int)(e - bootstrap_entries),
+				       e->class && e->class->name ? e->class->name :
+				       "(?)",
+				       e->links_to && e->links_to->name ?
+				       e->links_to->name : "(?)");
+				return false;
+			}
 		}
 	}
 
@@ -1427,30 +1431,32 @@ register_lock_class(struct lockdep_map *lock, unsigned int subclass, int force)
  */
 static struct lock_list *alloc_list_entry(void)
 {
-	int idx = find_first_zero_bit(list_entries_in_use,
-				      ARRAY_SIZE(list_entries));
+	struct lock_list *p;
 
-	if (idx >= ARRAY_SIZE(list_entries)) {
-		struct lock_list *p;
+	if (bootstrap_entries) {
+		int idx = find_first_zero_bit(bootstrap_entries_in_use,
+					      BOOTSTRAP_LOCKDEP_ENTRIES);
 
-		p = folio_pool_alloc_type(&lockdep_pool, struct lock_list,
-					  GFP_ATOMIC);
-		if (p) {
+		if (idx < BOOTSTRAP_LOCKDEP_ENTRIES) {
+			__set_bit(idx, bootstrap_entries_in_use);
 			nr_list_entries++;
-			return p;
+			return bootstrap_entries + idx;
 		}
-		if (!debug_locks_off_graph_unlock())
-			return NULL;
+	}
 
-		nbcon_cpu_emergency_enter();
-		print_lockdep_off("BUG: MAX_LOCKDEP_ENTRIES too low and folio_pool exhausted!");
-		dump_stack();
-		nbcon_cpu_emergency_exit();
-		return NULL;
+	p = folio_pool_alloc_type(&lockdep_pool, struct lock_list, GFP_ATOMIC);
+	if (p) {
+		nr_list_entries++;
+		return p;
 	}
-	nr_list_entries++;
-	__set_bit(idx, list_entries_in_use);
-	return list_entries + idx;
+	if (!debug_locks_off_graph_unlock())
+		return NULL;
+
+	nbcon_cpu_emergency_enter();
+	print_lockdep_off("BUG: MAX_LOCKDEP_ENTRIES too low and folio_pool exhausted!");
+	dump_stack();
+	nbcon_cpu_emergency_exit();
+	return NULL;
 }
 
 /*
@@ -6275,8 +6281,15 @@ static void remove_class_from_lock_chains(struct pending_free *pf,
 
 static inline bool is_bootstrap_entry(const struct lock_list *entry)
 {
-	return entry >= list_entries &&
-	       entry < list_entries + ARRAY_SIZE(list_entries);
+	return bootstrap_entries &&
+	       entry >= bootstrap_entries &&
+	       entry < bootstrap_entries + BOOTSTRAP_LOCKDEP_ENTRIES;
+}
+
+static inline void clear_bootstrap_entry_bit(const struct lock_list *entry)
+{
+	if (is_bootstrap_entry(entry))
+		__clear_bit(entry - bootstrap_entries, bootstrap_entries_in_use);
 }
 
 /*
@@ -6295,30 +6308,26 @@ static void zap_class(struct pending_free *pf, struct lock_class *class)
 	list_for_each_entry_safe(entry, tmp, &class->locks_after, entry) {
 		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_before, entry) {
 			if (other->links_to == class) {
-				if (is_bootstrap_entry(other))
-					__clear_bit(other - list_entries, list_entries_in_use);
+				clear_bootstrap_entry_bit(other);
 				nr_list_entries--;
 				list_del_rcu(&other->entry);
 				break;
 			}
 		}
-		if (is_bootstrap_entry(entry))
-			__clear_bit(entry - list_entries, list_entries_in_use);
+		clear_bootstrap_entry_bit(entry);
 		nr_list_entries--;
 		list_del_rcu(&entry->entry);
 	}
 	list_for_each_entry_safe(entry, tmp, &class->locks_before, entry) {
 		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_after, entry) {
 			if (other->links_to == class) {
-				if (is_bootstrap_entry(other))
-					__clear_bit(other - list_entries, list_entries_in_use);
+				clear_bootstrap_entry_bit(other);
 				nr_list_entries--;
 				list_del_rcu(&other->entry);
 				break;
 			}
 		}
-		if (is_bootstrap_entry(entry))
-			__clear_bit(entry - list_entries, list_entries_in_use);
+		clear_bootstrap_entry_bit(entry);
 		nr_list_entries--;
 		list_del_rcu(&entry->entry);
 	}
@@ -6693,6 +6702,9 @@ EXPORT_SYMBOL_GPL(lockdep_unregister_key);
 
 void __init lockdep_init(void)
 {
+	bootstrap_entries = early_list_entries;
+	bootstrap_entries_in_use = early_list_entries_in_use;
+
 	pr_info("Lock dependency validator: Copyright (c) 2006 Red Hat, Inc., Ingo Molnar\n");
 
 	pr_info("... MAX_LOCKDEP_SUBCLASSES:  %lu\n", MAX_LOCKDEP_SUBCLASSES);
@@ -6707,8 +6719,8 @@ void __init lockdep_init(void)
 	       (sizeof(lock_classes) +
 		sizeof(lock_classes_in_use) +
 		sizeof(classhash_table) +
-		sizeof(list_entries) +
-		sizeof(list_entries_in_use) +
+		sizeof(early_list_entries) +
+		sizeof(early_list_entries_in_use) +
 		sizeof(chainhash_table) +
 		sizeof(delayed_free)
 #ifdef CONFIG_PROVE_LOCKING
@@ -6733,12 +6745,101 @@ void __init lockdep_init(void)
 static int __init lockdep_boot_report(void)
 {
 	pr_info("lockdep: %lu/%lu bootstrap entries used before buddy init, folio_pool active\n",
-		min_t(unsigned long, nr_list_entries, ARRAY_SIZE(list_entries)),
-		ARRAY_SIZE(list_entries));
+		min_t(unsigned long, nr_list_entries, BOOTSTRAP_LOCKDEP_ENTRIES),
+		BOOTSTRAP_LOCKDEP_ENTRIES);
 	return 0;
 }
 core_initcall(lockdep_boot_report);
 
+static int __init lockdep_compact_boot_graph(void)
+{
+	struct lock_class *class;
+	struct lock_list *entry, *tmp, *new_entry;
+	unsigned long flags;
+	unsigned long migrated = 0;
+
+	if (!debug_locks)
+		return 0;
+
+	/* Pre-allocate 64KB folio chunk outside graph_lock to avoid MM recursion */
+	new_entry = folio_pool_alloc_type(&lockdep_pool, struct lock_list, GFP_KERNEL);
+	if (!new_entry) {
+		pr_err("lockdep: failed to pre-allocate folio chunk for boot compaction\n");
+		return -ENOMEM;
+	}
+
+	raw_local_irq_save(flags);
+	if (!graph_lock()) {
+		raw_local_irq_restore(flags);
+		return 0;
+	}
+
+	list_for_each_entry(class, &all_lock_classes, lock_entry) {
+		list_for_each_entry_safe(entry, tmp, &class->locks_after, entry) {
+			if (is_bootstrap_entry(entry)) {
+				if (new_entry) {
+					*new_entry = *entry;
+					list_replace_rcu(&entry->entry, &new_entry->entry);
+					new_entry = NULL;
+				} else {
+					struct lock_list *slot;
+
+					slot = folio_pool_alloc_type(&lockdep_pool,
+								     struct lock_list,
+								     GFP_ATOMIC);
+					if (!slot) {
+						debug_locks_off_graph_unlock();
+						raw_local_irq_restore(flags);
+						pr_err("lockdep: folio chunk exhausted during boot compaction\n");
+						return -ENOMEM;
+					}
+					*slot = *entry;
+					list_replace_rcu(&entry->entry, &slot->entry);
+				}
+				migrated++;
+			}
+		}
+
+		list_for_each_entry_safe(entry, tmp, &class->locks_before, entry) {
+			if (is_bootstrap_entry(entry)) {
+				if (new_entry) {
+					*new_entry = *entry;
+					list_replace_rcu(&entry->entry, &new_entry->entry);
+					new_entry = NULL;
+				} else {
+					struct lock_list *slot;
+
+					slot = folio_pool_alloc_type(&lockdep_pool,
+								     struct lock_list,
+								     GFP_ATOMIC);
+					if (!slot) {
+						debug_locks_off_graph_unlock();
+						raw_local_irq_restore(flags);
+						pr_err("lockdep: folio chunk exhausted during boot compaction\n");
+						return -ENOMEM;
+					}
+					*slot = *entry;
+					list_replace_rcu(&entry->entry, &slot->entry);
+				}
+				migrated++;
+			}
+		}
+	}
+
+	/* Adjust counter so compaction does not double-count migrated nodes */
+	nr_list_entries -= migrated;
+
+	bootstrap_entries = NULL;
+	bootstrap_entries_in_use = NULL;
+	graph_unlock();
+	raw_local_irq_restore(flags);
+
+	pr_info("lockdep: compacted %lu boot entries into folio_pool, freeing bootstrap memory\n",
+		migrated);
+	return 0;
+}
+late_initcall(lockdep_compact_boot_graph);
+
 static void
 print_freed_lock_bug(struct task_struct *curr, const void *mem_from,
 		     const void *mem_to, struct held_lock *hlock)

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 32+ messages in thread

* Re: [PATCH 4/9] drm/gpuvm: Route gpuva_op allocations to folio_scratchpad
  2026-08-17 17:22   ` Jim Cromie
  (?)
@ 2026-08-17 17:31   ` sashiko-bot
  -1 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-08-17 17:31 UTC (permalink / raw)
  To: Jim Cromie; +Cc: dri-devel, bpf

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Unbounded memory leak of transient `struct drm_gpuva_op` objects due to bump allocator usage without intermediate resets.
- [Low] Undeclared static key variable 'gpuvm_scratchpad_key' used in drm_gpuvm_init().
--

--- Patch [4]: [PATCH 4/9] drm/gpuvm: Route gpuva_op allocations to folio_scratchpad ---
commit 6e27181fef6cb87688764c9745ab2086b2091e88
Author: Jim Cromie <jim.cromie@gmail.com>
Subject: drm/gpuvm: Route gpuva_op allocations to folio_scratchpad

This commit modifies the DRM GPUVM subsystem to route default gpuva_op
allocations to a folio scratchpad embedded in struct drm_gpuvm. It updates
gpuva_op_alloc() to use folio_scratchpad_alloc_obj() and handles the bulk
teardown during drm_gpuvm_fini().

> diff --git a/drivers/gpu/drm/drm_gpuvm.c b/drivers/gpu/drm/drm_gpuvm.c
> --- a/drivers/gpu/drm/drm_gpuvm.c
> +++ b/drivers/gpu/drm/drm_gpuvm.c
> @@ -1109,6 +1109,8 @@ drm_gpuvm_init(struct drm_gpuvm *gpuvm, const char *name,
>  	spin_lock_init(&gpuvm->evict.lock);
>  
>  	init_llist_head(&gpuvm->bo_defer);
> +	folio_scratchpad_init_key(&gpuvm->va_scratchpad, get_order(SZ_64K),
> +				  &gpuvm_scratchpad_key);

[Severity: Low]
Does this cause a compilation failure?

In drm_gpuvm_init(), the code references gpuvm_scratchpad_key, but this
static key is not defined until later in the file via the
DEFINE_FOLIO_POOL_STATIC_KEY_PARAM macro.

[ ... ]

> @@ -2814,7 +2820,8 @@ gpuva_op_alloc(struct drm_gpuvm *gpuvm)
>  	if (fn && fn->op_alloc)
>  		op = fn->op_alloc();
>  	else
> -		op = kzalloc_obj(*op);
> +		op = folio_scratchpad_alloc_obj(gpuvm, va_scratchpad,
> +						struct drm_gpuva_op, GFP_KERNEL);
>  
>  	if (unlikely(!op))
>  		return NULL;
> @@ -2831,7 +2838,7 @@ gpuva_op_free(struct drm_gpuvm *gpuvm,
>  	if (fn && fn->op_free)
>  		fn->op_free(op);
>  	else
> -		kfree(op);
> +		folio_scratchpad_free_elem(op);
>  }

[Severity: High]
Will this cause an unbounded memory leak for long-lived VMs?

In gpuva_op_alloc(), struct drm_gpuva_op objects are allocated using
gpuvm->va_scratchpad, which acts as a bump allocator. However, the
folio_scratchpad_free_elem() called here in gpuva_op_free() cannot reclaim
individual elements from a scratchpad bump allocator.

Since the memory is only fully reclaimed during bulk teardown in
drm_gpuvm_fini(), and these objects are transiently allocated per map/unmap
operation, could this result in continuous memory leakage over the lifetime
of the GPU VM?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260817-folio-pool-v1-v1-0-0c1d230aa3af@gmail.com?part=4

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH 5/9] bpf/syscall: Route generic_map_update_batch key/value allocations to folio_scratchpad
  2026-08-17 17:22   ` Jim Cromie
  (?)
@ 2026-08-17 17:32   ` sashiko-bot
  -1 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-08-17 17:32 UTC (permalink / raw)
  To: Jim Cromie; +Cc: dri-devel, bpf

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Critical] Batch map updates fail with -ENOMEM for maps with keys or values larger than ~4072 bytes due to order=0 chunk limits.
- [High] Missing folio_scratchpad_free on the key allocation error path causes a permanent kernel memory leak when map->key_size exceeds the chunk capacity.
- [Low] Incompatible pointer type passed to folio_scratchpad_init_key causes a compiler warning.
--

--- Patch [5]: [PATCH 5/9] bpf/syscall: Route generic_map_update_batch key/value allocations to folio_scratchpad ---
commit d64109fbe5280b8aca05d025f4ea39aceb7430e9
Author: Jim Cromie <jim.cromie@gmail.com>

bpf/syscall: Route generic_map_update_batch key/value allocations to folio_scratchpad

Use struct folio_scratchpad to allocate transient key and value buffers
during map update batch operations, bypassing SLUB cache churn.

> diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
> index 6db306d23b479..8ef48673b9339 100644
> --- a/kernel/bpf/syscall.c
> +++ b/kernel/bpf/syscall.c
[ ... ]
> @@ -2083,13 +2087,17 @@ int generic_map_update_batch(struct bpf_map *map, struct file *map_file,
>  	if (put_user(0, &uattr->batch.count))
>  		return -EFAULT;
>  
> -	key = kvmalloc(map->key_size, GFP_USER | __GFP_NOWARN);
> +	folio_scratchpad_init_key(&batch_sp, 0, &bpf_batch_scratchpad_key);

[Severity: Critical]
Does this restrict the size of map elements, causing failures for large
elements?

By passing 0 for the chunk order in folio_scratchpad_init_key(), the
allocation limit is now restricted to a single page. Before this change, the
code used kvmalloc() which supported much larger sizes.

Since folio_scratchpad_alloc() returns NULL if the requested size exceeds the
remaining space in a single page:

lib/folio_pool.c:folio_scratchpad_alloc() {
    ...
    if (sp->remaining < aligned_size) {
        spin_unlock_irqrestore(&sp->lock, flags);
        return NULL;
    }
}

Will this cause legitimate bpf batch map updates with large keys or values to
unconditionally fail with -ENOMEM?

[Severity: Low]
Are we passing an incompatible pointer type here?

The variable bpf_batch_scratchpad_key is defined as struct static_key_true
via DEFINE_STATIC_KEY_TRUE, but folio_scratchpad_init_key() expects a
struct static_key *. Passing it directly might trigger strict aliasing
warnings and break builds.

> +
> +	key = folio_scratchpad_alloc(&batch_sp, map->key_size,
> +				     sizeof(void *), GFP_USER | __GFP_NOWARN);
>  	if (!key)
>  		return -ENOMEM;

[Severity: High]
Could this lead to a memory leak if the key allocation fails?

If map->key_size exceeds the capacity of the order-0 chunk,
folio_scratchpad_alloc() allocates a new folio and adds it to the chunks
list, but still returns NULL because the chunk is too small:

lib/folio_pool.c:folio_scratchpad_alloc() {
    ...
    list_add(&chunk->link, &sp->chunks);
    ...
    if (sp->remaining < aligned_size) {
        spin_unlock_irqrestore(&sp->lock, flags);
        return NULL;
    }
}

Returning -ENOMEM here in generic_map_update_batch() without calling
folio_scratchpad_free() leaves the newly allocated folio on the
sp->chunks list, leaking the memory.

>  
> -	value = kvmalloc(value_size, GFP_USER | __GFP_NOWARN);
> +	value = folio_scratchpad_alloc(&batch_sp, value_size,
> +				       sizeof(void *), GFP_USER | __GFP_NOWARN);
>  	if (!value) {
> -		kvfree(key);
> +		folio_scratchpad_free(&batch_sp);
>  		return -ENOMEM;
>  	}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260817-folio-pool-v1-v1-0-0c1d230aa3af@gmail.com?part=5

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH 1/9] lib/folio_pool: Introduce Direct-Map Large Folio Pool & Scratchpad bump allocators
  2026-08-17 17:22   ` Jim Cromie
  (?)
@ 2026-08-17 17:33   ` sashiko-bot
  -1 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-08-17 17:33 UTC (permalink / raw)
  To: Jim Cromie; +Cc: dri-devel, bpf

Thank you for your contribution! Sashiko AI review found 12 potential issue(s) to consider:
- [Critical] Passing ZERO_SIZE_PTR to is_folio_pool_ptr() triggers a kernel Oops.
- [Critical] Integer overflow in aligned_size calculation bypasses bounds checks and leads to out-of-bounds memset().
- [High] is_folio_pool_ptr() incorrectly identifies large kmalloc() allocations as pool pointers, causing memory leaks.
- [High] folio_pool_realloc() allocates unmanaged kmalloc objects that escape O(1) bulk teardown, leaking memory.
- [High] DEFINE_FREE wrapper for folio_scratchpad fails to guard against ERR_PTR, risking kernel panics.
- [High] Fallback kvzalloc() allocations are not tracked in the scratchpad, breaking bulk teardown and leaking memory.
- [High] Integer underflow when the requested alignment exceeds the fallback folio size causes an out-of-bounds memory write.
- [High] Calling krealloc() on vmalloc pointers triggers a kernel panic.
- [Medium] folio_scratchpad_reset() retains the newest (potentially smallest) chunk instead of the primary large chunk.
- [Medium] Requesting an allocation larger than a folio's capacity permanently leaks a new folio on every attempt.
- [Medium] TOCTOU race condition in folio_scratchpad_alloc() blindly overwrites pool state, abandoning free space.
- [Low] folio_scratchpad_stats() calculates incorrect telemetry metrics when the allocator falls back to order-0 folios.
--

--- Patch [1]: [PATCH 1/9] lib/folio_pool: Introduce Direct-Map Large Folio Pool & Scratchpad bump allocators ---
commit affebf70d4f5ab8fbcb579b4c9e3058f34859616
Author: Jim Cromie <jim.cromie@gmail.com>
lib/folio_pool: Introduce Direct-Map Large Folio Pool & Scratchpad bump allocators

This commit introduces region-based linear bump allocators backed by
direct-map large folios for kernel subsystems. It provides a core engine
(folio_scratchpad) and a wrapper (folio_pool), with static key controls
for subsystem autonomy.

> diff --git a/include/linux/folio_pool.h b/include/linux/folio_pool.h
> new file mode 100644
> index 0000000000000..5bbca8818877c
> --- /dev/null
> +++ b/include/linux/folio_pool.h

[ ... ]

> +void folio_scratchpad_stats(struct folio_scratchpad *sp, unsigned int *nr_chunks,
> +			    size_t *chunk_size, size_t *tail_used);
> +
> +DEFINE_FREE(folio_scratchpad, struct folio_scratchpad *, if (_T) folio_scratchpad_free(_T))

[Severity: High]
Does this wrapper properly handle ERR_PTR returns? If an allocator returns
an ERR_PTR, it evaluates as truthy in the if (_T) check. This would pass the
bogus pointer to folio_scratchpad_free(), which might dereference it while
acquiring the spinlock and cause a kernel crash.

> +
> +/**
> + * is_folio_pool_ptr - Check whether an address resides in a folio pool/scratchpad
> + * @ptr: Object pointer to test
> + *
> + * Direct-map large folios allocated via folio_alloc() are not slab pages,
> + * unlike objects returned by kmalloc/kzalloc.
> + */
> +static inline bool is_folio_pool_ptr(const void *ptr)
> +{
> +	return ptr && !is_vmalloc_addr(ptr) && !folio_test_slab(virt_to_folio(ptr));

[Severity: Critical]
Can this panic if ptr is ZERO_SIZE_PTR? If a caller uses kvzalloc(0), it
returns a ZERO_SIZE_PTR (typically (void *)16). This is truthy and not a
vmalloc address, so it seems virt_to_folio((void *)16) would be called, which
could index out of bounds in vmemmap and trigger an unhandled page fault.

[Severity: High]
Will this misidentify large kmalloc() allocations? Since kmalloc_large()
allocations bypass the SLUB allocator and are served directly by the page
allocator, they do not have the PG_slab flag set. It looks like they would be
misidentified as folio pool pointers, causing kvfree() to be skipped in
folio_pool_free_elem() and leading to a memory leak.

> +}
> +
> +/**
> + * folio_pool_free_elem - Safely release a pool object or SLUB fallback element

[ ... ]

> + * in a direct-map large folio, allocates a fresh @new_size buffer from SLUB
> + * and copies @old_size bytes; the original scratchpad slot remains abandoned
> + * until the entire scratchpad is released or reset at batch boundary.
> + */
> +static inline void *folio_pool_realloc(void *ptr, size_t old_size,
> +				       size_t new_size, gfp_t gfp)
> +{
> +	void *new_ptr;
> +
> +	if (!ptr)
> +		return kmalloc(new_size, gfp);
> +
> +	if (!is_folio_pool_ptr(ptr))
> +		return krealloc(ptr, new_size, gfp);

[Severity: High]
Can this trigger a kernel panic by passing a vmalloc pointer to krealloc()?
If the fallback allocation was performed by kvzalloc(), it may return a
vmalloc address. If passed here, is_folio_pool_ptr() would return false, and
krealloc() would be called on a vmalloc pointer, which could cause a panic.

> +
> +	new_ptr = kmalloc(new_size, gfp);

[Severity: High]
Does this allocate unmanaged kmalloc objects that escape bulk teardown?
The newly allocated buffer does not appear to be added to the scratchpad's
chunks list. Since it is invisible to folio_scratchpad_free(), subsystems
using bulk teardown might permanently leak these reallocated SLUB objects.

> +	if (new_ptr)
> +		memcpy(new_ptr, ptr, min(old_size, new_size));
> +	return new_ptr;
> +}

[ ... ]

> diff --git a/lib/folio_pool.c b/lib/folio_pool.c
> new file mode 100644
> index 0000000000000..07a290db6c506
> --- /dev/null
> +++ b/lib/folio_pool.c

[ ... ]

> +noinline void *folio_scratchpad_alloc(struct folio_scratchpad *sp, size_t size,
> +				     size_t align, gfp_t gfp)
> +{
> +	struct folio_pool_chunk *chunk;
> +	struct folio *folio;
> +	void *elem, *base;
> +	size_t chunk_size, aligned_size, pad, header_offset;
> +	unsigned long flags;
> +
> +	if (!folio_scratchpad_is_enabled(sp))
> +		return kvzalloc(size, gfp);

[Severity: High]
Are these fallback allocations tracked for bulk teardown? It looks like they
are never appended to sp->chunks. When folio_scratchpad_free() or
folio_scratchpad_reset() executes, it seems they only iterate over sp->chunks,
meaning all fallback allocations would be permanently leaked.

> +
> +	if (unlikely(!size))
> +		return NULL;
> +
> +	align = max_t(size_t, sizeof(void *), align ? align : sizeof(void *));
> +
> +	spin_lock_irqsave(&sp->lock, flags);
> +	pad = (uintptr_t)sp->free_ptr & (align - 1);
> +	if (pad)
> +		pad = align - pad;
> +	aligned_size = size + pad;

[Severity: Critical]
Is it possible for aligned_size to overflow? If size is very large (near
SIZE_MAX), aligned_size could wrap around to a small integer. The capacity
check below might then incorrectly succeed, leading to an out-of-bounds
memset(elem, 0, size) with the original massive size.

> +
> +	if (sp->remaining < aligned_size) {
> +		spin_unlock_irqrestore(&sp->lock, flags);
> +
> +		folio = folio_alloc(gfp, sp->chunk_order);
> +		if (!folio && sp->chunk_order > 0)
> +			folio = folio_alloc(gfp, 0);
> +		if (!folio)
> +			return NULL;
> +
> +		base = folio_address(folio);
> +		chunk = (struct folio_pool_chunk *)base;
> +		chunk->folio = folio;
> +		chunk_size = folio_size(folio);
> +		header_offset = ALIGN(sizeof(*chunk), max_t(size_t, sizeof(void *), align));
> +
> +		spin_lock_irqsave(&sp->lock, flags);
> +		list_add(&chunk->link, &sp->chunks);
> +		sp->free_ptr = base + header_offset;
> +		sp->remaining = chunk_size - header_offset;

[Severity: Medium]
Could this overwrite pool state unconditionally in a race condition? Since
sp->lock was dropped to call folio_alloc(), concurrent threads could also
allocate folios. Upon re-acquiring the lock, it looks like threads blindly
overwrite sp->free_ptr and sp->remaining without checking if the pool was
already replenished, potentially abandoning free space.

[Severity: High]
Can chunk_size - header_offset underflow? If the user requests an extremely
large alignment (e.g., page-block alignment) on a small fallback order-0
folio, header_offset might exceed chunk_size. This subtraction would
underflow, producing a massive positive integer, and returning an
out-of-bounds pointer.

> +
> +		pad = (uintptr_t)sp->free_ptr & (align - 1);
> +		if (pad)
> +			pad = align - pad;
> +		aligned_size = size + pad;
> +	}
> +
> +	if (sp->remaining < aligned_size) {
> +		spin_unlock_irqrestore(&sp->lock, flags);
> +		return NULL;

[Severity: Medium]
Does this leak the newly allocated folio if the size is too large? The new
folio was just added to sp->chunks. If the requested size is larger than the
entire folio's capacity, this check fails and returns NULL, but the folio
remains in sp->chunks. Repeatedly requesting an oversized allocation could
rapidly leak folios.

> +	}
> +
> +	elem = sp->free_ptr + pad;
> +	sp->free_ptr += aligned_size;
> +	sp->remaining -= aligned_size;
> +	spin_unlock_irqrestore(&sp->lock, flags);
> +
> +	memset(elem, 0, size);
> +	return elem;
> +}
> +EXPORT_SYMBOL_GPL(folio_scratchpad_alloc);
> +
> +noinline void folio_scratchpad_reset(struct folio_scratchpad *sp)
> +{
> +	struct folio_pool_chunk *head, *chunk, *tmp;
> +	size_t header_offset;
> +	unsigned long flags;
> +
> +	spin_lock_irqsave(&sp->lock, flags);
> +	if (list_empty(&sp->chunks)) {
> +		sp->free_ptr = NULL;
> +		sp->remaining = 0;
> +		spin_unlock_irqrestore(&sp->lock, flags);
> +		return;
> +	}
> +
> +	/* Retain primary head chunk; free overflow chunks */
> +	head = list_first_entry(&sp->chunks, struct folio_pool_chunk, link);

[Severity: Medium]
Does list_first_entry() retain the newest chunk instead of the primary large
chunk? Since list_add() is used during allocation, new chunks are prepended
in a LIFO order. If the final chunk was a small fallback order-0 chunk, it
seems this would free the large capacity chunk and retain the tiny chunk for
the next batch.

> +	list_for_each_entry_safe(chunk, tmp, &sp->chunks, link) {
> +		if (chunk == head)
> +			continue;
> +		list_del(&chunk->link);
> +		folio_put(chunk->folio);
> +	}

[ ... ]

> +void folio_scratchpad_stats(struct folio_scratchpad *sp, unsigned int *nr_chunks,
> +			    size_t *chunk_size, size_t *tail_used)
> +{
> +	unsigned long flags;
> +	size_t csz;
> +
> +	csz = (PAGE_SIZE << sp->chunk_order);
> +	if (chunk_size)
> +		*chunk_size = csz;
> +
> +	spin_lock_irqsave(&sp->lock, flags);
> +	if (nr_chunks)
> +		*nr_chunks = list_count_nodes(&sp->chunks);
> +	if (tail_used)
> +		*tail_used = csz > sp->remaining ? csz - sp->remaining : 0;

[Severity: Low]
Does this calculate incorrect telemetry when falling back to order-0 folios?
If the allocator falls back to an order-0 folio, sp->remaining is relative to
PAGE_SIZE. Subtracting the smaller order-0 remainder from the theoretical
high-order total (csz) seems to compute an artificially inflated tail_used
metric.

> +	spin_unlock_irqrestore(&sp->lock, flags);
> +}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260817-folio-pool-v1-v1-0-0c1d230aa3af@gmail.com?part=1

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH 3/9] bpf/verifier: Route verifier stack state node allocations to folio_pool
  2026-08-17 17:22   ` Jim Cromie
  (?)
@ 2026-08-17 17:35   ` sashiko-bot
  -1 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-08-17 17:35 UTC (permalink / raw)
  To: Jim Cromie; +Cc: dri-devel, bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Unbounded bump-allocation of `bpf_verifier_stack_elem` causes severe transient memory bloat during BPF verification.
--

--- Patch [3]: [PATCH 3/9] bpf/verifier: Route verifier stack state node allocations to folio_pool ---
commit 1f1ee699bf3c9e5a960869e7d6dbd7c57b050ee5
Author: Jim Cromie <jim.cromie@gmail.com>

bpf/verifier: Route verifier stack state node allocations to folio_pool

This commit embeds a folio_pool inside bpf_verifier_env to allocate transient
bpf_verifier_stack_elem frames, releasing them in bulk at the end of
bpf_check().

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -1725,7 +1725,7 @@ static int pop_stack(struct bpf_verifier_env *env, int *prev_insn_idx,
>  		*prev_insn_idx = head->prev_insn_idx;
>  	elem = head->next;
>  	bpf_free_verifier_state(&head->st, false);
> -	kfree(head);
> +	folio_pool_free_elem(head);

[Severity: High]
Does this cause unbounded memory growth during verification?

Looking at include/linux/folio_pool.h, folio_pool_free_elem() appears to be a
no-op for pointers allocated from the pool:

static inline void folio_pool_free_elem(const void *ptr)
{
	if (!ptr || is_folio_pool_ptr(ptr))
		return;
	kvfree(ptr);
}

Since elements popped from the verifier stack are not actually freed, won't
the memory consumption scale with the total number of branches explored
rather than the maximum depth of the verification stack?

>  	env->head = elem;
>  	env->stack_size--;
>  	return 0;

[ ... ]

> @@ -1751,7 +1753,9 @@ static struct bpf_verifier_state *push_stack(struct bpf_verifier_env *env,
>  	struct bpf_verifier_stack_elem *elem;
>  	int err;
>  
> -	elem = kzalloc_obj(struct bpf_verifier_stack_elem, GFP_KERNEL_ACCOUNT);
> +	elem = folio_pool_alloc_obj(env, state_pool,
> +				    struct bpf_verifier_stack_elem,
> +				    GFP_KERNEL_ACCOUNT);

[Severity: High]
With every explored branch allocating a new stack element here, and
pop_stack() abandoning the memory, can this transient memory bloat lead to
memory exhaustion and potential OOMs when verifying complex BPF programs
with many branches?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260817-folio-pool-v1-v1-0-0c1d230aa3af@gmail.com?part=3

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH 2/9] netfilter/nf_tables: Add folio_scratchpad collector to struct nftables_pernet
  2026-08-17 17:22   ` Jim Cromie
  (?)
@ 2026-08-17 17:35   ` sashiko-bot
  -1 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-08-17 17:35 UTC (permalink / raw)
  To: Jim Cromie; +Cc: bpf, dri-devel

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Critical] Asynchronous reset and synchronous abort paths cause severe use-after-free, memory corruption, and spinlock corruption due to sharing a single per-net scratchpad across overlapping batch lifecycles.
- [High] Transaction batches that consist exclusively of creation operations permanently leak the memory used for their `nft_trans` objects.
- [Medium] Re-initialization of the scratchpad during an abort drops the feature-toggle static key and hardcodes the allocation order, causing memory over-allocation on some architectures.
--

--- Patch [2]: [PATCH 2/9] netfilter/nf_tables: Add folio_scratchpad collector to struct nftables_pernet ---
commit 210be9d0ffcaa1541a9b10d23109de51629b6996
Author: Jim Cromie <jim.cromie@gmail.com>

netfilter/nf_tables: Add folio_scratchpad collector to struct nftables_pernet

This commit introduces a folio_scratchpad to struct nftables_pernet to handle
variable-sized transaction object allocations. It aims to improve allocation
and teardown speed by replacing standard SLUB allocations with bulk teardown.

> --- a/net/netfilter/nf_tables_api.c
> +++ b/net/netfilter/nf_tables_api.c
> [ ... ]
> @@ -221,7 +238,7 @@ static void nft_trans_list_del(struct nft_trans *trans)
>  static void nft_trans_destroy(struct nft_trans *trans)
>  {
>         nft_trans_list_del(trans);
> -       kfree(trans);
> +       nft_trans_free(trans);
>  }

[Severity: High]
During commit for creation operations, nft_trans_destroy() unlinks the
transaction and calls nft_trans_free(), which is a no-op for scratchpad
chunks.

By the time the code reaches nf_tables_commit_release(), the commit_list is
completely empty.

This causes nf_tables_commit_release() to return early and skip scheduling
the destroy worker. Since the asynchronous worker never runs,
folio_scratchpad_reset() is never called.

Can batches consisting only of creation operations permanently leak memory?

> [ ... ]
> @@ -10267,6 +10286,8 @@ static void nf_tables_trans_destroy_work(struct work_struct *w)
>                 nft_trans_list_del(trans);
>                 nft_commit_release(trans);
>         }
> +
> +       folio_scratchpad_reset(&nft_net->trans_scratchpad);
>  }

[Severity: Critical]
When nf_tables_commit_release() schedules nf_tables_trans_destroy_work()
and drops the commit_mutex, a new batch can immediately begin allocating
transactions from this exact same scratchpad.

Meanwhile, the asynchronous worker for the previous batch will unconditionally
call folio_scratchpad_reset() here.

This rewinds the allocator's free pointer and destroys the memory actively
being used by the new batch.

Does this shared per-net scratchpad suffer from a race condition between
concurrent batches?

> [ ... ]
> @@ -11491,6 +11514,9 @@ static int nf_tables_abort(struct net *net, struct sk_buff *skb,
>         else
>                 nf_tables_module_autoload_cleanup(net);
>  
> +       folio_scratchpad_free(&nft_net->trans_scratchpad);
> +       folio_scratchpad_init(&nft_net->trans_scratchpad, 4);
> +
>         mutex_unlock(&nft_net->commit_mutex);

[Severity: Critical]
If the first batch's async worker is currently accessing the folios or
waiting on the internal scratchpad spinlock, calling
folio_scratchpad_free() and folio_scratchpad_init() here will free the
underlying folios and re-initialize the spinlock.

Does this synchronous abort corrupt the allocator state if the previous
batch's asynchronous worker is still running?

[Severity: Medium]
In nf_tables_init_net(), the scratchpad is initialized properly using
folio_scratchpad_init_key() with get_order(SZ_64K) and the
nft_trans_scratchpad_key.

Passing the literal 4 here instead of get_order(SZ_64K) forces the
allocator to request 1MB pages on systems with 64KB pages.

Additionally, folio_scratchpad_init() sets sp->key = NULL, which
permanently strips the feature toggle validation from all subsequent
allocations in this network namespace.

Does this re-initialization hardcode the allocation order and drop the
feature toggle?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260817-folio-pool-v1-v1-0-0c1d230aa3af@gmail.com?part=2

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH 6/9] locking/lockdep: Fallback to folio_pool in alloc_list_entry when static pool is full
  2026-08-17 17:22   ` Jim Cromie
  (?)
@ 2026-08-17 17:36   ` sashiko-bot
  -1 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-08-17 17:36 UTC (permalink / raw)
  To: Jim Cromie; +Cc: bpf, dri-devel

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Atomic context violation and RT-deadlock in lockdep dynamic allocation.
- [High] Use-After-Free and memory leak due to untracked dynamic lock dependencies.
- [Low] Undefined behavior in pointer arithmetic for dynamically allocated lock entries.
--

--- Patch [6]: [PATCH 6/9] locking/lockdep: Fallback to folio_pool in alloc_list_entry when static pool is full ---
commit 618b3b19dc7c789afe3ea0f17bf4c674f06ef1f3
Author: Jim Cromie <jim.cromie@gmail.com>

locking/lockdep: Fallback to folio_pool in alloc_list_entry when static pool is full

Use struct folio_pool to dynamically expand lock dependency storage when
the static list_entries pool is exhausted, avoiding premature lockdep
disabling.

> diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
> index 2d4c5bab5af88..f0f58db090fff 100644
> --- a/kernel/locking/lockdep.c
> +++ b/kernel/locking/lockdep.c
[ ... ]
> @@ -1404,11 +1429,19 @@ static struct lock_list *alloc_list_entry(void)
>  				      ARRAY_SIZE(list_entries));
>  
>  	if (idx >= ARRAY_SIZE(list_entries)) {
> +		struct lock_list *p;
> +
> +		p = folio_pool_alloc_type(&lockdep_pool, struct lock_list,
> +					  GFP_ATOMIC);

[Severity: High]
Does dynamically allocating via folio_pool while holding a raw spinlock
violate atomic context rules?

alloc_list_entry() is called by add_lock_to_list() during lockdep graph updates,
which always run under the raw lockdep_lock (via graph_lock()).

Even with GFP_ATOMIC, folio_pool_alloc_type() acquires a standard spinlock_t
(sp->lock) and can call into the page allocator. On PREEMPT_RT, acquiring a
sleepable spinlock_t or calling the page allocator from within a raw spinlock
section causes a kernel panic (sleeping in atomic context). On non-RT kernels,
calling the allocator inside lockdep's graph lock violates its strict
non-reentrancy rules, which risks deadlocks if the allocator triggers warnings
or console output.

> +		if (p) {
> +			nr_list_entries++;
> +			return p;
> +		}

[Severity: High]
Could these dynamically allocated lock entries cause a use-after-free and
memory leak when modules are unloaded?

Dynamically allocated dependencies are not tracked in the list_entries_in_use
bitmap.

When a module is unloaded, zap_class() unlinks dependencies:

kernel/locking/lockdep.c:zap_class() {
    ...
	for_each_set_bit(i, list_entries_in_use, ARRAY_SIZE(list_entries)) {
		entry = list_entries + i;
		if (entry->class != class && entry->links_to != class)
			continue;
		__clear_bit(i, list_entries_in_use);
		nr_list_entries--;
		list_del_rcu(&entry->entry);
	}
    ...
}

Because dynamic entries aren't in this bitmap, zap_class() will skip them
and fail to unlink them from class->locks_after and class->locks_before.
When the module's memory is freed, lockdep retains dangling pointers to
the module's lock structures. Furthermore, the folio_pool bump allocator
does not support freeing individual elements, making the leaked
dependencies permanently unrecoverable.

[Severity: Low]
Could this result in undefined behavior when logging lock errors?

When lockdep detects a structural mismatch, it prints an error using pointer
subtraction:

kernel/locking/lockdep.c:class_lock_list_valid() {
    ...
	list_for_each_entry(e, h, entry) {
		if (e->links_to != c) {
			printk(KERN_INFO "class %s: mismatch for lock entry %ld; class %s <> %s",
			       c->name ? : "(?)",
			       (unsigned long)(e - list_entries),
    ...
}

If the entry e was dynamically allocated via folio_pool, it does not reside
within the static list_entries array. Subtracting pointers from different
memory objects is undefined behavior and produces a nonsensical array
index in the log.

>  		if (!debug_locks_off_graph_unlock())
>  			return NULL;
>  
>  		nbcon_cpu_emergency_enter();
> -		print_lockdep_off("BUG: MAX_LOCKDEP_ENTRIES too low!");
> +		print_lockdep_off("BUG: MAX_LOCKDEP_ENTRIES too low and folio_pool exhausted!");
>  		dump_stack();
>  		nbcon_cpu_emergency_exit();
>  		return NULL;

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260817-folio-pool-v1-v1-0-0c1d230aa3af@gmail.com?part=6

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH 7/9] locking/lockdep: Traverse adjacency lists directly in zap_class()
  2026-08-17 17:22   ` Jim Cromie
  (?)
@ 2026-08-17 17:39   ` sashiko-bot
  -1 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-08-17 17:39 UTC (permalink / raw)
  To: Jim Cromie; +Cc: dri-devel, bpf

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:

New issues:
- [High] zap_class() incorrectly searches the zapped class's own dependency lists instead of the connected classes' lists, failing to remove remote back-edges and causing Use-After-Free.
- [High] Out-of-bounds memory write in zap_class() due to unsafe pointer arithmetic on dynamically allocated lock_list entries.

Pre-existing issues:
- [High] lock_list entries are synchronously returned to the allocator before an RCU grace period expires, exposing lockless RCU readers to torn data (RCU-001 violation).
--

--- Patch [7]: [PATCH 7/9] locking/lockdep: Traverse adjacency lists directly in zap_class() ---
commit 71cf1d3ff4ec0c990dff428fe396cff0df9ba08d
Author: Jim Cromie <jim.cromie@gmail.com>

locking/lockdep: Traverse adjacency lists directly in zap_class()

Rework zap_class() to traverse class->locks_after and class->locks_before
directly, unlinking each edge and its matching counterpart on the
connected lock class via RCU while clearing the respective allocation
slot bits.

> diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
> index f0f58db090fff..c8975c9282bb9 100644
> --- a/kernel/locking/lockdep.c
> +++ b/kernel/locking/lockdep.c
> @@ -6285,11 +6284,29 @@ static void zap_class(struct pending_free *pf, struct lock_class *class)
>  	 * Remove all dependencies this lock is
>  	 * involved in:
>  	 */
> -	for_each_set_bit(i, list_entries_in_use, ARRAY_SIZE(list_entries)) {
> -		entry = list_entries + i;
> -		if (entry->class != class && entry->links_to != class)
> -			continue;
> -		__clear_bit(i, list_entries_in_use);
> +	list_for_each_entry_safe(entry, tmp, &class->locks_after, entry) {
> +		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_before, entry) {
> +			if (other->links_to == class) {

[Severity: High]
Does this code iterate the correct list to find the remote edge?

If entry->links_to points to the zapped class itself, iterating over
&entry->links_to->locks_before traverses the zapped class's own incoming
edges rather than the remote class's edges. Furthermore, the condition
(other->links_to == class) trivially matches elements in its own list.

Does this regression cause the actual reciprocal edges in the remote
classes to be skipped, leaving dangling pointers to the freed class that
could trigger a use-after-free during subsequent lockdep graph traversals?

> +				__clear_bit(other - list_entries, list_entries_in_use);

[Severity: High]
Could this pointer arithmetic regression cause out-of-bounds memory writes?

Lockdep supports allocating lock_list entries dynamically via a folio pool
when the static list_entries array is exhausted. By calculating bit indices
with (other - list_entries), this produces an out-of-bounds offset for
dynamically allocated entries, causing __clear_bit() to overwrite arbitrary
kernel memory.

Also, does this regression leak the dynamically allocated lock_list entries
since they are never freed back to their pool?

> +				nr_list_entries--;
> +				list_del_rcu(&other->entry);

[Severity: High]
This is a pre-existing issue, but does synchronously clearing the allocation
bit before an RCU grace period expose lockless RCU readers to torn data?

zap_class() unlinks dependency edges using list_del_rcu() but immediately
marks them as available by clearing their bit in the list_entries_in_use
bitmap above. This allows concurrent calls to alloc_list_entry() to
reallocate the struct lock_list block and insert it into a different list,
modifying its next pointer.

Could lockless readers traversing the original list observe the modified
next pointer and be steered into unrelated lists or infinite loops?

> +				break;
> +			}
> +		}
> +		__clear_bit(entry - list_entries, list_entries_in_use);
> +		nr_list_entries--;
> +		list_del_rcu(&entry->entry);
> +	}

[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260817-folio-pool-v1-v1-0-0c1d230aa3af@gmail.com?part=7

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH 9/9] locking/lockdep: Migrate and compact boot-time dependency graph from __initdata
  2026-08-17 17:22   ` Jim Cromie
  (?)
@ 2026-08-17 17:45   ` sashiko-bot
  -1 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-08-17 17:45 UTC (permalink / raw)
  To: Jim Cromie; +Cc: bpf, dri-devel

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] Invalid Wait Context on PREEMPT_RT and potential buddy allocator recursion.
- [High] Use-After-Free triggered by reading /proc/lockdep after boot compaction failure.
- [High] Permanent memory leak of dynamically allocated lock graph edges on module unload.
- [Medium] Integer underflow in global active edge counter `nr_list_entries`.
--

--- Patch [9]: [PATCH 9/9] locking/lockdep: Migrate and compact boot-time dependency graph from __initdata ---
commit f7e00f7b01be3f305f806e2c70e4d05c87137496
Author: Jim Cromie <jim.cromie@gmail.com>

locking/lockdep: Migrate and compact boot-time dependency graph from __initdata

This commit moves early boot lockdep nodes to __initdata and compacts them
into folio_pool allocations during late_initcall to save memory.

> diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
> index 5968a976bf8ea..d9541207b342b 100644
> --- a/kernel/locking/lockdep.c
> +++ b/kernel/locking/lockdep.c

[ ... ]

> @@ -6295,30 +6308,26 @@ static void zap_class(struct pending_free *pf, struct lock_class *class)
>  	list_for_each_entry_safe(entry, tmp, &class->locks_after, entry) {
>  		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_before, entry) {
>  			if (other->links_to == class) {
> -				if (is_bootstrap_entry(other))
> -					__clear_bit(other - list_entries, list_entries_in_use);
> +				clear_bootstrap_entry_bit(other);
>  				nr_list_entries--;
>  				list_del_rcu(&other->entry);

[Severity: High]
Does this leak the dynamically allocated edges?

When lock classes are dynamically destroyed during module unload, zap_class()
unlinks their graph edges using list_del_rcu(). With folio_pool allocations,
there is no explicit kfree() or folio_pool_free_obj() called. Discarding the
pointers leaves the memory stranded, leading to a permanent memory leak until
the bump allocator is exhausted.

[ ... ]

> @@ -6733,12 +6745,101 @@ void __init lockdep_init(void)
>  static int __init lockdep_boot_report(void)

[ ... ]

> +static int __init lockdep_compact_boot_graph(void)
> +{
> +	struct lock_class *class;
> +	struct lock_list *entry, *tmp, *new_entry;
> +	unsigned long flags;
> +	unsigned long migrated = 0;

[ ... ]

> +	raw_local_irq_save(flags);
> +	if (!graph_lock()) {
> +		raw_local_irq_restore(flags);
> +		return 0;
> +	}
> +
> +	list_for_each_entry(class, &all_lock_classes, lock_entry) {
> +		list_for_each_entry_safe(entry, tmp, &class->locks_after, entry) {
> +			if (is_bootstrap_entry(entry)) {
> +				if (new_entry) {
> +					*new_entry = *entry;
> +					list_replace_rcu(&entry->entry, &new_entry->entry);
> +					new_entry = NULL;
> +				} else {
> +					struct lock_list *slot;
> +
> +					slot = folio_pool_alloc_type(&lockdep_pool,
> +								     struct lock_list,
> +								     GFP_ATOMIC);

[Severity: High]
Can this cause an invalid wait context panic on PREEMPT_RT?

The folio_pool_alloc_type() function is called here while holding
graph_lock(). Because graph_lock() acquires a raw spinlock with local IRQs
disabled, it cannot safely call into folio_pool_alloc_type().

Internally, folio_pool_alloc_type() acquires a standard spinlock_t, which
maps to a sleepable rt_mutex on PREEMPT_RT. Acquiring a sleeping lock inside
a raw spinlock context violates the wait context rules.

Additionally, if the pre-allocated chunk is exhausted, this could call
folio_alloc(), which takes sleepable MM zone locks and triggers the exact
recursion the pre-allocation was intended to avoid.

> +					if (!slot) {
> +						debug_locks_off_graph_unlock();
> +						raw_local_irq_restore(flags);
> +						pr_err("lockdep: folio chunk exhausted during boot compaction\n");
> +						return -ENOMEM;

[Severity: High]
Could this lead to a use-after-free when reading /proc/lockdep?

If folio_pool_alloc_type() fails, this error path disables lockdep and
aborts. However, the partially migrated dependency graph still contains
pointers to early_list_entries, which resides in the __initdata section.

When free_initmem() later reclaims the __initdata section, these pointers
become dangling.

If /proc/lockdep is read, l_show() unconditionally traverses the
class->locks_after list without checking if debug_locks is enabled, which
would dereference the freed init memory.

[ ... ]

> +		}
> +	}
> +
> +	/* Adjust counter so compaction does not double-count migrated nodes */
> +	nr_list_entries -= migrated;

[Severity: Medium]
Is this subtraction correct?

The compaction loop replaces boot nodes with new allocations directly from
folio_pool without incrementing nr_list_entries. Subtracting migrated from
nr_list_entries here reduces the counter even though the active edge count
remains unchanged.

When these dynamically allocated lock classes are later freed during module
unload, zap_class() unconditionally decrements nr_list_entries--, which will
cause the global counter to underflow.

> +
> +	bootstrap_entries = NULL;

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260817-folio-pool-v1-v1-0-0c1d230aa3af@gmail.com?part=9

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH 8/9] locking/lockdep: Shrink static list_entries array to early bootstrap buffer
  2026-08-17 17:22   ` Jim Cromie
  (?)
@ 2026-08-17 17:52   ` sashiko-bot
  -1 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-08-17 17:52 UTC (permalink / raw)
  To: Jim Cromie; +Cc: bpf, dri-devel

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:

New issues:
- [High] Dynamically allocated lock_list entries from lockdep_pool are permanently leaked when lock classes are zapped.
- [High] Allocating from folio_pool under graph_lock violates lock nesting rules and will deadlock on PREEMPT_RT.
- [High] `nf_tables_abort()` resets the scratchpad incorrectly, breaking subsystem toggles and hardcoding chunk sizes.
- [High] Transient operations are leaked on every map/unmap due to lack of scratchpad reset in DRM GPUVM.
- [High] BPF `generic_map_update_batch()` unconditionally fails with `-ENOMEM` for maps with large elements.

Pre-existing issues:
- [High] Synchronous clearing of list_entries_in_use allows reallocation of bootstrap entries before an RCU grace period elapses, causing UAF for lockless readers.
--

--- Patch [8]: [PATCH 8/9] locking/lockdep: Shrink static list_entries array to early bootstrap buffer ---
commit 50e28e082c0e7079e9acf36ed3d4b7ea1bf5fae3
Author: Jim Cromie <jim.cromie@gmail.com>

locking/lockdep: Shrink static list_entries array to early bootstrap buffer

Shrink the static compile-time list_entries[] array and its tracking
bitmap from MAX_LOCKDEP_ENTRIES (32,768 entries, ~1.31 MB BSS) down to
a 4096-entry early boot bootstrap buffer (~160 KB BSS).

[ ... ]

> diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
> index c8975c9282bb9..5968a976bf8ea 100644
> --- a/kernel/locking/lockdep.c
> +++ b/kernel/locking/lockdep.c

[ ... ]

> @@ -6287,26 +6295,30 @@ static void zap_class(struct pending_free *pf, struct lock_class *class)
>  	list_for_each_entry_safe(entry, tmp, &class->locks_after, entry) {
>  		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_before, entry) {
>  			if (other->links_to == class) {
> -				__clear_bit(other - list_entries, list_entries_in_use);
> +				if (is_bootstrap_entry(other))
> +					__clear_bit(other - list_entries, list_entries_in_use);

[Severity: High]
This is a pre-existing issue, but does synchronous clearing of
list_entries_in_use allow reallocation of bootstrap entries before an
RCU grace period elapses?

When a module is unloaded, zap_class() unlinks the class's lock_list entries
and clears their bootstrap_entries_in_use bit immediately. If
alloc_list_entry() instantly reallocates the bootstrap entry and mutates its
list pointers while lockless RCU readers are actively iterating over it,
can this cause a use-after-free or RCU violation for lockless readers?

>  				nr_list_entries--;
>  				list_del_rcu(&other->entry);

[Severity: High]
Are dynamically allocated lock_list entries from lockdep_pool being
permanently leaked when lock classes are zapped?

Because the underlying folio_pool allocator does not support individual
element deallocation via a simple unlink, entries that are not bootstrap
entries are permanently forgotten after being unlinked.

Since unloading modules registers and unregisters lock classes, could this
eventually lead to a system out-of-memory condition?

>  				break;
>  			}

[ ... ]

[Severity: High]
Does allocating from folio_pool under graph_lock violate lock nesting rules
and deadlock on PREEMPT_RT?

As mentioned in the commit message, alloc_list_entry() spills subsequent
allocations to the lockdep_pool. When called with graph_lock() held (which
acquires an arch_spinlock_t and disables interrupts), it invokes the
folio_pool allocator:

kernel/locking/lockdep.c:alloc_list_entry() {
    ...
	p = folio_pool_alloc_type(&lockdep_pool, struct lock_list, GFP_ATOMIC);
    ...
}

lib/folio_pool.c:folio_scratchpad_alloc() {
    ...
	spin_lock_irqsave(&sp->lock, flags);
    ...
}

Because sp->lock is a spinlock_t, which maps to a sleepable rt_mutex on
PREEMPT_RT, attempting to acquire it while holding a raw spinlock causes a
sleep-in-atomic deadlock. Can this cause an invalid wait context deadlock and
lockdep splat?


[Severity: High]
Does nf_tables_abort() reset the scratchpad incorrectly, breaking subsystem
toggles and hardcoding chunk sizes?

When an nf_tables transaction is aborted (e.g., due to an error in a batch
ruleset load), nf_tables_abort() reinitializes the scratchpad:

net/netfilter/nf_tables_api.c:nf_tables_abort() {
    ...
	folio_scratchpad_free(&nft_net->trans_scratchpad);
	folio_scratchpad_init(&nft_net->trans_scratchpad, 4);
    ...
}

On architectures with 64KB pages, order 4 results in 1MB chunks instead of
the intended 64KB chunks, wasting large amounts of memory. Additionally,
passing a NULL key (implicitly through folio_scratchpad_init) permanently
disconnects the scratchpad from the nft_trans_scratchpad_key module parameter.


[Severity: High]
Are transient operations leaked on every map/unmap due to lack of scratchpad
reset in DRM GPUVM?

drm_gpuva_op structures are allocated dynamically via gpuva_op_alloc() using
folio_scratchpad_alloc_obj(). When the operation completes, it is freed:

drivers/gpu/drm/drm_gpuvm.c:gpuva_op_free() {
    ...
	else
		folio_scratchpad_free_elem(op);
}

The caller assumes this frees the operation state, but because
folio_scratchpad_free_elem() is a no-op for bump allocators, and drm_gpuvm
never explicitly calls folio_scratchpad_reset() between operations, the
embedded va_scratchpad will endlessly consume memory.

Could this lead to a rapid out-of-memory condition when continuously mapping
and unmapping GPU memory?


[Severity: High]
Does generic_map_update_batch() unconditionally fail with -ENOMEM for maps
with large elements?

When a user issues a BPF_MAP_UPDATE_BATCH syscall, the batch_sp scratchpad
is initialized with chunk_order = 0:

kernel/bpf/syscall.c:generic_map_update_batch() {
    ...
	folio_scratchpad_init_key(&batch_sp, 0, &bpf_batch_scratchpad_key);

	key = folio_scratchpad_alloc(&batch_sp, map->key_size,
				     sizeof(void *), GFP_USER | __GFP_NOWARN);
    ...
}

Because the stack-local bump allocator cannot satisfy requests larger than the
chunk size (page limit minus header), it will unconditionally return NULL for
keys or values exceeding a single page. Does this deterministically break
backward compatibility for large map elements?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260817-folio-pool-v1-v1-0-0c1d230aa3af@gmail.com?part=8

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH 0/9] lib/folio_pool: Direct-Map Large Folio Pool & Scratchpad bump allocators
  2026-08-17 17:22 ` Jim Cromie
                   ` (9 preceding siblings ...)
  (?)
@ 2026-08-17 18:17 ` David Hildenbrand (Arm)
  -1 siblings, 0 replies; 32+ messages in thread
From: David Hildenbrand (Arm) @ 2026-08-17 18:17 UTC (permalink / raw)
  To: jim.cromie, Andrew Morton, Pablo Neira Ayuso, Florian Westphal,
	Phil Sutter, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, Matthew Wilcox, Zi Yan
  Cc: netfilter-devel, bpf, dri-devel, linux-kernel, linux-mm, coreteam,
	netdev

On 8/17/26 19:22, Jim Cromie via B4 Relay wrote:
> Introduce two light-weight bump allocators backed directly by compound
> folio pages from the buddy allocator (skipping SLUB metadata overhead):

We are running into a bit of a naming problem here: folios are really supposed
to describe things that we will map to user space, whereby we have to track
mapping/index/mapcount etc. That's really just anon + pagecache memory.

Right now that naming difference doesn't matter, because each compound page
really is a folio.

But once we start decoupling folios from the rest (memdescs), we really only
expect anon+pagecache to resemble "folios".

There will be other compound page types (similar to slab pages), and things like
page_folio() will fail for them (like slab pages). And we are actually working
on making page_folio() fail earlier for anything that didn't go through
page_rmappable_folio() as part of the (folio) allocation.

So, does what you do here fit the definition of folios, or is it really just
some kind of compound pages?

-- 
Cheers,

David

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH 0/9] lib/folio_pool: Direct-Map Large Folio Pool & Scratchpad bump allocators
  2026-08-17 17:22 ` Jim Cromie
                   ` (10 preceding siblings ...)
  (?)
@ 2026-08-17 18:34 ` Matthew Wilcox
  -1 siblings, 0 replies; 32+ messages in thread
From: Matthew Wilcox @ 2026-08-17 18:34 UTC (permalink / raw)
  To: jim.cromie
  Cc: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Peter Zijlstra, Ingo Molnar, Will Deacon,
	Waiman Long, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, John Fastabend, Thomas Hellström,
	Alice Ryhl, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, Boqun Feng, netfilter-devel, bpf,
	dri-devel, linux-kernel, linux-mm, coreteam, netdev,
	Byungchul Park

On Mon, Aug 17, 2026 at 11:22:14AM -0600, Jim Cromie via B4 Relay wrote:
> Introduce two light-weight bump allocators backed directly by compound
> folio pages from the buddy allocator (skipping SLUB metadata overhead):
> 
> 1. struct folio_scratchpad - Variable-sized, alignment-aware bump allocator
>    for bursty, append-mostly lifecycles with bulk teardown.
> 2. struct folio_pool - Fixed uniform-slot bump allocator for homogeneous
>    descriptors.

This is an inappropriate use of folios.  Folios are for memory that's
in the page cache and anonymous user memory, not for general kernel
use.  They would need their own memdesc type.

But Byungchul is already doing that work.  So you should just reuse what
he's done.

> These serve 2 kinds of use-cases:
> 
> 1: Ephemeral Batch/transaction Lifecycles (Netfilter, BPF, DRM GPUVM)
> 
> - Transactional subsystems allocate bursts of hundreds or thousands of
>   homogeneous or heterogeneous objects, only to tear them all down
>   simultaneously at batch completion or error abort.
> 
> - Under SLUB: Each descriptor incurs freelist traversal, lock
>   contention, and O(N) kfree() loops on teardown.
> 
> - Under Folio-Scratchpad: Allocations are straight-line pointer bumps,
>   bulk teardown is O(1) folio_put(), and consecutive Netlink
>   transactions reuse warm L1/L2 cachelines without buddy lock churn
>   via folio_scratchpad_reset().
> 
> 2: Dynamic Long-Lived Graph Scaling (Lockdep)
> 
> - Core subsystems historically avoided SLUB by statically
>   preallocating massive compile-time arrays in .bss
>   (e.g. list_entries[32768], 1.31 MB) to prevent SLUB allocator
>   recursion deadlocks.
> 
> - Fixed arrays exhaust their limit and permanently disable validation
>   (BUG: MAX_LOCKDEP_ENTRIES too low!).
>   
> - Under Folio-Pool: Lockdep uses a 4,096-entry __initdata bootstrap
>   buffer for early boot prior to buddy initialization, then seamlessly
>   migrates and compacts all dependency edges into 64 KB direct-map
>   folios at late_initcall.
>   
> - Permanent static .bss allocation for dependency edges is reduced
>   from 1.31 MB to 0 KB (100% reclamation at free_initmem()).
> 
> Core Primitives in include/linux/folio_pool.h & lib/folio_pool.c:
> -----------------------------------------------------------------
> 0. struct folio_scratchpad (Variable-Sized Elements):
>    - Dynamic alignment-aware bump pointer with zero padding waste.
>    - API: folio_scratchpad_init(), folio_scratchpad_alloc(),
>      folio_scratchpad_reset(), folio_scratchpad_free(),
>      folio_scratchpad_stats().
>    - Typed macros: folio_scratchpad_alloc_obj(),
>      folio_scratchpad_alloc_bytes(), DEFINE_FREE(folio_scratchpad, ...).
> 
> 1. struct folio_pool (Fixed Uniform Elements):
>    - Thin wrapper embedding struct folio_scratchpad for homogeneous objects.
>    - API: folio_pool_init(), folio_pool_alloc(), folio_pool_free(),
>      folio_pool_stats().
>    - Typed macros: folio_pool_alloc_obj(), folio_pool_alloc_type().
> 
> 2. Encapsulated Slab Discrimination & Reallocation:
>    - is_folio_pool_ptr(ptr): Direct-map folios are identified via
>      virt_to_folio(ptr) && !folio_test_slab(f), avoiding custom bitflags.
>    - folio_scratchpad_free_elem(ptr): Safe no-op for folio-backed objects;
>      delegates to kvfree() for SLUB/vmalloc fallback objects.
>    - folio_scratchpad_realloc(ptr, old_size, new_size, gfp): Delegates to
>      krealloc() for SLUB objects; allocates a fresh SLUB buffer and copies
>      payload for folio-backed descriptors.
> 
> 3. Subsystem Autonomy & Runtime Static-Key Control:
>    - DEFINE_FOLIO_POOL_STATIC_KEY_PARAM() allows maintainers of each
>      subsystem (Netfilter, Lockdep, DRM, BPF) to choose their own default
>      enablement policy (DECLARE_STATIC_KEY_TRUE vs FALSE) and independent
>      module/boot parameters (e.g. lockdep.folio_pool, nf_tables.trans_scratchpad).
>    - Enables isolated, zero-overhead A/B benchmarking and production triage
>      without cross-subsystem coupling or kernel recompilation.
> 
> Active Proofs & Performance Profiling:
> --------------------------------------
> 0. Virtualized Guest Netfilter Benchmark (KVM / virtme-ng ftrace):
>    Controlled A/B benchmark across 10,025 Netlink transaction descriptors
>    and 5 asynchronous commit/abort teardown passes:
> 
>    Function: nft_trans_alloc (10,025 descriptor allocations)
>    -----------------------------------------------------------------------------
>    Mode                                 Total Time       Avg / Alloc      Delta
>    -----------------------------------------------------------------------------
>    folio_scratchpad Enabled (Bump)      9,158.02 us          0.914 us    -19.4%
>    SLUB Fallback (kzalloc)             11,366.53 us          1.134 us  Baseline
>    -----------------------------------------------------------------------------
>    Net allocation latency reduction:   -2,208.52 us (-2.21 ms)
> 
>    Function: nf_tables_trans_destroy_work (5 asynchronous teardown passes)
>    -----------------------------------------------------------------------------
>    Mode                                 Total Time       Avg / Pass       Delta
>    -----------------------------------------------------------------------------
>    folio_scratchpad Enabled (Bulk)     36,065.65 us          7.21 ms      -8.6%
>    SLUB Fallback (O(N) kfree)          39,449.65 us          7.89 ms   Baseline
>    -----------------------------------------------------------------------------
>    Net teardown latency reduction:     -3,384.00 us (-3.38 ms)
> 
>    Combined In-Kernel Netlink Transaction Lifecycle Savings: -5.59 ms (-13.3%)
> 
> 1. Bare-Metal Host Netfilter Profile (AMD Zen 3 Hardware PMU & ftrace):
>    - Asynchronous Teardown: 53.77 ms vs 56.03 ms (-2.26 ms / -4.0%).
>    - Instruction Retirement: 281.29M -> 282.46M (-1,174,062 instructions).
>    - Branch Instructions:     62.94M ->  63.20M (-265,961 branches).
> 
> 2. Bare-Metal Host Lockdep Telemetry (Physical Host frodo, AMD Zen SMP):
>    - Pre-Buddy Watermark: 928/4096 bootstrap entries consumed during boot.
>    - Graph Compaction: Migrated and compacted bootstrap dependency edges
>      into folio_pool at late_initcall; bootstrap array reclaimed by
>      free_initmem() (0 KB permanent .bss cost).
>    - Live procfs Telemetry (/proc/lockdep_stats):
>      lock-classes:                          996 [max: 8192]
>      direct dependencies:                  2324 [dynamic: 7 x 64 kB, tail: 17 kB/64 kB]
>      indirect dependencies:               24612
>      all direct dependencies:             81884
>      zapped classes:                          2
>      zapped lock chains:                    173
>    - Active Invariant Validation: Live module unloads exercised modernized
>      zap_class() direct list unlinking across dynamic folios with zero
>      global bitmap scans and zero assertions.
> 
> What's Unchanged:
> -----------------
> - All consumer object structures, alignment rules, and lifecycle boundaries
>   remain strictly identical.
> - Memory safety invariants are preserved: individual deallocation is a safe
>   no-op for arena-backed objects while SLUB objects continue to use kfree().
> 
> Subsystem Adoptions in this Series:
> -----------------------------------
> 0. lib/folio_pool: Core direct-map large-folio pool and scratchpad allocators.
> 1. netfilter/nf_tables: Pack netlink transaction descriptors into per-net
>    trans_scratchpad with bulk reclaim & warm chunk reset.
> 2. bpf/verifier: Route verifier stack state node allocations to folio_pool.
> 3. drm/gpuvm: Route gpuva_op allocations to folio_scratchpad.
> 4. bpf/syscall: Route generic_map_update_batch key/value allocations to
>    folio_scratchpad.
> 5. locking/lockdep: Fallback to folio_pool in alloc_list_entry when static
>    pool is full and expose dynamic folio telemetry in /proc/lockdep_stats.
> 6. locking/lockdep: Traverse adjacency lists directly in zap_class().
> 7. locking/lockdep: Shrink static list_entries array to early bootstrap buffer.
> 8. locking/lockdep: Migrate and compact boot-time dependency graph from __initdata.
> 
> Patches in this series:
> -----------------------
> [PATCH 1/9] lib/folio_pool: Introduce Direct-Map Large Folio Pool & Scratchpad bump allocators
> [PATCH 2/9] netfilter/nf_tables: Add folio_scratchpad collector to struct nftables_pernet
> [PATCH 3/9] bpf/verifier: Route verifier stack state node allocations to folio_pool
> [PATCH 4/9] drm/gpuvm: Route gpuva_op allocations to folio_scratchpad
> [PATCH 5/9] bpf/syscall: Route generic_map_update_batch key/value allocations to folio_scratchpad
> [PATCH 6/9] locking/lockdep: Fallback to folio_pool in alloc_list_entry when static pool is full
> [PATCH 7/9] locking/lockdep: Traverse adjacency lists directly in zap_class()
> [PATCH 8/9] locking/lockdep: Shrink static list_entries array to early bootstrap buffer
> [PATCH 9/9] locking/lockdep: Migrate and compact boot-time dependency graph from __initdata
> 
> Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
> ---
> Jim Cromie (9):
>       lib/folio_pool: Introduce Direct-Map Large Folio Pool & Scratchpad bump allocators
>       netfilter/nf_tables: Add folio_scratchpad collector to struct nftables_pernet
>       bpf/verifier: Route verifier stack state node allocations to folio_pool
>       drm/gpuvm: Route gpuva_op allocations to folio_scratchpad
>       bpf/syscall: Route generic_map_update_batch key/value allocations to folio_scratchpad
>       locking/lockdep: Fallback to folio_pool in alloc_list_entry when static pool is full
>       locking/lockdep: Traverse adjacency lists directly in zap_class()
>       locking/lockdep: Shrink static list_entries array to early bootstrap buffer
>       locking/lockdep: Migrate and compact boot-time dependency graph from __initdata
> 
>  drivers/gpu/drm/drm_gpuvm.c        |  11 +-
>  include/drm/drm_gpuvm.h            |   6 +
>  include/linux/bpf_verifier.h       |   3 +
>  include/linux/folio_pool.h         | 279 +++++++++++++++++++++++++++++++++++++
>  include/net/netfilter/nf_tables.h  |   3 +
>  kernel/bpf/syscall.c               |  17 ++-
>  kernel/bpf/verifier.c              |  16 ++-
>  kernel/locking/lockdep.c           | 278 +++++++++++++++++++++++++++++-------
>  kernel/locking/lockdep_internals.h |   2 +
>  kernel/locking/lockdep_proc.c      |  16 ++-
>  lib/Makefile                       |   2 +-
>  lib/folio_pool.c                   | 230 ++++++++++++++++++++++++++++++
>  net/netfilter/nf_tables_api.c      |  51 +++++--
>  13 files changed, 837 insertions(+), 77 deletions(-)
> ---
> base-commit: 1a7ac48cd3e62f281ce62f8ed89dc2cc8866eaa4
> change-id: 20260815-folio-pool-v1-cb75c247692c
> 
> Best regards,
> -- 
> Jim Cromie <jim.cromie@gmail.com>
> 
> 
> 

^ permalink raw reply	[flat|nested] 32+ messages in thread

* Re: [PATCH 6/9] locking/lockdep: Fallback to folio_pool in alloc_list_entry when static pool is full
  2026-08-17 17:22   ` Jim Cromie
  (?)
  (?)
@ 2026-08-17 21:01   ` Peter Zijlstra
  -1 siblings, 0 replies; 32+ messages in thread
From: Peter Zijlstra @ 2026-08-17 21:01 UTC (permalink / raw)
  To: jim.cromie
  Cc: Andrew Morton, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Danilo Krummrich, Matthew Brost,
	Boris Brezillon, Ingo Molnar, Will Deacon, Waiman Long,
	David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Simon Horman, Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu,
	Yonghong Song, Jiri Olsa, Emil Tsalapatis, John Fastabend,
	Thomas Hellström, Alice Ryhl, Maarten Lankhorst,
	Maxime Ripard, Thomas Zimmermann, David Airlie, Simona Vetter,
	Boqun Feng, netfilter-devel, bpf, dri-devel, linux-kernel,
	linux-mm, coreteam, netdev

On Mon, Aug 17, 2026 at 11:22:20AM -0600, Jim Cromie via B4 Relay wrote:
> From: Jim Cromie <jim.cromie@gmail.com>
> 
> Use struct folio_pool to dynamically expand lock dependency storage when
> the static list_entries pool is exhausted, avoiding premature lockdep
> disabling.

Since that thing has locks itself, no.

NAK.


^ permalink raw reply	[flat|nested] 32+ messages in thread

end of thread, other threads:[~2026-08-17 21:01 UTC | newest]

Thread overview: 32+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-17 17:22 [PATCH 0/9] lib/folio_pool: Direct-Map Large Folio Pool & Scratchpad bump allocators Jim Cromie via B4 Relay
2026-08-17 17:22 ` Jim Cromie
2026-08-17 17:22 ` [PATCH 1/9] lib/folio_pool: Introduce " Jim Cromie via B4 Relay
2026-08-17 17:22   ` Jim Cromie
2026-08-17 17:33   ` sashiko-bot
2026-08-17 17:22 ` [PATCH 2/9] netfilter/nf_tables: Add folio_scratchpad collector to struct nftables_pernet Jim Cromie via B4 Relay
2026-08-17 17:22   ` Jim Cromie
2026-08-17 17:35   ` sashiko-bot
2026-08-17 17:22 ` [PATCH 3/9] bpf/verifier: Route verifier stack state node allocations to folio_pool Jim Cromie via B4 Relay
2026-08-17 17:22   ` Jim Cromie
2026-08-17 17:35   ` sashiko-bot
2026-08-17 17:22 ` [PATCH 4/9] drm/gpuvm: Route gpuva_op allocations to folio_scratchpad Jim Cromie via B4 Relay
2026-08-17 17:22   ` Jim Cromie
2026-08-17 17:31   ` sashiko-bot
2026-08-17 17:22 ` [PATCH 5/9] bpf/syscall: Route generic_map_update_batch key/value " Jim Cromie via B4 Relay
2026-08-17 17:22   ` Jim Cromie
2026-08-17 17:32   ` sashiko-bot
2026-08-17 17:22 ` [PATCH 6/9] locking/lockdep: Fallback to folio_pool in alloc_list_entry when static pool is full Jim Cromie via B4 Relay
2026-08-17 17:22   ` Jim Cromie
2026-08-17 17:36   ` sashiko-bot
2026-08-17 21:01   ` Peter Zijlstra
2026-08-17 17:22 ` [PATCH 7/9] locking/lockdep: Traverse adjacency lists directly in zap_class() Jim Cromie via B4 Relay
2026-08-17 17:22   ` Jim Cromie
2026-08-17 17:39   ` sashiko-bot
2026-08-17 17:22 ` [PATCH 8/9] locking/lockdep: Shrink static list_entries array to early bootstrap buffer Jim Cromie via B4 Relay
2026-08-17 17:22   ` Jim Cromie
2026-08-17 17:52   ` sashiko-bot
2026-08-17 17:22 ` [PATCH 9/9] locking/lockdep: Migrate and compact boot-time dependency graph from __initdata Jim Cromie via B4 Relay
2026-08-17 17:22   ` Jim Cromie
2026-08-17 17:45   ` sashiko-bot
2026-08-17 18:17 ` [PATCH 0/9] lib/folio_pool: Direct-Map Large Folio Pool & Scratchpad bump allocators David Hildenbrand (Arm)
2026-08-17 18:34 ` Matthew Wilcox

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.