Building the Linux kernel with Clang and LLVM
 help / color / mirror / Atom feed
* [PATCH v5 0/4] selftests: add shared Makefile for BPF selftests and a new memcg test
@ 2026-09-10 23:32 Ziyang Men
  2026-09-10 23:33 ` [PATCH v5 1/4] selftests: add shared lib.bpf.mk to build BPF progs and skeletons Ziyang Men
                   ` (3 more replies)
  0 siblings, 4 replies; 8+ messages in thread
From: Ziyang Men @ 2026-09-10 23:32 UTC (permalink / raw)
  To: Shuah Khan, Tejun Heo, Johannes Weiner, Michal Koutný,
	Jiri Kosina, Benjamin Tissoires, David Vernet, Eduard Zingerman
  Cc: Viktor Malik, Andrea Righi, Changwoo Min, Michal Hocko,
	Roman Gushchin, Shakeel Butt, Muchun Song, Andrew Morton,
	JP Kobryn, Mykola Lysenko, Nathan Chancellor, Ziyang Men,
	kernel-team, linux-kselftest, cgroups, linux-input, sched-ext,
	linux-mm, bpf, llvm, linux-kernel

This series refactors the BPF-related build machinery that is currently
duplicated across selftests/{bpf,sched_ext,hid}/. It adds the shared
tools/testing/selftests/lib.bpf.mk fragment and wires up three consumers,
simplifying both existing BPF selftests and future additions.

The series also adds a cgroup memory-controller selftest that complements
the existing coverage.

Patch 1 adds lib.bpf.mk.

Patch 2 adds selftests/cgroup/test_memcg_stat_cross_cpu, which verifies a
cgroup flush by comparing values read through BPF memcg kfuncs with the
corresponding cgroupfs values. The existing bpf/cgroup_iter_memcg test only
checks whether selected values are non-zero.

Patches 3 and 4 replace the duplicated BPF build machinery in
selftests/{hid,sched_ext} with lib.bpf.mk.

===
Changes since v4:
- Apply Tejun's suggestion: make the vmlinux.h stamp a normal
  prerequisite so dependent BPF objects rebuild in the same make
  invocation when the header changes.
- Fix AI bot suggestion: assert in the BPF selftest that the memcg event
  enum argument types remain unsigned. This assert might not be
  necessary since these enum values are always uint32 on BTF.

===
Changes since v3:
For shared Makefile:
- Track in-tree libbpf headers directly so a single make invocation rebuilds
  BPF objects and skeletons after a header change.
- Restore the target definitions needed by LoongArch and MIPS cross builds.
- Validate output directories and skeleton suffixes, and reject dotted or
  hyphenated source stems before they reach build and cleanup rules.
- Add stamp tracking for vmlinux.h generation while preserving content-based
  header updates.
- Make skeletons and subskeletons explicit outputs, so a missing subskeleton is
  regenerated and both sets participate in the all target.
- Namespace private variables, expand the caller-visible contract, track a
  supplied VMLINUX_H, and remove unnecessary include search paths.

For cgroup selftest:
- Add CONFIG_BPF_EVENTS and probe every required kfunc plus the sleepable
  cgroup iterator before running.
- Bound CPU fanout, cgroup/process count, and total memory use; skip a case
  when the flush threshold would require more than 256 MiB.
- Check CPU discovery, polling, iterator creation, and BPF errors, with clearer
  diagnostics.
- Compare each BPF statistic against two following memory.stat samples, keeping
  exact equality on a quiet subtree while tolerating values that move between
  the two samples.

For hid and sched_ext:
- Remove unused HID locals instead of suppressing warnings, drop obsolete
  variables and comments, tighten cleanup, and correct the generated-output
  description.

===
Changes since v2:
For shared Makefile:
- Use USERCFLAGS and USERLDFLAGS to extend CFLAGS and LDFLAGS.
- Pass OPT_FLAGS/RELEASE and EXTRA_CFLAGS to the libbpf and bpftool sub-makes.
- Use one rule per BPF source instead of a global vpath that also affected the
  caller's normal %.c rules.
- Take HOSTCC/HOSTLD from lib.mk instead of including Makefile.include, which
  also rewrote the caller's AR, LD, and CFLAGS.
- Report a missing vmlinux from the recipe rather than at parse time, so
  "make clean" still works without BTF.

For cgroup selftest:
- Create one cgroup tree, perform one flush, then read through BPF and
  cgroupfs.
- Use common cgroup helpers such as cg_read_key_long() and values_close().
- Use cpu_set_t rather than manually allocated CPU lists.
- Move test_memcontrol.c's anonymous-memory allocator into cgroup_util.
- Address the remaining review feedback.

===
Changes since v1:
- Generalize lib.bpf.mk for source layouts and suffixes, extra headers and
  flags, skeleton suffixes, subskeletons, and configurable output directories.
- Add patch 3 (hid) and patch 4 (sched_ext), converting those directories to
  the shared fragment.

Ziyang Men (4):
  selftests: add shared lib.bpf.mk to build BPF progs and skeletons
  selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush
  selftests/hid: build the BPF program via the shared lib.bpf.mk
  selftests/sched_ext: build BPF schedulers via the shared lib.bpf.mk

 tools/testing/selftests/cgroup/.gitignore     |   8 +
 tools/testing/selftests/cgroup/Makefile       |  46 ++
 tools/testing/selftests/cgroup/config         |   5 +
 .../selftests/cgroup/lib/cgroup_util.c        |  82 ++
 .../cgroup/lib/include/cgroup_util.h          |   3 +
 .../cgroup/memcg_stat_cross_cpu.bpf.c         | 101 +++
 .../selftests/cgroup/memcg_stat_cross_cpu.h   |  18 +
 .../cgroup/test_memcg_stat_cross_cpu.c        | 755 ++++++++++++++++++
 .../selftests/cgroup/test_memcontrol.c        |  29 +-
 tools/testing/selftests/hid/.gitignore        |   1 +
 tools/testing/selftests/hid/Makefile          | 179 +----
 tools/testing/selftests/hid/progs/hid.c       |   7 +-
 tools/testing/selftests/lib.bpf.mk            | 321 ++++++++
 tools/testing/selftests/sched_ext/Makefile    | 149 +---
 14 files changed, 1388 insertions(+), 316 deletions(-)
 create mode 100644 tools/testing/selftests/cgroup/memcg_stat_cross_cpu.bpf.c
 create mode 100644 tools/testing/selftests/cgroup/memcg_stat_cross_cpu.h
 create mode 100644 tools/testing/selftests/cgroup/test_memcg_stat_cross_cpu.c
 create mode 100644 tools/testing/selftests/lib.bpf.mk

-- 
2.53.0-Meta


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

* [PATCH v5 1/4] selftests: add shared lib.bpf.mk to build BPF progs and skeletons
  2026-09-10 23:32 [PATCH v5 0/4] selftests: add shared Makefile for BPF selftests and a new memcg test Ziyang Men
@ 2026-09-10 23:33 ` Ziyang Men
  2026-09-10 23:33 ` [PATCH v5 2/4] selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush Ziyang Men
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 8+ messages in thread
From: Ziyang Men @ 2026-09-10 23:33 UTC (permalink / raw)
  To: Shuah Khan, Tejun Heo, Johannes Weiner, Michal Koutný,
	Jiri Kosina, Benjamin Tissoires, David Vernet, Eduard Zingerman
  Cc: Viktor Malik, Andrea Righi, Changwoo Min, Michal Hocko,
	Roman Gushchin, Shakeel Butt, Muchun Song, Andrew Morton,
	JP Kobryn, Mykola Lysenko, Nathan Chancellor, Ziyang Men,
	kernel-team, linux-kselftest, cgroups, linux-input, sched-ext,
	linux-mm, bpf, llvm, linux-kernel

The libbpf + bpftool + vmlinux.h + BPF-object + skeleton build tool-chain
is currently duplicated across tools/testing/selftests/{bpf,sched_ext,
hid}/, each carrying ~100-140 lines of near-identical Makefile.  As more
subsystems grow BPF-based selftests, the duplication scales poorly.

This patch adds tools/testing/selftests/lib.bpf.mk, a single includable
fragment that provides the whole chain end-to-end.

To use: set BPF_SRCS and OVERRIDE_TARGETS := 1 before including ../lib.mk
(so lib.mk's default link rule is suppressed), then include ../lib.bpf.mk
and list $(BPF_SKELS) and $(BPFOBJ) as prerequisites of the test binary,
e.g.,:

    BPF_SRCS         := progs/foo.bpf.c
    TEST_GEN_PROGS   := foo_test
    OVERRIDE_TARGETS := 1
    include ../lib.mk
    include ../lib.bpf.mk
    $(OUTPUT)/foo_test: foo_test.c $(BPF_SKELS) $(BPFOBJ)
            $(call bpf_link,$@,$<)

This eases adding BPF-based selftests in other directories, such as
cgroup.

Suggested-by: Shakeel Butt <shakeel.butt@linux.dev>
Suggested-by: Eduard Zingerman <eddyz87@gmail.com>
Suggested-by: Mykola Lysenko <mykolal@meta.com>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Ziyang Men <ziyang.meme@gmail.com>
---
 tools/testing/selftests/lib.bpf.mk | 321 +++++++++++++++++++++++++++++
 1 file changed, 321 insertions(+)
 create mode 100644 tools/testing/selftests/lib.bpf.mk

diff --git a/tools/testing/selftests/lib.bpf.mk b/tools/testing/selftests/lib.bpf.mk
new file mode 100644
index 000000000000..9238cbf1de06
--- /dev/null
+++ b/tools/testing/selftests/lib.bpf.mk
@@ -0,0 +1,321 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# Build BPF programs and skeletons for selftests.
+#
+# Use it from a test Makefile like this:
+#
+#     BPF_SRCS         := foo.bpf.c bar.bpf.c
+#     TEST_GEN_PROGS   := foo_test
+#     OVERRIDE_TARGETS := 1              # set before lib.mk
+#     include ../lib.mk
+#     include ../lib.bpf.mk
+#
+#     $(OUTPUT)/foo_test: foo_test.c $(BPF_SKELS) $(BPFOBJ)
+#         $(call bpf_link,$@,$<)
+#
+# Depend on $(BPFOBJ) to relink the test when libbpf.a changes.
+#
+# Options to set before including lib.bpf.mk:
+#   BPF_PROG_EXT     - source ending; default .bpf.c. Use .c for progs/foo.c.
+#   BPF_EXTRA_HDRS   - more headers needed by the BPF objects.
+#   BPF_EXTRA_CFLAGS - more flags for compiling BPF programs.
+#   BPF_SKEL_EXT     - skeleton header ending; default .skel.h. Must end in
+#                      skel.h.
+#   BPF_GEN_SUBSKEL  - also create a subskeleton header when set.
+#   BPF_OBJ_DIR      - folder for BPF objects; default $(OUTPUT).
+#   BPF_SKEL_DIR     - folder for skeleton headers; default $(OUTPUT).
+#
+# Set output directories that use $(OUTPUT) after including lib.mk.
+#
+# BPF_CFLAGS may be replaced after this include. Generated names use the source
+# stem without its directory.
+#
+# Include lib.mk first. This file defines the BPF build variables and bpf_link,
+# extends all, CFLAGS and EXTRA_CLEAN, and enables .DELETE_ON_ERROR.
+
+include $(top_srcdir)/tools/scripts/Makefile.arch	# ARCH / SRCARCH / HOSTARCH
+
+# Remove partial files written directly to $@.
+.DELETE_ON_ERROR:
+
+# Match the libbpf and bpftool host compiler to LLVM=.
+ifneq ($(LLVM),)
+HOSTCC ?= $(LLVM_PREFIX)clang$(LLVM_SUFFIX)
+HOSTLD ?= $(LLVM_PREFIX)ld.lld$(LLVM_SUFFIX)
+else
+HOSTCC ?= gcc
+HOSTLD ?= ld
+endif
+CLANG  ?= clang
+
+ifneq ($(V),1)
+lib_bpf_submake_extras := feature_display=0
+endif
+
+# Match selftests/bpf debug and release optimization.
+OPT_FLAGS ?= $(if $(RELEASE),-O2,-O0)
+
+# ---- files and tools ------------------------------------------------------
+# Use one full path. Different forms of the same -I path can change BTF order.
+lib_bpf_tools_dir         := $(abspath $(top_srcdir)/tools)
+lib_bpf_dir               := $(lib_bpf_tools_dir)/lib/bpf
+lib_bpf_tools_include_dir := $(lib_bpf_tools_dir)/include
+lib_bpf_bpftool_dir       := $(lib_bpf_tools_dir)/bpf/bpftool
+lib_bpf_api_dir           := $(lib_bpf_tools_include_dir)/uapi
+lib_bpf_sources           := $(wildcard $(lib_bpf_dir)/*.[ch] \
+					       $(lib_bpf_dir)/Makefile)
+lib_bpf_header_sources    := $(filter %.h,$(lib_bpf_sources)) \
+				     $(lib_bpf_api_dir)/linux/bpf.h
+# Keep these names for callers which add their own BPF flags or dependencies.
+BPFDIR     := $(lib_bpf_dir)
+APIDIR     := $(lib_bpf_api_dir)
+
+# Keep private build files under $(OUTPUT) for both in-tree and O= builds.
+lib_bpf_scratch_dir := $(OUTPUT)/tools
+lib_bpf_build_dir   := $(lib_bpf_scratch_dir)/build
+lib_bpf_include_dir := $(lib_bpf_scratch_dir)/include
+# INCLUDE_DIR is also used by callers that replace BPF_CFLAGS.
+INCLUDE_DIR := $(lib_bpf_include_dir)
+BPFOBJ      := $(lib_bpf_build_dir)/libbpf/libbpf.a
+
+# Cross builds use a separate native libbpf for bpftool.
+ifneq ($(CROSS_COMPILE)$(filter-out $(HOSTARCH),$(SRCARCH)),)
+lib_bpf_host_build_dir   := $(lib_bpf_build_dir)/host
+lib_bpf_host_scratch_dir := $(OUTPUT)/host-tools
+else
+lib_bpf_host_build_dir   := $(lib_bpf_build_dir)
+lib_bpf_host_scratch_dir := $(lib_bpf_scratch_dir)
+endif
+lib_bpf_host_obj := $(lib_bpf_host_build_dir)/libbpf/libbpf.a
+DEFAULT_BPFTOOL  := $(lib_bpf_host_scratch_dir)/sbin/bpftool
+BPFTOOL          ?= $(DEFAULT_BPFTOOL)
+
+# Reuse target USERCFLAGS only when bpftool shares the target libbpf.
+ifeq ($(BPFOBJ),$(lib_bpf_host_obj))
+lib_bpf_host_user_cflags := $(USERCFLAGS)
+endif
+
+# ---- find vmlinux BTF -----------------------------------------------------
+VMLINUX_BTF_PATHS ?= $(if $(O),$(O)/vmlinux)				\
+		     $(if $(KBUILD_OUTPUT),$(KBUILD_OUTPUT)/vmlinux)	\
+		     $(top_srcdir)/vmlinux				\
+		     /sys/kernel/btf/vmlinux				\
+		     /boot/vmlinux-$(shell uname -r)
+VMLINUX_BTF ?= $(abspath $(firstword $(wildcard $(VMLINUX_BTF_PATHS))))
+# Delay missing-vmlinux errors so "make clean" still works.
+lib_bpf_vmlinux_deps := $(if $(VMLINUX_H),$(VMLINUX_H),$(VMLINUX_BTF) $(BPFTOOL))
+
+# ---- compiler flags -------------------------------------------------------
+# Find the normal system headers that Clang omits with --target=bpf. Put them
+# last with -idirafter so they cannot replace project headers.
+define lib_bpf_get_sys_includes
+$(shell $(1) $(2) -v -E - </dev/null 2>&1 \
+	| sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') \
+$(shell $(1) $(2) -dM -E - </dev/null | grep '__riscv_xlen ' | awk '{printf("-D__riscv_xlen=%d -D__BITS_PER_LONG=%d", $$3, $$3)}') \
+$(shell $(1) $(2) -dM -E - </dev/null | grep '__loongarch_grlen ' | awk '{printf("-D__BITS_PER_LONG=%d", $$3)}') \
+$(shell $(1) $(2) -dM -E - </dev/null | grep -E 'MIPS(EL|EB)|_MIPS_SZ(PTR|LONG) |_MIPS_SIM |_ABI(O32|N32|64) ' | awk '{printf("-D%s=%s ", $$2, $$3)}')
+endef
+ifneq ($(CROSS_COMPILE),)
+lib_bpf_clang_target_arch = --target=$(notdir $(CROSS_COMPILE:%-=%))
+endif
+# Find the system include flags once and reuse them.
+CLANG_SYS_INCLUDES := $(call lib_bpf_get_sys_includes,$(CLANG),$(lib_bpf_clang_target_arch))
+
+lib_bpf_is_little_endian := $(shell $(CC) -dM -E - </dev/null | \
+				    grep 'define __BYTE_ORDER__ __ORDER_LITTLE_ENDIAN__')
+MENDIAN := $(if $(lib_bpf_is_little_endian),-mlittle-endian,-mbig-endian)
+
+# Use BPF CPU v3 when Clang supports it. Otherwise use v2.
+lib_bpf_clang_cpu := $(shell $(CLANG) --target=bpf -mcpu=help 2>&1 | \
+			     grep -q 'v3' && echo v3 || echo v2)
+
+# Accept anonymous struct and union members in vmlinux.h.
+BPF_CFLAGS = -g -Wall -Werror -D__TARGET_ARCH_$(SRCARCH) $(MENDIAN)	\
+	     -I$(INCLUDE_DIR) -I$(APIDIR)				\
+	     -std=gnu11							\
+	     -fno-strict-aliasing					\
+	     -fms-extensions -Wno-microsoft-anon-tag			\
+	     -Wno-compare-distinct-pointer-types			\
+	     $(CLANG_SYS_INCLUDES) $(BPF_EXTRA_CFLAGS)
+
+# $1 = source, $2 = object. -MMD -MP tracks non-system headers.
+define lib_bpf_build_rule
+	$(call msg,CLNG-BPF,,$2)
+	$(Q)$(CLANG) $(BPF_CFLAGS) -O2 --target=bpf -mcpu=$(lib_bpf_clang_cpu) \
+		-MMD -MP -c $1 -o $2
+endef
+
+# ---- output folders -------------------------------------------------------
+BPF_OBJ_DIR  ?= $(OUTPUT)
+BPF_SKEL_DIR ?= $(OUTPUT)
+# Reject empty directories before cleanup globs can reach the filesystem root.
+ifeq ($(strip $(BPF_OBJ_DIR)),)
+$(error lib.bpf.mk: BPF_OBJ_DIR is empty; set it after "include ../lib.mk")
+endif
+ifeq ($(strip $(BPF_SKEL_DIR)),)
+$(error lib.bpf.mk: BPF_SKEL_DIR is empty; set it after "include ../lib.mk")
+endif
+
+# ---- build folders --------------------------------------------------------
+lib_bpf_make_dirs := $(sort $(lib_bpf_build_dir)/libbpf			\
+			    $(lib_bpf_host_build_dir)/libbpf		\
+			    $(lib_bpf_host_build_dir)/bpftool		\
+			    $(lib_bpf_include_dir)			\
+			    $(filter-out $(OUTPUT),$(BPF_OBJ_DIR) $(BPF_SKEL_DIR)))
+$(lib_bpf_make_dirs):
+	$(call msg,MKDIR,,$@)
+	$(Q)mkdir -p $@
+
+# ---- target libbpf --------------------------------------------------------
+# Pass unexported toolchain settings and build PIC for PIE test binaries.
+# libbpf consumes USERCFLAGS through EXTRA_CFLAGS.
+$(BPFOBJ): $(lib_bpf_sources) $(lib_bpf_api_dir)/linux/bpf.h \
+	   | $(lib_bpf_build_dir)/libbpf
+	$(Q)$(MAKE) $(lib_bpf_submake_extras) -C $(lib_bpf_dir) \
+		    OUTPUT=$(lib_bpf_build_dir)/libbpf/ \
+		    ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) CC="$(CC)"     \
+		    EXTRA_CFLAGS='-g $(OPT_FLAGS) -fPIC $(EXTRA_CFLAGS) $(USERCFLAGS)' \
+		    DESTDIR=$(lib_bpf_scratch_dir) prefix= all install_headers
+
+# ---- host libbpf, only when the target differs ----------------------------
+ifneq ($(BPFOBJ),$(lib_bpf_host_obj))
+$(lib_bpf_host_obj): $(lib_bpf_sources) $(lib_bpf_api_dir)/linux/bpf.h \
+		| $(lib_bpf_host_build_dir)/libbpf
+	$(Q)$(MAKE) $(lib_bpf_submake_extras) -C $(lib_bpf_dir) \
+		    ARCH= CROSS_COMPILE= \
+		    OUTPUT=$(lib_bpf_host_build_dir)/libbpf/ \
+		    CC="$(HOSTCC)" LD="$(HOSTLD)" \
+		    EXTRA_CFLAGS='-g $(OPT_FLAGS) $(EXTRA_CFLAGS)' \
+		    DESTDIR=$(lib_bpf_host_scratch_dir) prefix= all install_headers
+endif
+
+# ---- host bpftool ---------------------------------------------------------
+$(DEFAULT_BPFTOOL): $(wildcard $(lib_bpf_bpftool_dir)/*.[ch] \
+				 $(lib_bpf_bpftool_dir)/Makefile) \
+		    $(lib_bpf_host_obj) | $(lib_bpf_host_build_dir)/bpftool
+	$(Q)$(MAKE) $(lib_bpf_submake_extras) -C $(lib_bpf_bpftool_dir) \
+		    ARCH= CROSS_COMPILE= CC="$(HOSTCC)" LD="$(HOSTLD)" \
+		    EXTRA_CFLAGS='-g $(OPT_FLAGS) $(EXTRA_CFLAGS) $(lib_bpf_host_user_cflags)' \
+		    EXTRA_LDFLAGS='$(EXTRA_LDFLAGS)'		  \
+		    OUTPUT=$(lib_bpf_host_build_dir)/bpftool/	  \
+		    LIBBPF_OUTPUT=$(lib_bpf_host_build_dir)/libbpf/ \
+		    LIBBPF_DESTDIR=$(lib_bpf_host_scratch_dir)/	  \
+		    prefix= DESTDIR=$(lib_bpf_host_scratch_dir)/ install-bin
+
+# ---- build vmlinux.h ------------------------------------------------------
+lib_bpf_vmlinux_h     := $(INCLUDE_DIR)/vmlinux.h
+lib_bpf_vmlinux_stamp := $(INCLUDE_DIR)/vmlinux.h.stamp
+
+# Preserve vmlinux.h's timestamp when its contents do not change.
+ifeq ($(wildcard $(lib_bpf_vmlinux_h)),)
+# Regenerate if the header is missing but its stamp remains.
+.PHONY: $(lib_bpf_vmlinux_stamp)
+endif
+
+# Use a normal edge so dependents see a changed header in the same make run.
+$(lib_bpf_vmlinux_h): $(lib_bpf_vmlinux_stamp) ;
+
+$(lib_bpf_vmlinux_stamp): $(lib_bpf_vmlinux_deps) | $(INCLUDE_DIR)
+ifeq ($(VMLINUX_H),)
+	$(call msg,GEN,,$(lib_bpf_vmlinux_h))
+	$(Q)test -n "$(VMLINUX_BTF)" || { \
+		echo "lib.bpf.mk: no vmlinux at any of \"$(VMLINUX_BTF_PATHS)\"" >&2; \
+		exit 1; }
+	$(Q)$(BPFTOOL) btf dump file $(VMLINUX_BTF) format c > $@.tmp
+else
+	$(call msg,CP,,$(lib_bpf_vmlinux_h))
+	$(Q)cp "$(VMLINUX_H)" $@.tmp
+endif
+	$(Q)cmp -s $@.tmp $(lib_bpf_vmlinux_h) || mv $@.tmp $(lib_bpf_vmlinux_h)
+	$(Q)rm -f $@.tmp
+	$(Q)touch $@
+
+# ---- BPF objects and skeletons --------------------------------------------
+BPF_PROG_EXT ?= .bpf.c
+# Each source must end with BPF_PROG_EXT so Make can remove that ending.
+lib_bpf_bad_srcs := $(filter-out %$(BPF_PROG_EXT),$(BPF_SRCS))
+ifneq ($(lib_bpf_bad_srcs),)
+$(error lib.bpf.mk: BPF_SRCS entries must end in $(BPF_PROG_EXT): $(lib_bpf_bad_srcs))
+endif
+lib_bpf_stems := $(patsubst %$(BPF_PROG_EXT),%,$(notdir $(BPF_SRCS)))
+# The stem becomes the skeleton's C name; reject dots and hyphens.
+lib_bpf_bad_stems := $(strip $(foreach s,$(lib_bpf_stems),			\
+			$(if $(findstring .,$(s))$(findstring -,$(s)),$(s))))
+ifneq ($(lib_bpf_bad_stems),)
+$(error lib.bpf.mk: BPF_SRCS basenames must not contain '.' or '-': $(lib_bpf_bad_stems))
+endif
+# Output names omit directories, so reject duplicate stems.
+ifneq ($(words $(lib_bpf_stems)),$(words $(sort $(lib_bpf_stems))))
+$(error lib.bpf.mk: BPF_SRCS has colliding basenames: $(BPF_SRCS))
+endif
+BPF_SKEL_EXT    ?= .skel.h
+lib_bpf_subskel_ext := $(patsubst %skel.h,%subskel.h,$(BPF_SKEL_EXT))
+# Keep skeleton suffixes distinct and cleanup globs narrow.
+ifeq ($(lib_bpf_subskel_ext),$(BPF_SKEL_EXT))
+$(error lib.bpf.mk: BPF_SKEL_EXT must end in skel.h: $(BPF_SKEL_EXT))
+endif
+BPF_OBJS  := $(addprefix $(BPF_OBJ_DIR)/,$(addsuffix .bpf.o,$(lib_bpf_stems)))
+BPF_SKELS := $(addprefix $(BPF_SKEL_DIR)/,$(addsuffix $(BPF_SKEL_EXT),$(lib_bpf_stems)))
+ifneq ($(BPF_GEN_SUBSKEL),)
+BPF_SUBSKELS := $(addprefix $(BPF_SKEL_DIR)/,$(addsuffix $(lib_bpf_subskel_ext),$(lib_bpf_stems)))
+endif
+
+# Use per-source rules to avoid changing normal .c lookup.
+# Track source headers before BPFOBJ installs their updated copies.
+define lib_bpf_obj_rule
+$(BPF_OBJ_DIR)/$(patsubst %$(BPF_PROG_EXT),%,$(notdir $(1))).bpf.o: $(1)	\
+		$(BPF_EXTRA_HDRS) $(lib_bpf_header_sources)			\
+		$(INCLUDE_DIR)/vmlinux.h | $(BPF_OBJ_DIR) $(BPFOBJ)
+	$$(call lib_bpf_build_rule,$$<,$$@)
+endef
+$(foreach src,$(BPF_SRCS),$(eval $(call lib_bpf_obj_rule,$(src))))
+
+# Generate both headers together so either missing target rebuilds the pair.
+lib_bpf_skel_targets := $(BPF_SKEL_DIR)/%$(BPF_SKEL_EXT)
+ifneq ($(BPF_GEN_SUBSKEL),)
+lib_bpf_skel_targets += $(BPF_SKEL_DIR)/%$(lib_bpf_subskel_ext)
+endif
+
+# Link three times and require the final two objects to match.
+$(lib_bpf_skel_targets): $(BPF_OBJ_DIR)/%.bpf.o $(BPFTOOL) | $(BPF_SKEL_DIR)
+	$(call msg,GEN-SKEL,,$(BPF_SKEL_DIR)/$*$(BPF_SKEL_EXT))
+	$(Q)$(BPFTOOL) gen object $(<:.o=.linked1.o) $<
+	$(Q)$(BPFTOOL) gen object $(<:.o=.linked2.o) $(<:.o=.linked1.o)
+	$(Q)$(BPFTOOL) gen object $(<:.o=.linked3.o) $(<:.o=.linked2.o)
+	$(Q)diff $(<:.o=.linked2.o) $(<:.o=.linked3.o)
+	$(Q)$(BPFTOOL) gen skeleton $(<:.o=.linked3.o) name $* > $(BPF_SKEL_DIR)/$*$(BPF_SKEL_EXT)
+ifneq ($(BPF_GEN_SUBSKEL),)
+	$(Q)$(BPFTOOL) gen subskeleton $(<:.o=.linked3.o) name $* > $(BPF_SKEL_DIR)/$*$(lib_bpf_subskel_ext)
+endif
+	$(Q)rm -f $(<:.o=.linked1.o) $(<:.o=.linked2.o) $(<:.o=.linked3.o)
+
+# Read the header dependencies written by -MMD.
+-include $(BPF_OBJS:.o=.d)
+
+# ---- values for the test Makefile -----------------------------------------
+# Add the installed libbpf/vmlinux.h directory and the skeleton directory.
+CFLAGS += -I$(INCLUDE_DIR) -I$(BPF_SKEL_DIR)
+
+# Add target zstd when found by the target pkg-config.
+PKG_CONFIG ?= $(CROSS_COMPILE)pkg-config
+BPF_LDLIBS := $(BPFOBJ) -lelf -lz
+ifneq ($(shell $(PKG_CONFIG) --exists libzstd 2>/dev/null && echo y),)
+BPF_LDLIBS += -lzstd
+endif
+
+# Add skeletons after lib.mk defines all.
+all: $(BPF_SKELS) $(BPF_SUBSKELS)
+
+# The skeleton already embeds each BPF object in the test binary. To install the
+# objects separately, add TEST_GEN_FILES += $(BPF_OBJS).
+
+# $1 = binary, $2 = test source. Use the same compile and link flags as lib.mk.
+define bpf_link
+	$(call msg,BINARY,,$1)
+	$(Q)$(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH) $2 \
+		$(BPF_LDLIBS) $(LDLIBS) -o $1
+endef
+
+EXTRA_CLEAN += $(sort $(lib_bpf_scratch_dir) $(lib_bpf_host_scratch_dir)) \
+	       $(addprefix $(BPF_OBJ_DIR)/,*.bpf.o *.bpf.d *.linked*.o)	\
+	       $(addprefix $(BPF_SKEL_DIR)/,*$(BPF_SKEL_EXT) *$(lib_bpf_subskel_ext))
-- 
2.53.0-Meta


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

* [PATCH v5 2/4] selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush
  2026-09-10 23:32 [PATCH v5 0/4] selftests: add shared Makefile for BPF selftests and a new memcg test Ziyang Men
  2026-09-10 23:33 ` [PATCH v5 1/4] selftests: add shared lib.bpf.mk to build BPF progs and skeletons Ziyang Men
@ 2026-09-10 23:33 ` Ziyang Men
  2026-09-11  8:03   ` bot+bpf-ci
  2026-09-10 23:33 ` [PATCH v5 3/4] selftests/hid: build the BPF program via the shared lib.bpf.mk Ziyang Men
  2026-09-10 23:33 ` [PATCH v5 4/4] selftests/sched_ext: build BPF schedulers " Ziyang Men
  3 siblings, 1 reply; 8+ messages in thread
From: Ziyang Men @ 2026-09-10 23:33 UTC (permalink / raw)
  To: Shuah Khan, Tejun Heo, Johannes Weiner, Michal Koutný,
	Jiri Kosina, Benjamin Tissoires, David Vernet, Eduard Zingerman
  Cc: Viktor Malik, Andrea Righi, Changwoo Min, Michal Hocko,
	Roman Gushchin, Shakeel Butt, Muchun Song, Andrew Morton,
	JP Kobryn, Mykola Lysenko, Nathan Chancellor, Ziyang Men,
	kernel-team, linux-kselftest, cgroups, linux-input, sched-ext,
	linux-mm, bpf, llvm, linux-kernel

Add test_memcg_stat_cross_cpu, which verifies that flushing a memcg
subtree produces the same statistics through the BPF memcg kfuncs and
cgroupfs.

The existing cgroup_iter_memcg test only checks that selected values are
non-zero.  That can pass even when aggregation is incomplete, and it does
not exercise a hierarchy with charges spread over multiple CPUs.

Build a multi-level cgroup tree, charge each leaf from several CPUs, flush
the subtree once from a sleepable cgroup iterator, then compare every
reported value with memory.stat.  Also check that the root value equals the
sum of its leaves.

Probe all required kfuncs and sleepable iterator support before running.
Bound CPU fanout and aggregate memory use so large systems skip cases that
would exceed the limit instead of exhausting resources.

Suggested-by: Shakeel Butt <shakeel.butt@linux.dev>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Ziyang Men <ziyang.meme@gmail.com>
---
 tools/testing/selftests/cgroup/.gitignore     |   8 +
 tools/testing/selftests/cgroup/Makefile       |  46 ++
 tools/testing/selftests/cgroup/config         |   5 +
 .../selftests/cgroup/lib/cgroup_util.c        |  82 ++
 .../cgroup/lib/include/cgroup_util.h          |   3 +
 .../cgroup/memcg_stat_cross_cpu.bpf.c         | 101 +++
 .../selftests/cgroup/memcg_stat_cross_cpu.h   |  18 +
 .../cgroup/test_memcg_stat_cross_cpu.c        | 755 ++++++++++++++++++
 .../selftests/cgroup/test_memcontrol.c        |  29 +-
 9 files changed, 1022 insertions(+), 25 deletions(-)
 create mode 100644 tools/testing/selftests/cgroup/memcg_stat_cross_cpu.bpf.c
 create mode 100644 tools/testing/selftests/cgroup/memcg_stat_cross_cpu.h
 create mode 100644 tools/testing/selftests/cgroup/test_memcg_stat_cross_cpu.c

diff --git a/tools/testing/selftests/cgroup/.gitignore b/tools/testing/selftests/cgroup/.gitignore
index 952e4448bf07..561a3e891b98 100644
--- a/tools/testing/selftests/cgroup/.gitignore
+++ b/tools/testing/selftests/cgroup/.gitignore
@@ -6,7 +6,15 @@ test_freezer
 test_hugetlb_memcg
 test_kill
 test_kmem
+test_memcg_stat_cross_cpu
 test_memcontrol
 test_pids
 test_zswap
 wait_inotify
+# Artifacts generated by lib.bpf.mk
+/tools
+/host-tools
+*.bpf.o
+*.bpf.d
+*.linked*.o
+*.skel.h
diff --git a/tools/testing/selftests/cgroup/Makefile b/tools/testing/selftests/cgroup/Makefile
index e01584c2189a..cb999bc2c875 100644
--- a/tools/testing/selftests/cgroup/Makefile
+++ b/tools/testing/selftests/cgroup/Makefile
@@ -20,9 +20,46 @@ TEST_GEN_PROGS += test_zswap
 
 LOCAL_HDRS += $(selfdir)/clone3/clone3_selftests.h $(selfdir)/pidfd/pidfd.h
 
+# Resolve Clang early so the optional BPF test can be selected.
+ifneq ($(filter %/,$(LLVM)),)
+CLANG ?= $(LLVM)clang
+else
+CLANG ?= clang$(filter -%,$(LLVM))
+endif
+READELF ?= $(CROSS_COMPILE)readelf
+VMLINUX_BTF_PATHS ?= $(if $(O),$(O)/vmlinux)				\
+		     $(if $(KBUILD_OUTPUT),$(KBUILD_OUTPUT)/vmlinux)	\
+		     ../../../../vmlinux				\
+		     /sys/kernel/btf/vmlinux				\
+		     /boot/vmlinux-$(shell uname -r)
+# Ignore ELF images without .BTF; raw sysfs BTF needs no check.
+VMLINUX_BTF ?= $(abspath $(firstword $(foreach v,$(wildcard $(VMLINUX_BTF_PATHS)),\
+		 $(if $(filter /sys/kernel/btf/%,$(v)),$(v),\
+		   $(if $(shell $(READELF) -S "$(v)" 2>/dev/null | grep -F .BTF),$(v))))))
+# Accept either a supplied header or usable BTF.
+HAVE_BPF := $(and $(or $(VMLINUX_H),$(VMLINUX_BTF)),			\
+		  $(shell command -v $(CLANG) 2>/dev/null))
+
+ifneq ($(HAVE_BPF),)
+TEST_GEN_PROGS += test_memcg_stat_cross_cpu
+BPF_SRCS := memcg_stat_cross_cpu.bpf.c
+# Replace lib.mk's link rule below.
+OVERRIDE_TARGETS := 1
+endif
+
 include ../lib.mk
 include lib/libcgroup.mk
 
+ifneq ($(HAVE_BPF),)
+include ../lib.bpf.mk
+
+# Restore lib.mk's rule for non-BPF tests.
+LOCAL_HDRS += $(selfdir)/kselftest_harness.h $(selfdir)/kselftest.h
+$(OUTPUT)/%: %.c $(LOCAL_HDRS)
+	$(call msg,CC,,$@)
+	$(Q)$(LINK.c) $(filter-out $(LOCAL_HDRS),$^) $(LDLIBS) -o $@
+endif
+
 $(OUTPUT)/test_core: $(LIBCGROUP_O)
 $(OUTPUT)/test_cpu: $(LIBCGROUP_O)
 $(OUTPUT)/test_cpuset: $(LIBCGROUP_O)
@@ -33,3 +70,12 @@ $(OUTPUT)/test_kmem: $(LIBCGROUP_O)
 $(OUTPUT)/test_memcontrol: $(LIBCGROUP_O)
 $(OUTPUT)/test_pids: $(LIBCGROUP_O)
 $(OUTPUT)/test_zswap: $(LIBCGROUP_O)
+
+ifneq ($(HAVE_BPF),)
+# Keep in-tree UAPI headers private to the BPF test.
+$(OUTPUT)/test_memcg_stat_cross_cpu: private CFLAGS += -I$(top_srcdir)/tools/include/uapi
+# Link cgroup_util and the skeleton against the in-tree libbpf.
+$(OUTPUT)/test_memcg_stat_cross_cpu: test_memcg_stat_cross_cpu.c \
+					$(BPF_SKELS) $(LIBCGROUP_O) $(BPFOBJ)
+	$(call bpf_link,$@,$< $(LIBCGROUP_O))
+endif
diff --git a/tools/testing/selftests/cgroup/config b/tools/testing/selftests/cgroup/config
index 39f979690dd3..1f585ed9a596 100644
--- a/tools/testing/selftests/cgroup/config
+++ b/tools/testing/selftests/cgroup/config
@@ -4,3 +4,8 @@ CONFIG_CGROUP_FREEZER=y
 CONFIG_CGROUP_SCHED=y
 CONFIG_MEMCG=y
 CONFIG_PAGE_COUNTER=y
+CONFIG_BPF=y
+CONFIG_BPF_SYSCALL=y
+CONFIG_BPF_EVENTS=y
+CONFIG_DEBUG_INFO_BTF=y
+CONFIG_DEBUG_INFO_DWARF4=y
diff --git a/tools/testing/selftests/cgroup/lib/cgroup_util.c b/tools/testing/selftests/cgroup/lib/cgroup_util.c
index 2596c12cd864..ca12d794c092 100644
--- a/tools/testing/selftests/cgroup/lib/cgroup_util.c
+++ b/tools/testing/selftests/cgroup/lib/cgroup_util.c
@@ -54,6 +54,88 @@ ssize_t write_text(const char *path, char *buf, ssize_t len)
 	return len < 0 ? -errno : len;
 }
 
+/**
+ * cg_get_id - return a cgroup's kernfs ID
+ * @cgroup: absolute cgroup path
+ *
+ * This is cgrp->kn->id, not st_ino.
+ *
+ * Return: ID or 0 on failure.
+ */
+unsigned long long cg_get_id(const char *cgroup)
+{
+	union {
+		unsigned long long id;
+		unsigned char raw[8];
+	} handle;
+	struct file_handle *fhp, *fhp2;
+	int mount_id, fhsize, err;
+	unsigned long long ret = 0;
+
+	fhsize = sizeof(*fhp);
+	fhp = calloc(1, fhsize);
+	if (!fhp)
+		return 0;
+
+	/* The failed probe reports the cgroupfs handle size. */
+	err = name_to_handle_at(AT_FDCWD, cgroup, fhp, &mount_id, 0);
+	if (err >= 0 || fhp->handle_bytes != 8)
+		goto out;
+
+	fhsize = sizeof(*fhp) + fhp->handle_bytes;
+	fhp2 = realloc(fhp, fhsize);
+	if (!fhp2)
+		goto out;
+	fhp = fhp2;
+
+	if (name_to_handle_at(AT_FDCWD, cgroup, fhp, &mount_id, 0) < 0)
+		goto out;
+
+	memcpy(handle.raw, fhp->f_handle, 8);
+	ret = handle.id;
+out:
+	free(fhp);
+	return ret;
+}
+
+/**
+ * cg_touch_pages - fault every page in a region
+ * @buf: start of the region
+ * @size: length of the region in bytes
+ *
+ * Pages are charged to the caller's cgroup on its current CPU.
+ */
+void cg_touch_pages(char *buf, size_t size)
+{
+	long page_size = sysconf(_SC_PAGESIZE);
+	char *ptr;
+
+	if (page_size <= 0)
+		page_size = BUF_SIZE;
+
+	for (ptr = buf; ptr < buf + size; ptr += page_size)
+		*ptr = 0;
+}
+
+/**
+ * cg_alloc_anon - allocate and fault anonymous memory
+ * @size: bytes to allocate
+ *
+ * Return: Region to free(), or NULL.
+ */
+char *cg_alloc_anon(size_t size)
+{
+	char *buf = malloc(size);
+
+	if (!buf) {
+		fprintf(stderr, "malloc() failed\n");
+		return NULL;
+	}
+
+	cg_touch_pages(buf, size);
+	return buf;
+}
+
 char *cg_name(const char *root, const char *name)
 {
 	size_t len = strlen(root) + strlen(name) + 2;
diff --git a/tools/testing/selftests/cgroup/lib/include/cgroup_util.h b/tools/testing/selftests/cgroup/lib/include/cgroup_util.h
index 8ebb2b4d4ec0..640778018780 100644
--- a/tools/testing/selftests/cgroup/lib/include/cgroup_util.h
+++ b/tools/testing/selftests/cgroup/lib/include/cgroup_util.h
@@ -54,6 +54,9 @@ extern ssize_t write_text(const char *path, char *buf, ssize_t len);
 extern int cg_find_controller_root(char *root, size_t len, const char *controller);
 extern int cg_find_unified_root(char *root, size_t len, bool *nsdelegate);
 extern char *cg_name(const char *root, const char *name);
+extern unsigned long long cg_get_id(const char *cgroup);
+extern void cg_touch_pages(char *buf, size_t size);
+extern char *cg_alloc_anon(size_t size);
 extern char *cg_name_indexed(const char *root, const char *name, int index);
 extern char *cg_control(const char *cgroup, const char *control);
 extern int cg_create(const char *cgroup);
diff --git a/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.bpf.c b/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.bpf.c
new file mode 100644
index 000000000000..fff3b9256aa3
--- /dev/null
+++ b/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.bpf.c
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#define BPF_NO_KFUNC_PROTOTYPES
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_core_read.h>
+#include "memcg_stat_cross_cpu.h"
+
+char _license[] SEC("license") = "GPL";
+
+/* Ensure the upper-bound checks also reject negative BPF arguments. */
+_Static_assert((enum vm_event_item)-1 >= NR_VM_EVENT_ITEMS,
+	       "negative vm_event_item must fail the bounds check");
+_Static_assert((enum memcg_memory_event)-1 >= MEMCG_NR_MEMORY_EVENTS,
+	       "negative memcg_memory_event must fail the bounds check");
+
+/* Declare kfuncs that may be absent from the build-time vmlinux.h. */
+struct mem_cgroup *bpf_get_mem_cgroup(struct cgroup_subsys_state *css) __ksym;
+void bpf_put_mem_cgroup(struct mem_cgroup *memcg) __ksym;
+void bpf_mem_cgroup_flush_stats(struct mem_cgroup *memcg) __ksym;
+unsigned long bpf_mem_cgroup_page_state(struct mem_cgroup *memcg, int idx) __ksym;
+unsigned long bpf_mem_cgroup_vm_events(struct mem_cgroup *memcg,
+				       enum vm_event_item event) __ksym;
+
+/* Userspace resizes this cgroup-ID map before load. */
+struct {
+	__uint(type, BPF_MAP_TYPE_HASH);
+	__uint(max_entries, 1);
+	__type(key, __u64);
+	__type(value, struct memcg_stat_snapshot);
+} results SEC(".maps");
+
+/* Isolate capability failures from verifier failures in the test program. */
+SEC("iter.s/cgroup")
+int memcg_kfuncs_probe(struct bpf_iter__cgroup *ctx)
+{
+	struct cgroup *cgrp = ctx->cgroup;
+	struct mem_cgroup *memcg;
+
+	if (!cgrp)
+		return 0;
+
+	memcg = bpf_get_mem_cgroup(&cgrp->self);
+	if (!memcg)
+		return 0;
+
+	bpf_mem_cgroup_flush_stats(memcg);
+	bpf_mem_cgroup_page_state(memcg, 0);
+	bpf_mem_cgroup_vm_events(memcg, 0);
+	bpf_put_mem_cgroup(memcg);
+	return 0;
+}
+
+/* Sleepable iterator: flush the root once, then snapshot each cgroup. */
+SEC("iter.s/cgroup")
+int cgroup_memcg_stat_cross_cpu(struct bpf_iter__cgroup *ctx)
+{
+	struct cgroup *cgrp = ctx->cgroup;
+	struct memcg_stat_snapshot snap = {};
+	struct cgroup_subsys_state *css;
+	struct mem_cgroup *memcg;
+	int idx_anon, idx_file, idx_shmem, idx_fmapped, idx_pgfault;
+	__u64 cg_id;
+
+	/* Ignore the final post-processing call. */
+	if (!cgrp)
+		return 0;
+
+	css = &cgrp->self;
+	memcg = bpf_get_mem_cgroup(css);
+	if (!memcg)
+		return 0;
+
+	/* DESCENDANTS_PRE visits the subtree root first. */
+	if (ctx->meta->seq_num == 0)
+		bpf_mem_cgroup_flush_stats(memcg);
+
+	cg_id = BPF_CORE_READ(cgrp, kn, id);
+	snap.cgroup_id = cg_id;
+
+	idx_anon = bpf_core_enum_value(enum node_stat_item, NR_ANON_MAPPED);
+	idx_file = bpf_core_enum_value(enum node_stat_item, NR_FILE_PAGES);
+	idx_shmem = bpf_core_enum_value(enum node_stat_item, NR_SHMEM);
+	idx_fmapped = bpf_core_enum_value(enum node_stat_item, NR_FILE_MAPPED);
+	idx_pgfault = bpf_core_enum_value(enum vm_event_item, PGFAULT);
+
+	snap.anon = bpf_mem_cgroup_page_state(memcg, idx_anon);
+	snap.file = bpf_mem_cgroup_page_state(memcg, idx_file);
+	snap.shmem = bpf_mem_cgroup_page_state(memcg, idx_shmem);
+	snap.file_mapped = bpf_mem_cgroup_page_state(memcg, idx_fmapped);
+	snap.pgfault = bpf_mem_cgroup_vm_events(memcg, idx_pgfault);
+
+	/* Read page counters from the trusted pointer. */
+	snap.usage_pages = BPF_CORE_READ(memcg, memory.usage.counter);
+	snap.max_pages = BPF_CORE_READ(memcg, memory.max);
+
+	bpf_map_update_elem(&results, &cg_id, &snap, BPF_ANY);
+
+	bpf_put_mem_cgroup(memcg);
+	return 0;
+}
diff --git a/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.h b/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.h
new file mode 100644
index 000000000000..f75e341f8cb8
--- /dev/null
+++ b/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#ifndef __MEMCG_STAT_CROSS_CPU_H
+#define __MEMCG_STAT_CROSS_CPU_H
+
+/* Fixed-width snapshot shared by BPF and userspace. */
+struct memcg_stat_snapshot {
+	__u64 cgroup_id;
+	__u64 anon;		/* NR_ANON_MAPPED, bytes */
+	__u64 file;		/* NR_FILE_PAGES, bytes */
+	__u64 shmem;		/* NR_SHMEM, bytes */
+	__u64 file_mapped;	/* NR_FILE_MAPPED, bytes */
+	__u64 pgfault;		/* PGFAULT, count */
+	__u64 usage_pages;	/* page_counter memory.usage, in PAGES */
+	__u64 max_pages;	/* page_counter memory.max, in PAGES */
+};
+
+#endif /* __MEMCG_STAT_CROSS_CPU_H */
diff --git a/tools/testing/selftests/cgroup/test_memcg_stat_cross_cpu.c b/tools/testing/selftests/cgroup/test_memcg_stat_cross_cpu.c
new file mode 100644
index 000000000000..96e4a4a958ed
--- /dev/null
+++ b/tools/testing/selftests/cgroup/test_memcg_stat_cross_cpu.c
@@ -0,0 +1,755 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+/*
+ * Compare memcg BPF kfuncs with memory.stat over a charged subtree. Chargers
+ * spread anonymous memory across CPUs and hold it. Read BPF first so its flush
+ * is not consumed by memory.stat, then require two file samples to bracket each
+ * BPF value. Each leaf must cover its charge; root anon must equal the leaf sum.
+ */
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <linux/limits.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdint.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/wait.h>
+
+#include <linux/bpf.h>
+#include <bpf/bpf.h>
+#include <bpf/libbpf.h>
+#include <bpf/btf.h>
+
+#include "kselftest.h"
+#include "cgroup_util.h"
+#include "memcg_stat_cross_cpu.h"
+#include "memcg_stat_cross_cpu.skel.h"
+
+#define SUBTREE_NAME		"mcg_xcpu"
+
+#define MEMCG_CHARGE_BATCH	64
+
+#define FLUSH_MARGIN		2
+
+/* Bound total charge on large systems. */
+#define MAX_TOTAL_CHARGE	(256UL << 20)
+
+/* Bound the CPUs used by each leaf. */
+#define MAX_CPUS_PER_LEAF	16
+
+#define CHARGE_WAIT_RETRIES	500
+
+static char root[PATH_MAX];
+static char *subtree_root;
+static long page_size;
+
+/* cgroupfs values in bytes. */
+struct file_snap {
+	long anon, file, shmem, file_mapped, pgfault;
+	long current;
+};
+
+struct cg_node {
+	char path[PATH_MAX];
+	unsigned long long id;
+	bool is_leaf;			/* holds a charge of its own */
+	long previous_current;		/* previous memory.current poll */
+	struct memcg_stat_snapshot bpf;	/* read through the kfuncs */
+	struct file_snap file[2];	/* post-BPF cgroupfs samples */
+};
+
+static struct cg_node *nodes;	/* DFS order: a parent precedes its children */
+static int n_nodes;
+static int n_leaves;
+
+/* ---- CPUs this test may run on ----------------------------------------- */
+
+static cpu_set_t allowed_cpus;
+static int n_cpu;
+static long n_online_cpu;
+
+static int nth_cpu(int n)
+{
+	int i, seen = 0;
+
+	for (i = 0; i < CPU_SETSIZE; i++) {
+		if (!CPU_ISSET(i, &allowed_cpus))
+			continue;
+		if (seen++ == n % n_cpu)
+			return i;
+	}
+	return -1;
+}
+
+static int pin_cpu(int cpu)
+{
+	cpu_set_t set;
+
+	if (cpu < 0)
+		return -1;
+
+	CPU_ZERO(&set);
+	CPU_SET(cpu, &set);
+	return sched_setaffinity(0, sizeof(set), &set);
+}
+
+/* ---- tree construction -------------------------------------------------- */
+
+static int add_node(const char *path, bool is_leaf)
+{
+	struct cg_node *n = &nodes[n_nodes];
+
+	if (cg_create(path))
+		return -1;
+
+	strncpy(n->path, path, sizeof(n->path) - 1);
+	n->id = cg_get_id(path);
+	n->is_leaf = is_leaf;
+	n->previous_current = -1;
+	if (is_leaf)
+		n_leaves++;
+	n_nodes++;
+	return 0;
+}
+
+/* Build @levels below @path; only leaves are charged. */
+static int build_children(const char *path, int fanout, int levels)
+{
+	char child[PATH_MAX];
+	int i;
+
+	if (levels == 0)
+		return 0;
+
+	/* Give child cgroups a memcg. */
+	if (cg_write(path, "cgroup.subtree_control", "+memory"))
+		return -1;
+
+	for (i = 0; i < fanout; i++) {
+		snprintf(child, sizeof(child), "%s/c%d", path, i);
+		if (add_node(child, levels == 1))
+			return -1;
+		if (build_children(child, fanout, levels - 1))
+			return -1;
+	}
+	return 0;
+}
+
+static size_t tree_capacity(int fanout, int depth)
+{
+	size_t total = 1, level = 1;
+	int d;
+
+	for (d = 0; d < depth; d++) {
+		level *= fanout;
+		total += level;
+	}
+	return total;
+}
+
+static int build_tree(int fanout, int depth, int *root_fd)
+{
+	n_nodes = 0;
+	n_leaves = 0;
+	nodes = calloc(tree_capacity(fanout, depth), sizeof(*nodes));
+	if (!nodes)
+		return -1;
+
+	if (add_node(subtree_root, depth == 0))
+		return -1;
+
+	*root_fd = open(subtree_root, O_RDONLY);
+	if (*root_fd < 0)
+		return -1;
+
+	return build_children(subtree_root, fanout, depth);
+}
+
+/* Destroy children before parents, then reap chargers. */
+static void destroy_tree(void)
+{
+	int i;
+
+	if (!nodes)
+		return;
+
+	for (i = n_nodes - 1; i >= 0; i--)
+		cg_destroy(nodes[i].path);
+	free(nodes);
+	nodes = NULL;
+
+	while (waitpid(-1, NULL, 0) > 0)
+		;
+}
+
+/* ---- cross-CPU charge (one child per leaf) ------------------------------ */
+
+struct charge_args {
+	size_t bytes;	/* anon this leaf holds */
+	int base;	/* index of the first CPU to fault on */
+	int k;		/* CPUs to spread the charge over */
+};
+
+static int charge_leaf(const char *cgroup, void *arg)
+{
+	const struct charge_args *ca = arg;
+	int ppid = getppid();
+	size_t per, off;
+	char *buf;
+	int j;
+
+	buf = malloc(ca->bytes);
+	if (!buf) {
+		fprintf(stderr, "malloc() failed\n");
+		return -1;
+	}
+
+	/* Keep each CPU's slice page-aligned. */
+	per = ca->bytes / ca->k / page_size * page_size;
+
+	for (j = 0; j < ca->k; j++) {
+		off = (size_t)j * per;
+		if (pin_cpu(nth_cpu(ca->base + j))) {
+			free(buf);
+			return -1;
+		}
+		cg_touch_pages(buf + off,
+			       j == ca->k - 1 ? ca->bytes - off : per);
+	}
+
+	while (getppid() == ppid)
+		sleep(1);
+
+	free(buf);
+	return 0;
+}
+
+static int leaf_charge(size_t want, int k, size_t *bytes)
+{
+	size_t min_pages, pages, want_pages;
+
+	if ((size_t)n_online_cpu > SIZE_MAX / FLUSH_MARGIN /
+					 MEMCG_CHARGE_BATCH)
+		return -EOVERFLOW;
+
+	/* Force more than one pending batch per CPU across the subtree. */
+	min_pages = (size_t)FLUSH_MARGIN * MEMCG_CHARGE_BATCH * n_online_cpu;
+	pages = min_pages / n_leaves + !!(min_pages % n_leaves);
+	want_pages = want / page_size + !!(want % page_size);
+	if (pages < want_pages)
+		pages = want_pages;
+	if (pages < (size_t)k)
+		pages = k;
+
+	if (pages > MAX_TOTAL_CHARGE / (size_t)n_leaves / page_size)
+		return -E2BIG;
+
+	*bytes = pages * page_size;
+	return 0;
+}
+
+static int start_chargers(int k, size_t bytes)
+{
+	struct charge_args ca = { .bytes = bytes, .k = k };
+	long prev, cur;
+	bool ready;
+	int i, retries;
+
+	for (i = 0; i < n_nodes; i++) {
+		if (!nodes[i].is_leaf)
+			continue;
+		if (cg_run_nowait(nodes[i].path, charge_leaf, &ca) < 0) {
+			ksft_print_msg("cannot start a charger on %s\n",
+				       nodes[i].path);
+			return -1;
+		}
+		ca.base += k;
+	}
+
+	/* Wait until every leaf is fully charged and stable. */
+	for (retries = CHARGE_WAIT_RETRIES; retries; retries--) {
+		ready = true;
+		for (i = 0; i < n_nodes; i++) {
+			if (!nodes[i].is_leaf)
+				continue;
+
+			cur = cg_read_long(nodes[i].path, "memory.current");
+			if (cur < 0) {
+				ksft_print_msg("cannot read %s/memory.current: %s\n",
+					       nodes[i].path, strerror(errno));
+				return -1;
+			}
+
+			prev = nodes[i].previous_current;
+			nodes[i].previous_current = cur;
+			if (cur < (long)bytes || cur != prev)
+				ready = false;
+		}
+		if (ready)
+			return 0;
+		usleep(DEFAULT_WAIT_INTERVAL_US / 10);
+	}
+
+	for (i = 0; i < n_nodes; i++) {
+		if (nodes[i].is_leaf && nodes[i].previous_current < (long)bytes) {
+			ksft_print_msg("%s reached only %ld of %zu charged bytes\n",
+				       nodes[i].path, nodes[i].previous_current,
+				       bytes);
+			return -1;
+		}
+	}
+	ksft_print_msg("memory.current did not settle before the timeout\n");
+	return -1;
+}
+
+/* ---- the two readers ---------------------------------------------------- */
+
+/* Flush the subtree and collect each cgroup's kfunc values. */
+static int read_bpf(int root_fd)
+{
+	DECLARE_LIBBPF_OPTS(bpf_iter_attach_opts, opts);
+	struct memcg_stat_cross_cpu *skel = NULL;
+	union bpf_iter_link_info linfo = {};
+	struct bpf_link *link = NULL;
+	int ret = -1, err, i, mfd, fd;
+	char buf[4096];
+	ssize_t r;
+
+	skel = memcg_stat_cross_cpu__open();
+	if (!skel) {
+		ksft_print_msg("skel open failed: %s (%d)\n",
+			       strerror(errno), errno);
+		return -1;
+	}
+	err = bpf_program__set_autoload(skel->progs.memcg_kfuncs_probe, false);
+	if (err) {
+		ksft_print_msg("disabling capability probe failed: %s (%d)\n",
+			       strerror(-err), err);
+		goto out;
+	}
+	err = bpf_map__set_max_entries(skel->maps.results, n_nodes + 8);
+	if (err) {
+		ksft_print_msg("set max_entries failed: %s (%d)\n",
+			       strerror(-err), err);
+		goto out;
+	}
+	err = memcg_stat_cross_cpu__load(skel);
+	if (err) {
+		ksft_print_msg("skel load failed: %s (%d)\n",
+			       strerror(-err), err);
+		goto out;
+	}
+
+	linfo.cgroup.cgroup_fd = root_fd;
+	linfo.cgroup.order = BPF_CGROUP_ITER_DESCENDANTS_PRE;
+	opts.link_info = &linfo;
+	opts.link_info_len = sizeof(linfo);
+
+	link = bpf_program__attach_iter(skel->progs.cgroup_memcg_stat_cross_cpu,
+					&opts);
+	err = libbpf_get_error(link);
+	if (err) {
+		link = NULL;
+		ksft_print_msg("attach iter failed: %s (%d)\n",
+			       strerror(-err), err);
+		goto out;
+	}
+
+	fd = bpf_iter_create(bpf_link__fd(link));
+	if (fd < 0) {
+		ksft_print_msg("bpf_iter_create failed: %s (%d)\n",
+			       strerror(errno), errno);
+		goto out;
+	}
+	do {
+		r = read(fd, buf, sizeof(buf));
+	} while (r > 0 || (r < 0 && errno == EINTR));
+	err = errno;
+	close(fd);
+	if (r < 0) {
+		ksft_print_msg("bpf walk failed: %s (%d)\n",
+			       strerror(err), err);
+		goto out;
+	}
+
+	mfd = bpf_map__fd(skel->maps.results);
+	for (i = 0; i < n_nodes; i++)
+		if (bpf_map_lookup_elem(mfd, &nodes[i].id, &nodes[i].bpf)) {
+			ksft_print_msg("no map entry for %s: %s (%d)\n",
+				       nodes[i].path, strerror(errno), errno);
+			goto out;
+		}
+	ret = 0;
+out:
+	bpf_link__destroy(link);
+	memcg_stat_cross_cpu__destroy(skel);
+	return ret;
+}
+
+/* Read one cgroupfs snapshot. */
+static int read_files(int slot)
+{
+	int i;
+
+	for (i = 0; i < n_nodes; i++) {
+		const char *path = nodes[i].path;
+		struct file_snap *f = &nodes[i].file[slot];
+
+		f->anon = cg_read_key_long(path, "memory.stat", "anon ");
+		f->file = cg_read_key_long(path, "memory.stat", "file ");
+		f->shmem = cg_read_key_long(path, "memory.stat", "shmem ");
+		f->file_mapped = cg_read_key_long(path, "memory.stat",
+						  "file_mapped ");
+		f->pgfault = cg_read_key_long(path, "memory.stat", "pgfault ");
+		f->current = cg_read_long(path, "memory.current");
+
+		if (f->anon < 0 || f->file < 0 || f->shmem < 0 ||
+		    f->file_mapped < 0 || f->pgfault < 0 || f->current < 0) {
+			ksft_print_msg("reading the stats of %s failed\n", path);
+			return -1;
+		}
+	}
+	return 0;
+}
+
+/* ---- comparison --------------------------------------------------------- */
+
+static void dump_node(const struct cg_node *n)
+{
+	int s;
+
+	ksft_print_msg("%s bpf   : anon=%llu file=%llu shmem=%llu fmapped=%llu pgfault=%llu\n",
+		       n->path, n->bpf.anon, n->bpf.file, n->bpf.shmem,
+		       n->bpf.file_mapped, n->bpf.pgfault);
+	for (s = 0; s < 2; s++)
+		ksft_print_msg("%s file%d: anon=%ld file=%ld shmem=%ld fmapped=%ld pgfault=%ld\n",
+			       n->path, s, n->file[s].anon, n->file[s].file,
+			       n->file[s].shmem, n->file[s].file_mapped,
+			       n->file[s].pgfault);
+}
+
+/* Equal file samples require an exact BPF match; otherwise accept their range. */
+static bool bracketed(unsigned long long v, long a, long b)
+{
+	long lo = a < b ? a : b;
+	long hi = a < b ? b : a;
+
+	return v >= (unsigned long long)lo && v <= (unsigned long long)hi;
+}
+
+static int check_tree(size_t charged)
+{
+	unsigned long long root_anon = 0, leaf_anon = 0;
+	int i, bad = 0;
+
+	for (i = 0; i < n_nodes; i++) {
+		const struct cg_node *n = &nodes[i];
+		const struct memcg_stat_snapshot *b = &n->bpf;
+		const struct file_snap *f0 = &n->file[0], *f1 = &n->file[1];
+
+		if (!bracketed(b->anon, f0->anon, f1->anon) ||
+		    !bracketed(b->file, f0->file, f1->file) ||
+		    !bracketed(b->shmem, f0->shmem, f1->shmem) ||
+		    !bracketed(b->file_mapped, f0->file_mapped,
+			       f1->file_mapped) ||
+		    !bracketed(b->pgfault, f0->pgfault, f1->pgfault)) {
+			ksft_print_msg("kfuncs disagree with memory.stat\n");
+			dump_node(n);
+			bad++;
+		}
+
+		/* Live page counters are bounds, not flushed statistics. */
+		if (b->anon > b->usage_pages * (unsigned long long)page_size ||
+		    f1->anon > f1->current) {
+			ksft_print_msg("%s: anon above usage: bpf %llu/%llu file %ld/%ld\n",
+				       n->path, b->anon,
+				       b->usage_pages * (unsigned long long)page_size,
+				       f1->anon, f1->current);
+			bad++;
+		}
+
+		if (n->is_leaf) {
+			if (b->anon < charged) {
+				ksft_print_msg("%s: flushed anon %llu, charged %zu\n",
+					       n->path, b->anon, charged);
+				bad++;
+			}
+			leaf_anon += b->anon;
+		}
+		if (i == 0)
+			root_anon = b->anon;
+	}
+
+	if (root_anon != leaf_anon) {
+		ksft_print_msg("subtree root anon %llu, sum of the leaves %llu\n",
+			       root_anon, leaf_anon);
+		bad++;
+	}
+	return bad ? -1 : 0;
+}
+
+/* Accept PAGE_COUNTER_MAX values from either 32- or 64-bit kernels. */
+static int check_unlimited(void)
+{
+	unsigned long long max64 = (unsigned long long)INT64_MAX / page_size;
+	unsigned long long max32 = INT32_MAX;
+
+	if (cg_read_strcmp(nodes[0].path, "memory.max", "max\n"))
+		return 0;
+
+	if (nodes[0].bpf.max_pages != max64 && nodes[0].bpf.max_pages != max32) {
+		ksft_print_msg("memory.max reads max, kfunc reports %llu pages\n",
+			       nodes[0].bpf.max_pages);
+		return -1;
+	}
+	return 0;
+}
+
+/* ---- one case ----------------------------------------------------------- */
+
+struct testcase {
+	const char *name;
+	int fanout;
+	int depth;
+	int cpus_per_leaf;	/* K, or 0 for the bounded cross-CPU count */
+	size_t resident_bytes;	/* anon per leaf, raised if too small */
+};
+
+static int run_case(const struct testcase *tc)
+{
+	int root_fd = -1, ret = KSFT_FAIL, err, k;
+	size_t charged;
+
+	if (build_tree(tc->fanout, tc->depth, &root_fd)) {
+		ksft_print_msg("cannot build the tree\n");
+		goto out;
+	}
+
+	k = tc->cpus_per_leaf;
+	if (k <= 0)
+		k = n_cpu < MAX_CPUS_PER_LEAF ? n_cpu : MAX_CPUS_PER_LEAF;
+	else if (k > n_cpu)
+		k = n_cpu;
+	err = leaf_charge(tc->resident_bytes, k, &charged);
+	if (err == -E2BIG) {
+		ksft_print_msg("%s needs more than %luMB to trigger a flush on %ld online CPUs\n",
+			       tc->name, MAX_TOTAL_CHARGE >> 20, n_online_cpu);
+		ret = KSFT_SKIP;
+		goto out;
+	}
+	if (err) {
+		ksft_print_msg("cannot calculate the charge for %s: %s (%d)\n",
+			       tc->name, strerror(-err), err);
+		goto out;
+	}
+
+	ksft_print_msg("%s: %d cgroups, %d leaves, %d/%d cpus, %zuKB per leaf\n",
+		       tc->name, n_nodes, n_leaves, k, n_cpu, charged >> 10);
+
+	if (start_chargers(k, charged))
+		goto out;
+
+	/* Read BPF first; memory.stat would consume the pending flush. */
+	if (read_bpf(root_fd) || read_files(0) || read_files(1))
+		goto out;
+
+	if (!check_tree(charged) && !check_unlimited())
+		ret = KSFT_PASS;
+out:
+	if (root_fd >= 0)
+		close(root_fd);
+	destroy_tree();
+	return ret;
+}
+
+static const struct testcase cases[] = {
+	/* name, fanout, depth, K, anon per leaf */
+	{ "single_cpu_small_tree", 4, 2, 1, 2 << 20 },
+	{ "cross_cpu_small_tree", 4, 2, 0, 2 << 20 },
+	{ "single_cpu_large_tree", 4, 3, 1, 256 << 10 },
+	{ "cross_cpu_large_tree", 4, 3, 0, 256 << 10 },
+};
+
+static bool memcg_kfuncs_available(void)
+{
+	static const char *const kfuncs[] = {
+		"bpf_get_mem_cgroup",
+		"bpf_put_mem_cgroup",
+		"bpf_mem_cgroup_flush_stats",
+		"bpf_mem_cgroup_page_state",
+		"bpf_mem_cgroup_vm_events",
+	};
+	struct btf *btf;
+	int err, i;
+
+	btf = btf__load_vmlinux_btf();
+	err = libbpf_get_error(btf);
+	if (err) {
+		ksft_print_msg("cannot load vmlinux BTF: %s (%d)\n",
+			       strerror(-err), err);
+		return false;
+	}
+
+	for (i = 0; i < ARRAY_SIZE(kfuncs); i++) {
+		if (btf__find_by_name_kind(btf, kfuncs[i], BTF_KIND_FUNC) > 0)
+			continue;
+		ksft_print_msg("required kfunc %s is not in vmlinux BTF\n",
+			       kfuncs[i]);
+		btf__free(btf);
+		return false;
+	}
+	if (btf__find_by_name_kind(btf, "bpf_iter_cgroup", BTF_KIND_FUNC) <= 0) {
+		ksft_print_msg("cgroup BPF iterator is not in vmlinux BTF\n");
+		btf__free(btf);
+		return false;
+	}
+	btf__free(btf);
+	return true;
+}
+
+static bool unsupported_bpf_feature_error(int err)
+{
+	return err == -EINVAL || err == -ENOENT || err == -EOPNOTSUPP;
+}
+
+/* Return 1 if supported, 0 if unavailable, or a negative error. */
+static int probe_memcg_bpf_features(int root_fd)
+{
+	DECLARE_LIBBPF_OPTS(bpf_iter_attach_opts, opts);
+	struct memcg_stat_cross_cpu *skel;
+	union bpf_iter_link_info linfo = {};
+	struct bpf_link *link = NULL;
+	int ret, err, iter_fd = -1;
+
+	skel = memcg_stat_cross_cpu__open();
+	if (!skel) {
+		err = errno ? -errno : -EINVAL;
+		ksft_print_msg("capability probe open failed: %s (%d)\n",
+			       strerror(-err), err);
+		return err;
+	}
+
+	err = bpf_program__set_autoload(skel->progs.cgroup_memcg_stat_cross_cpu,
+					false);
+	if (err) {
+		ksft_print_msg("disabling test program failed: %s (%d)\n",
+			       strerror(-err), err);
+		ret = err;
+		goto out;
+	}
+
+	err = memcg_stat_cross_cpu__load(skel);
+	if (err) {
+		ksft_print_msg("BPF capability probe load failed: %s (%d)\n",
+			       strerror(-err), err);
+		ret = unsupported_bpf_feature_error(err) ? 0 : err;
+		goto out;
+	}
+
+	linfo.cgroup.cgroup_fd = root_fd;
+	linfo.cgroup.order = BPF_CGROUP_ITER_SELF_ONLY;
+	opts.link_info = &linfo;
+	opts.link_info_len = sizeof(linfo);
+	link = bpf_program__attach_iter(skel->progs.memcg_kfuncs_probe, &opts);
+	err = libbpf_get_error(link);
+	if (err) {
+		link = NULL;
+		ksft_print_msg("BPF capability probe attach failed: %s (%d)\n",
+			       strerror(-err), err);
+		ret = unsupported_bpf_feature_error(err) ? 0 : err;
+		goto out;
+	}
+
+	iter_fd = bpf_iter_create(bpf_link__fd(link));
+	if (iter_fd < 0) {
+		err = -errno;
+		ksft_print_msg("BPF capability probe iterator creation failed: %s (%d)\n",
+			       strerror(-err), err);
+		ret = unsupported_bpf_feature_error(err) ? 0 : err;
+		goto out;
+	}
+
+	ret = 1;
+out:
+	if (iter_fd >= 0)
+		close(iter_fd);
+	bpf_link__destroy(link);
+	memcg_stat_cross_cpu__destroy(skel);
+	return ret;
+}
+
+int main(int argc, char **argv)
+{
+	int feature_fd, i, ret;
+
+	ksft_print_header();
+
+	/* Probe BTF before operations that may require privileges. */
+	if (!memcg_kfuncs_available())
+		ksft_exit_skip("memcg BPF kfuncs are not available\n");
+
+	if (cg_find_unified_root(root, sizeof(root), NULL))
+		ksft_exit_skip("cgroup v2 isn't mounted\n");
+	feature_fd = open(root, O_RDONLY | O_DIRECTORY);
+	if (feature_fd < 0)
+		ksft_exit_fail_msg("cannot open cgroup root: %s (%d)\n",
+				   strerror(errno), errno);
+	ret = probe_memcg_bpf_features(feature_fd);
+	close(feature_fd);
+	if (!ret)
+		ksft_exit_skip("sleepable cgroup iterator or memcg kfuncs are not available\n");
+	if (ret < 0)
+		ksft_exit_fail_msg("cannot probe BPF capabilities: %s (%d)\n",
+				   strerror(-ret), ret);
+
+	if (cg_read_strstr(root, "cgroup.controllers", "memory"))
+		ksft_exit_skip("memory controller isn't available\n");
+
+	if (cg_read_strstr(root, "cgroup.subtree_control", "memory"))
+		if (cg_write(root, "cgroup.subtree_control", "+memory"))
+			ksft_exit_skip("Failed to set memory controller\n");
+
+	CPU_ZERO(&allowed_cpus);
+	if (sched_getaffinity(0, sizeof(allowed_cpus), &allowed_cpus))
+		ksft_exit_skip("cannot read CPU affinity\n");
+	n_cpu = CPU_COUNT(&allowed_cpus);
+	if (n_cpu <= 0)
+		ksft_exit_skip("no CPU to run on\n");
+	n_online_cpu = sysconf(_SC_NPROCESSORS_ONLN);
+	if (n_online_cpu <= 0)
+		ksft_exit_fail_msg("cannot determine the number of online CPUs\n");
+
+	page_size = sysconf(_SC_PAGESIZE);
+	if (page_size <= 0)
+		page_size = BUF_SIZE;
+
+	subtree_root = cg_name(root, SUBTREE_NAME);
+	if (!subtree_root)
+		ksft_exit_skip("cannot build subtree root path\n");
+
+	/* Set the plan after all global skip checks. */
+	ksft_set_plan(ARRAY_SIZE(cases));
+
+	for (i = 0; i < ARRAY_SIZE(cases); i++) {
+		switch (run_case(&cases[i])) {
+		case KSFT_PASS:
+			ksft_test_result_pass("%s\n", cases[i].name);
+			break;
+		case KSFT_SKIP:
+			ksft_test_result_skip("%s\n", cases[i].name);
+			break;
+		default:
+			ksft_test_result_fail("%s\n", cases[i].name);
+			break;
+		}
+	}
+
+	free(subtree_root);
+	ksft_finished();
+}
diff --git a/tools/testing/selftests/cgroup/test_memcontrol.c b/tools/testing/selftests/cgroup/test_memcontrol.c
index 0ebf796f3cff..15ba46879504 100644
--- a/tools/testing/selftests/cgroup/test_memcontrol.c
+++ b/tools/testing/selftests/cgroup/test_memcontrol.c
@@ -26,7 +26,6 @@
 
 static bool has_localevents;
 static bool has_recursiveprot;
-static int page_size;
 
 int get_temp_fd(void)
 {
@@ -56,28 +55,12 @@ int alloc_pagecache(int fd, size_t size)
 	return -1;
 }
 
-static char *alloc_and_populate_anon(size_t size)
-{
-	char *buf, *ptr;
-
-	buf = malloc(size);
-	if (buf == NULL) {
-		fprintf(stderr, "malloc() failed\n");
-		return NULL;
-	}
-
-	for (ptr = buf; ptr < buf + size; ptr += page_size)
-		*ptr = 0;
-
-	return buf;
-}
-
 int alloc_anon(const char *cgroup, void *arg)
 {
 	size_t size = (unsigned long)arg;
 	char *buf;
 
-	buf = alloc_and_populate_anon(size);
+	buf = cg_alloc_anon(size);
 	if (!buf)
 		return -1;
 
@@ -195,7 +178,7 @@ static int alloc_anon_50M_check(const char *cgroup, void *arg)
 	long anon, current;
 	int ret = -1;
 
-	buf = alloc_and_populate_anon(size);
+	buf = cg_alloc_anon(size);
 	if (!buf)
 		return -1;
 
@@ -420,7 +403,7 @@ static int alloc_anon_noexit(const char *cgroup, void *arg)
 	size_t size = (unsigned long)arg;
 	char *buf;
 
-	buf = alloc_and_populate_anon(size);
+	buf = cg_alloc_anon(size);
 	if (!buf)
 		return -1;
 
@@ -1001,7 +984,7 @@ static int alloc_anon_50M_check_swap(const char *cgroup, void *arg)
 	long mem_current, swap_current;
 	int ret = -1;
 
-	buf = alloc_and_populate_anon(size);
+	buf = cg_alloc_anon(size);
 	if (!buf)
 		return -1;
 
@@ -1793,10 +1776,6 @@ int main(int argc, char **argv)
 	char root[PATH_MAX];
 	int i, proc_status;
 
-	page_size = sysconf(_SC_PAGE_SIZE);
-	if (page_size <= 0)
-		page_size = BUF_SIZE;
-
 	ksft_print_header();
 	ksft_set_plan(ARRAY_SIZE(tests));
 	if (cg_find_unified_root(root, sizeof(root), NULL))
-- 
2.53.0-Meta


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

* [PATCH v5 3/4] selftests/hid: build the BPF program via the shared lib.bpf.mk
  2026-09-10 23:32 [PATCH v5 0/4] selftests: add shared Makefile for BPF selftests and a new memcg test Ziyang Men
  2026-09-10 23:33 ` [PATCH v5 1/4] selftests: add shared lib.bpf.mk to build BPF progs and skeletons Ziyang Men
  2026-09-10 23:33 ` [PATCH v5 2/4] selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush Ziyang Men
@ 2026-09-10 23:33 ` Ziyang Men
  2026-09-11  7:44   ` bot+bpf-ci
  2026-09-10 23:33 ` [PATCH v5 4/4] selftests/sched_ext: build BPF schedulers " Ziyang Men
  3 siblings, 1 reply; 8+ messages in thread
From: Ziyang Men @ 2026-09-10 23:33 UTC (permalink / raw)
  To: Shuah Khan, Tejun Heo, Johannes Weiner, Michal Koutný,
	Jiri Kosina, Benjamin Tissoires, David Vernet, Eduard Zingerman
  Cc: Viktor Malik, Andrea Righi, Changwoo Min, Michal Hocko,
	Roman Gushchin, Shakeel Butt, Muchun Song, Andrew Morton,
	JP Kobryn, Mykola Lysenko, Nathan Chancellor, Ziyang Men,
	kernel-team, linux-kselftest, cgroups, linux-input, sched-ext,
	linux-mm, bpf, llvm, linux-kernel

hid carries its own ~150 lines of libbpf + bpftool + vmlinux.h +
BPF-object + skeleton build machinery, copied from selftests/bpf.
Replace it with an include of the shared
tools/testing/selftests/lib.bpf.mk, so that the previous ~150 lines of
BPF build configuration can now be achieved in only ~10 lines.

The generated hid.skel.h public API remains byte-identical, and hid_bpf
and hidraw are linked the same way.  The shared fragment also supplies
the common warning, language, system-include and adaptive BPF CPU flags.
Remove four unused locals so hid.c builds with that warning set without a
directory-wide suppression.

Suggested-by: Eduard Zingerman <eddyz87@gmail.com>
Suggested-by: Mykola Lysenko <mykolal@meta.com>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Ziyang Men <ziyang.meme@gmail.com>
---
 tools/testing/selftests/hid/.gitignore  |   1 +
 tools/testing/selftests/hid/Makefile    | 179 ++----------------------
 tools/testing/selftests/hid/progs/hid.c |   7 +-
 3 files changed, 19 insertions(+), 168 deletions(-)

diff --git a/tools/testing/selftests/hid/.gitignore b/tools/testing/selftests/hid/.gitignore
index 933f483815b2..69943301dc1d 100644
--- a/tools/testing/selftests/hid/.gitignore
+++ b/tools/testing/selftests/hid/.gitignore
@@ -1,5 +1,6 @@
 bpftool
 *.skel.h
+*.bpf.d
 /host-tools
 /tools
 hid_bpf
diff --git a/tools/testing/selftests/hid/Makefile b/tools/testing/selftests/hid/Makefile
index 2f423de83147..1190a747036d 100644
--- a/tools/testing/selftests/hid/Makefile
+++ b/tools/testing/selftests/hid/Makefile
@@ -47,7 +47,6 @@ msg =
 else
 msg = @printf '  %-8s%s %s%s\n' "$(1)" "$(if $(2), [$(2)])" "$(notdir $(3))" "$(if $(4), $(4))";
 MAKEFLAGS += --no-print-directory
-submake_extras := feature_display=0
 endif
 
 # override lib.mk's default rules
@@ -58,172 +57,25 @@ override define CLEAN
 	$(Q)$(RM) -r $(EXTRA_CLEAN)
 endef
 
-include ../lib.mk
-
-TOOLSDIR := $(top_srcdir)/tools
-LIBDIR := $(TOOLSDIR)/lib
-BPFDIR := $(LIBDIR)/bpf
-TOOLSINCDIR := $(TOOLSDIR)/include
-BPFTOOLDIR := $(TOOLSDIR)/bpf/bpftool
-SCRATCH_DIR := $(OUTPUT)/tools
-BUILD_DIR := $(SCRATCH_DIR)/build
-INCLUDE_DIR := $(SCRATCH_DIR)/include
-BPFOBJ := $(BUILD_DIR)/libbpf/libbpf.a
-ifneq ($(CROSS_COMPILE),)
-HOST_BUILD_DIR		:= $(BUILD_DIR)/host
-HOST_SCRATCH_DIR	:= $(OUTPUT)/host-tools
-HOST_INCLUDE_DIR	:= $(HOST_SCRATCH_DIR)/include
-else
-HOST_BUILD_DIR		:= $(BUILD_DIR)
-HOST_SCRATCH_DIR	:= $(SCRATCH_DIR)
-HOST_INCLUDE_DIR	:= $(INCLUDE_DIR)
-endif
-HOST_BPFOBJ := $(HOST_BUILD_DIR)/libbpf/libbpf.a
-RESOLVE_BTFIDS := $(HOST_BUILD_DIR)/resolve_btfids/resolve_btfids
-
-VMLINUX_BTF_PATHS ?= $(if $(O),$(O)/vmlinux)				\
-		     $(if $(KBUILD_OUTPUT),$(KBUILD_OUTPUT)/vmlinux)	\
-		     ../../../../vmlinux				\
-		     /sys/kernel/btf/vmlinux				\
-		     /boot/vmlinux-$(shell uname -r)
-VMLINUX_BTF ?= $(abspath $(firstword $(wildcard $(VMLINUX_BTF_PATHS))))
-ifeq ($(VMLINUX_BTF),)
-$(error Cannot find a vmlinux for VMLINUX_BTF at any of "$(VMLINUX_BTF_PATHS)")
-endif
-
-# Define simple and short `make test_progs`, `make test_sysctl`, etc targets
-# to build individual tests.
-# NOTE: Semicolon at the end is critical to override lib.mk's default static
-# rule for binaries.
-$(notdir $(TEST_GEN_PROGS)): %: $(OUTPUT)/% ;
-
-# sort removes libbpf duplicates when not cross-building
-MAKE_DIRS := $(sort $(BUILD_DIR)/libbpf $(HOST_BUILD_DIR)/libbpf		\
-	       $(HOST_BUILD_DIR)/bpftool $(HOST_BUILD_DIR)/resolve_btfids	\
-	       $(INCLUDE_DIR))
-$(MAKE_DIRS):
-	$(call msg,MKDIR,,$@)
-	$(Q)mkdir -p $@
+# Build the legacy progs/*.c layout with the shared BPF rules.
+BPF_SRCS       := $(wildcard progs/*.c)
+BPF_PROG_EXT   := .c
+# BPFDIR is defined when this recursive variable is expanded.
+BPF_EXTRA_HDRS  = $(wildcard progs/*.h) $(wildcard $(BPFDIR)/hid_bpf_*.h) \
+		  $(wildcard $(BPFDIR)/*.bpf.h)
 
-DEFAULT_BPFTOOL := $(HOST_SCRATCH_DIR)/sbin/bpftool
+include ../lib.mk
+include ../lib.bpf.mk
 
+# Preserve the existing install list.
+TEST_GEN_FILES += $(BPF_OBJS)
 TEST_GEN_PROGS_EXTENDED += $(DEFAULT_BPFTOOL)
 
-$(TEST_GEN_PROGS) $(TEST_GEN_PROGS_EXTENDED): $(BPFOBJ)
-
-BPFTOOL ?= $(DEFAULT_BPFTOOL)
-$(DEFAULT_BPFTOOL): $(wildcard $(BPFTOOLDIR)/*.[ch] $(BPFTOOLDIR)/Makefile)    \
-		    $(HOST_BPFOBJ) | $(HOST_BUILD_DIR)/bpftool
-	$(Q)$(MAKE) $(submake_extras)  -C $(BPFTOOLDIR)			       \
-		    ARCH= CROSS_COMPILE= CC=$(HOSTCC) LD=$(HOSTLD) 	       \
-		    EXTRA_CFLAGS='-g -O0'				       \
-		    OUTPUT=$(HOST_BUILD_DIR)/bpftool/			       \
-		    LIBBPF_OUTPUT=$(HOST_BUILD_DIR)/libbpf/		       \
-		    LIBBPF_DESTDIR=$(HOST_SCRATCH_DIR)/			       \
-		    prefix= DESTDIR=$(HOST_SCRATCH_DIR)/ install-bin
-
-$(BPFOBJ): $(wildcard $(BPFDIR)/*.[ch] $(BPFDIR)/Makefile)		       \
-	   | $(BUILD_DIR)/libbpf
-	$(Q)$(MAKE) $(submake_extras) -C $(BPFDIR) OUTPUT=$(BUILD_DIR)/libbpf/ \
-		    EXTRA_CFLAGS='-g -O0'				       \
-		    DESTDIR=$(SCRATCH_DIR) prefix= all install_headers
-
-ifneq ($(BPFOBJ),$(HOST_BPFOBJ))
-$(HOST_BPFOBJ): $(wildcard $(BPFDIR)/*.[ch] $(BPFDIR)/Makefile)		       \
-		| $(HOST_BUILD_DIR)/libbpf
-	$(Q)$(MAKE) $(submake_extras) -C $(BPFDIR)                             \
-		    EXTRA_CFLAGS='-g -O0' ARCH= CROSS_COMPILE=		       \
-		    OUTPUT=$(HOST_BUILD_DIR)/libbpf/ CC=$(HOSTCC) LD=$(HOSTLD) \
-		    DESTDIR=$(HOST_SCRATCH_DIR)/ prefix= all install_headers
-endif
-
-$(INCLUDE_DIR)/vmlinux.h: $(VMLINUX_BTF) $(BPFTOOL) | $(INCLUDE_DIR)
-ifeq ($(VMLINUX_H),)
-	$(call msg,GEN,,$@)
-	$(Q)$(BPFTOOL) btf dump file $(VMLINUX_BTF) format c > $@
-else
-	$(call msg,CP,,$@)
-	$(Q)cp "$(VMLINUX_H)" $@
-endif
-
-$(RESOLVE_BTFIDS): $(HOST_BPFOBJ) | $(HOST_BUILD_DIR)/resolve_btfids	\
-		       $(TOOLSDIR)/bpf/resolve_btfids/main.c	\
-		       $(TOOLSDIR)/lib/rbtree.c			\
-		       $(TOOLSDIR)/lib/zalloc.c			\
-		       $(TOOLSDIR)/lib/string.c			\
-		       $(TOOLSDIR)/lib/ctype.c			\
-		       $(TOOLSDIR)/lib/str_error_r.c
-	$(Q)$(MAKE) $(submake_extras) -C $(TOOLSDIR)/bpf/resolve_btfids	\
-		CC=$(HOSTCC) LD=$(HOSTLD) AR=$(HOSTAR) \
-		LIBBPF_INCLUDE=$(HOST_INCLUDE_DIR) \
-		OUTPUT=$(HOST_BUILD_DIR)/resolve_btfids/ BPFOBJ=$(HOST_BPFOBJ)
-
-# Get Clang's default includes on this system, as opposed to those seen by
-# '--target=bpf'. This fixes "missing" files on some architectures/distros,
-# such as asm/byteorder.h, asm/socket.h, asm/sockios.h, sys/cdefs.h etc.
-#
-# Use '-idirafter': Don't interfere with include mechanics except where the
-# build would have failed anyways.
-define get_sys_includes
-$(shell $(1) -v -E - </dev/null 2>&1 \
-	| sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') \
-$(shell $(1) -dM -E - </dev/null | grep '__riscv_xlen ' | awk '{printf("-D__riscv_xlen=%d -D__BITS_PER_LONG=%d", $$3, $$3)}')
-endef
-
-# Determine target endianness.
-IS_LITTLE_ENDIAN = $(shell $(CC) -dM -E - </dev/null | \
-			grep 'define __BYTE_ORDER__ __ORDER_LITTLE_ENDIAN__')
-MENDIAN=$(if $(IS_LITTLE_ENDIAN),-mlittle-endian,-mbig-endian)
-
-CLANG_SYS_INCLUDES = $(call get_sys_includes,$(CLANG))
-BPF_CFLAGS = -g -Werror -D__TARGET_ARCH_$(SRCARCH) $(MENDIAN) 		\
-	     -Wno-microsoft-anon-tag                                    \
-	     -fms-extensions                                            \
-	     -I$(INCLUDE_DIR)
-
-CLANG_CFLAGS = $(CLANG_SYS_INCLUDES) \
-	       -Wno-compare-distinct-pointer-types
-
-# Build BPF object using Clang
-# $1 - input .c file
-# $2 - output .o file
-# $3 - CFLAGS
-define CLANG_BPF_BUILD_RULE
-	$(call msg,CLNG-BPF,$(TRUNNER_BINARY),$2)
-	$(Q)$(CLANG) $3 -O2 --target=bpf -c $1 -mcpu=v3 -o $2
-endef
-# Similar to CLANG_BPF_BUILD_RULE, but with disabled alu32
-define CLANG_NOALU32_BPF_BUILD_RULE
-	$(call msg,CLNG-BPF,$(TRUNNER_BINARY),$2)
-	$(Q)$(CLANG) $3 -O2 --target=bpf -c $1 -mcpu=v2 -o $2
-endef
-# Build BPF object using GCC
-define GCC_BPF_BUILD_RULE
-	$(call msg,GCC-BPF,$(TRUNNER_BINARY),$2)
-	$(Q)$(BPF_GCC) $3 -O2 -c $1 -o $2
-endef
-
-BPF_PROGS_DIR := progs
-BPF_BUILD_RULE := CLANG_BPF_BUILD_RULE
-BPF_SRCS := $(notdir $(wildcard $(BPF_PROGS_DIR)/*.c))
-BPF_OBJS := $(patsubst %.c,$(OUTPUT)/%.bpf.o, $(BPF_SRCS))
-BPF_SKELS := $(patsubst %.c,$(OUTPUT)/%.skel.h, $(BPF_SRCS))
-TEST_GEN_FILES += $(BPF_OBJS)
+# Keep aliases such as "make hid_bpf" from matching lib.mk's binary rule.
+$(notdir $(TEST_GEN_PROGS)): %: $(OUTPUT)/% ;
 
-$(BPF_PROGS_DIR)-bpfobjs := y
-$(BPF_OBJS): $(OUTPUT)/%.bpf.o:				\
-	     $(BPF_PROGS_DIR)/%.c			\
-	     $(wildcard $(BPF_PROGS_DIR)/*.h)		\
-	     $(INCLUDE_DIR)/vmlinux.h				\
-	     $(wildcard $(BPFDIR)/hid_bpf_*.h)			\
-	     $(wildcard $(BPFDIR)/*.bpf.h)			\
-	     | $(OUTPUT) $(BPFOBJ)
-	$(call $(BPF_BUILD_RULE),$<,$@, $(BPF_CFLAGS))
-
-$(BPF_SKELS): %.skel.h: %.bpf.o $(BPFTOOL) | $(OUTPUT)
-	$(call msg,GEN-SKEL,$(BINARY),$@)
-	$(Q)$(BPFTOOL) gen object $(<:.o=.linked1.o) $<
-	$(Q)$(BPFTOOL) gen skeleton $(<:.o=.linked1.o) name $(notdir $(<:.bpf.o=)) > $@
+# Relink tests when the in-tree libbpf changes.
+$(TEST_GEN_PROGS): $(BPFOBJ)
 
 $(OUTPUT)/%.o: %.c $(BPF_SKELS) hid_common.h
 	$(call msg,CC,,$@)
@@ -233,5 +85,4 @@ $(OUTPUT)/%: $(OUTPUT)/%.o
 	$(call msg,BINARY,,$@)
 	$(Q)$(LINK.c) $^ $(LDLIBS) -o $@
 
-EXTRA_CLEAN := $(SCRATCH_DIR) $(HOST_SCRATCH_DIR) feature bpftool	\
-	$(addprefix $(OUTPUT)/,*.o *.skel.h no_alu32)
+EXTRA_CLEAN += $(OUTPUT)/*.o
diff --git a/tools/testing/selftests/hid/progs/hid.c b/tools/testing/selftests/hid/progs/hid.c
index b21fbb13c926..ec734e18d23a 100644
--- a/tools/testing/selftests/hid/progs/hid.c
+++ b/tools/testing/selftests/hid/progs/hid.c
@@ -111,7 +111,7 @@ int hid_user_raw_request(struct hid_hw_request_syscall_args *args)
 {
 	struct hid_bpf_ctx *ctx;
 	const size_t size = args->size;
-	int i, ret = 0;
+	int ret = 0;
 
 	if (size > sizeof(args->data))
 		return -7; /* -E2BIG */
@@ -137,7 +137,7 @@ int hid_user_output_report(struct hid_hw_request_syscall_args *args)
 {
 	struct hid_bpf_ctx *ctx;
 	const size_t size = args->size;
-	int i, ret = 0;
+	int ret = 0;
 
 	if (size > sizeof(args->data))
 		return -7; /* -E2BIG */
@@ -161,7 +161,7 @@ int hid_user_input_report(struct hid_hw_request_syscall_args *args)
 {
 	struct hid_bpf_ctx *ctx;
 	const size_t size = args->size;
-	int i, ret = 0;
+	int ret = 0;
 
 	if (size > sizeof(args->data))
 		return -7; /* -E2BIG */
@@ -417,7 +417,6 @@ SEC("?struct_ops.s/hid_hw_output_report")
 int BPF_PROG(hid_test_hidraw_output_report, struct hid_bpf_ctx *hctx, __u64 source)
 {
 	__u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 3 /* size */);
-	int ret;
 
 	if (!data)
 		return 0; /* EPERM check */
-- 
2.53.0-Meta


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

* [PATCH v5 4/4] selftests/sched_ext: build BPF schedulers via the shared lib.bpf.mk
  2026-09-10 23:32 [PATCH v5 0/4] selftests: add shared Makefile for BPF selftests and a new memcg test Ziyang Men
                   ` (2 preceding siblings ...)
  2026-09-10 23:33 ` [PATCH v5 3/4] selftests/hid: build the BPF program via the shared lib.bpf.mk Ziyang Men
@ 2026-09-10 23:33 ` Ziyang Men
  2026-09-11  7:44   ` bot+bpf-ci
  3 siblings, 1 reply; 8+ messages in thread
From: Ziyang Men @ 2026-09-10 23:33 UTC (permalink / raw)
  To: Shuah Khan, Tejun Heo, Johannes Weiner, Michal Koutný,
	Jiri Kosina, Benjamin Tissoires, David Vernet, Eduard Zingerman
  Cc: Viktor Malik, Andrea Righi, Changwoo Min, Michal Hocko,
	Roman Gushchin, Shakeel Butt, Muchun Song, Andrew Morton,
	JP Kobryn, Mykola Lysenko, Nathan Chancellor, Ziyang Men,
	kernel-team, linux-kselftest, cgroups, linux-input, sched-ext,
	linux-mm, bpf, llvm, linux-kernel

sched_ext carries its own ~130 lines of libbpf + bpftool + vmlinux.h +
BPF-object + skeleton build machinery.  Replace it with an include of the
shared tools/testing/selftests/lib.bpf.mk, making sched_ext the third
in-tree consumer of that fragment, after selftests/cgroup and
selftests/hid.

All generated skeletons and subskeletons keep the same public API, and
the runner is built and linked the same way.

Suggested-by: Eduard Zingerman <eddyz87@gmail.com>
Suggested-by: Mykola Lysenko <mykolal@meta.com>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Ziyang Men <ziyang.meme@gmail.com>
---
 tools/testing/selftests/sched_ext/Makefile | 149 ++++-----------------
 1 file changed, 26 insertions(+), 123 deletions(-)

diff --git a/tools/testing/selftests/sched_ext/Makefile b/tools/testing/selftests/sched_ext/Makefile
index 5d2dffca0e91..856fe7dea685 100644
--- a/tools/testing/selftests/sched_ext/Makefile
+++ b/tools/testing/selftests/sched_ext/Makefile
@@ -14,48 +14,29 @@ CURDIR := $(abspath .)
 REPOROOT := $(abspath ../../../..)
 TOOLSDIR := $(REPOROOT)/tools
 LIBDIR := $(TOOLSDIR)/lib
-BPFDIR := $(LIBDIR)/bpf
 TOOLSINCDIR := $(TOOLSDIR)/include
-BPFTOOLDIR := $(TOOLSDIR)/bpf/bpftool
-APIDIR := $(TOOLSINCDIR)/uapi
 GENDIR := $(REPOROOT)/include/generated
 GENHDR := $(GENDIR)/autoconf.h
-SCXTOOLSDIR := $(TOOLSDIR)/sched_ext
 SCXTOOLSINCDIR := $(TOOLSDIR)/sched_ext/include
 
-OUTPUT_DIR := $(OUTPUT)/build
-OBJ_DIR := $(OUTPUT_DIR)/obj
-INCLUDE_DIR := $(OUTPUT_DIR)/include
-BPFOBJ_DIR := $(OBJ_DIR)/libbpf
-SCXOBJ_DIR := $(OBJ_DIR)/sched_ext
-BPFOBJ := $(BPFOBJ_DIR)/libbpf.a
-LIBBPF_OUTPUT := $(OBJ_DIR)/libbpf/libbpf.a
-
-DEFAULT_BPFTOOL := $(OUTPUT_DIR)/host/sbin/bpftool
-HOST_OBJ_DIR := $(OBJ_DIR)/host/bpftool
-HOST_LIBBPF_OUTPUT := $(OBJ_DIR)/host/libbpf/
-HOST_LIBBPF_DESTDIR := $(OUTPUT_DIR)/host/
-HOST_DESTDIR := $(OUTPUT_DIR)/host/
-
-VMLINUX_BTF_PATHS ?= $(if $(O),$(O)/vmlinux)					\
-		     $(if $(KBUILD_OUTPUT),$(KBUILD_OUTPUT)/vmlinux)		\
-		     ../../../../vmlinux					\
-		     /sys/kernel/btf/vmlinux					\
-		     /boot/vmlinux-$(shell uname -r)
-VMLINUX_BTF ?= $(abspath $(firstword $(wildcard $(VMLINUX_BTF_PATHS))))
-ifeq ($(VMLINUX_BTF),)
-$(error Cannot find a vmlinux for VMLINUX_BTF at any of "$(VMLINUX_BTF_PATHS)")
-endif
+# Build scheduler skeletons and subskeletons with the shared BPF rules.
+BPF_SRCS        := $(wildcard *.bpf.c)
+BPF_SKEL_EXT    := .bpf.skel.h
+BPF_GEN_SUBSKEL := 1
+# Preserve the existing build/ layout.
+BPF_OBJ_DIR  := $(OUTPUT)/build/obj/sched_ext
+BPF_SKEL_DIR := $(OUTPUT)/build/include
+SCXOBJ_DIR   := $(BPF_OBJ_DIR)
 
-BPFTOOL ?= $(DEFAULT_BPFTOOL)
+include ../lib.bpf.mk
 
 ifneq ($(wildcard $(GENHDR)),)
   GENFLAGS := -DHAVE_GENHDR
 endif
 
 CFLAGS += -g -O2 -rdynamic -pthread -Wall -Werror $(GENFLAGS)			\
-	  -I$(INCLUDE_DIR) -I$(GENDIR) -I$(LIBDIR)				\
-	  -I$(TOOLSINCDIR) -I$(APIDIR) -I$(CURDIR)/include -I$(SCXTOOLSINCDIR)
+	  -I$(GENDIR) -I$(LIBDIR) -I$(TOOLSINCDIR) -I$(APIDIR)			\
+	  -I$(CURDIR)/include -I$(SCXTOOLSINCDIR)
 
 # Silence some warnings when compiled with clang
 ifneq ($(LLVM),)
@@ -64,102 +45,26 @@ endif
 
 LDFLAGS = -lelf -lz -lpthread -lzstd
 
-IS_LITTLE_ENDIAN = $(shell $(CC) -dM -E - </dev/null |				\
-			grep 'define __BYTE_ORDER__ __ORDER_LITTLE_ENDIAN__')
-
-# Get Clang's default includes on this system, as opposed to those seen by
-# '-target bpf'. This fixes "missing" files on some architectures/distros,
-# such as asm/byteorder.h, asm/socket.h, asm/sockios.h, sys/cdefs.h etc.
-#
-# Use '-idirafter': Don't interfere with include mechanics except where the
-# build would have failed anyways.
-define get_sys_includes
-$(shell $(1) $(2) -v -E - </dev/null 2>&1 \
-	| sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') \
-$(shell $(1) $(2) -dM -E - </dev/null | grep '__riscv_xlen ' | awk '{printf("-D__riscv_xlen=%d -D__BITS_PER_LONG=%d", $$3, $$3)}')
-endef
-
-ifneq ($(CROSS_COMPILE),)
-CLANG_TARGET_ARCH = --target=$(notdir $(CROSS_COMPILE:%-=%))
-endif
-
-CLANG_SYS_INCLUDES = $(call get_sys_includes,$(CLANG),$(CLANG_TARGET_ARCH))
-
-BPF_CFLAGS = -g -D__TARGET_ARCH_$(SRCARCH)					\
-	     $(if $(IS_LITTLE_ENDIAN),-mlittle-endian,-mbig-endian)		\
+# Preserve the scheduler-specific flags; the shared recipe adds target flags.
+BPF_CFLAGS = -g -D__TARGET_ARCH_$(SRCARCH) $(MENDIAN)				\
 	     -I$(CURDIR)/include -I$(CURDIR)/include/bpf-compat			\
 	     -I$(INCLUDE_DIR) -I$(APIDIR) -I$(SCXTOOLSINCDIR)			\
 	     -I$(REPOROOT)/include						\
-	     $(CLANG_SYS_INCLUDES) 						\
+	     $(CLANG_SYS_INCLUDES)						\
 	     -Wall -Wno-compare-distinct-pointer-types				\
 	     -Wno-incompatible-function-pointer-types				\
 	     -Wno-microsoft-anon-tag						\
 	     -fms-extensions							\
-	     -O2 -mcpu=v3
-
-# sort removes libbpf duplicates when not cross-building
-MAKE_DIRS := $(sort $(OBJ_DIR)/libbpf $(OBJ_DIR)/libbpf				\
-	       $(OBJ_DIR)/bpftool $(OBJ_DIR)/resolve_btfids			\
-	       $(HOST_OBJ_DIR) $(INCLUDE_DIR) $(SCXOBJ_DIR))
-
-$(MAKE_DIRS):
-	$(call msg,MKDIR,,$@)
-	$(Q)mkdir -p $@
-
-$(BPFOBJ): $(wildcard $(BPFDIR)/*.[ch] $(BPFDIR)/Makefile)			\
-	   $(APIDIR)/linux/bpf.h						\
-	   | $(OBJ_DIR)/libbpf
-	$(Q)$(MAKE) $(submake_extras) -C $(BPFDIR) OUTPUT=$(OBJ_DIR)/libbpf/	\
-		    ARCH=$(ARCH) CC="$(CC)" CROSS_COMPILE=$(CROSS_COMPILE)	\
-		    EXTRA_CFLAGS='-g -O0 -fPIC'					\
-		    DESTDIR=$(OUTPUT_DIR) prefix= all install_headers
-
-$(DEFAULT_BPFTOOL): $(wildcard $(BPFTOOLDIR)/*.[ch] $(BPFTOOLDIR)/Makefile)	\
-		    $(LIBBPF_OUTPUT) | $(HOST_OBJ_DIR)
-	$(Q)$(MAKE) $(submake_extras)  -C $(BPFTOOLDIR)				\
-		    ARCH= CROSS_COMPILE= CC=$(HOSTCC) LD=$(HOSTLD)		\
-		    EXTRA_CFLAGS='-g -O0'					\
-		    OUTPUT=$(HOST_OBJ_DIR)/					\
-		    LIBBPF_OUTPUT=$(HOST_LIBBPF_OUTPUT)				\
-		    LIBBPF_DESTDIR=$(HOST_LIBBPF_DESTDIR)			\
-		    prefix= DESTDIR=$(HOST_DESTDIR) install-bin
-
-$(INCLUDE_DIR)/vmlinux.h: $(VMLINUX_BTF) $(BPFTOOL) | $(INCLUDE_DIR)
-ifeq ($(VMLINUX_H),)
-	$(call msg,GEN,,$@)
-	$(Q)$(BPFTOOL) btf dump file $(VMLINUX_BTF) format c > $@
-else
-	$(call msg,CP,,$@)
-	$(Q)cp "$(VMLINUX_H)" $@
-endif
-
-$(SCXOBJ_DIR)/%.bpf.o: %.bpf.c $(INCLUDE_DIR)/vmlinux.h	| $(BPFOBJ) $(SCXOBJ_DIR)
-	$(call msg,CLNG-BPF,,$(notdir $@))
-	$(Q)$(CLANG) $(BPF_CFLAGS) -target bpf -c $< -o $@
+	     $(BPF_EXTRA_CFLAGS)
 
-$(INCLUDE_DIR)/%.bpf.skel.h: $(SCXOBJ_DIR)/%.bpf.o $(INCLUDE_DIR)/vmlinux.h $(BPFTOOL) | $(INCLUDE_DIR)
-	$(eval sched=$(notdir $@))
-	$(call msg,GEN-SKEL,,$(sched))
-	$(Q)$(BPFTOOL) gen object $(<:.o=.linked1.o) $<
-	$(Q)$(BPFTOOL) gen object $(<:.o=.linked2.o) $(<:.o=.linked1.o)
-	$(Q)$(BPFTOOL) gen object $(<:.o=.linked3.o) $(<:.o=.linked2.o)
-	$(Q)diff $(<:.o=.linked2.o) $(<:.o=.linked3.o)
-	$(Q)$(BPFTOOL) gen skeleton $(<:.o=.linked3.o) name $(subst .bpf.skel.h,,$(sched)) > $@
-	$(Q)$(BPFTOOL) gen subskeleton $(<:.o=.linked3.o) name $(subst .bpf.skel.h,,$(sched)) > $(@:.skel.h=.subskel.h)
+EXTRA_CLEAN += $(OUTPUT)/build
 
 ################
 # C schedulers #
 ################
 
-override define CLEAN
-	rm -rf $(OUTPUT_DIR)
-	rm -f $(TEST_GEN_PROGS)
-endef
-
-# Every testcase takes all of the BPF progs are dependencies by default. This
-# allows testcases to load any BPF scheduler, which is useful for testcases
-# that don't need their own prog to run their test.
-all_test_bpfprogs := $(foreach prog,$(wildcard *.bpf.c),$(INCLUDE_DIR)/$(patsubst %.c,%.skel.h,$(prog)))
+# Build every scheduler before each test.
+all_test_bpfprogs := $(BPF_SKELS)
 
 auto-test-targets :=			\
 	create_dsq			\
@@ -195,24 +100,22 @@ auto-test-targets :=			\
 testcase-targets := $(addsuffix .o,$(addprefix $(SCXOBJ_DIR)/,$(auto-test-targets)))
 
 $(SCXOBJ_DIR)/runner.o: runner.c | $(SCXOBJ_DIR) $(BPFOBJ)
-	$(CC) $(CFLAGS) -c $< -o $@
+	$(call msg,CC,,$@)
+	$(Q)$(CC) $(CFLAGS) -c $< -o $@
 
 # Create all of the test targets object files, whose testcase objects will be
 # registered into the runner in ELF constructors.
-#
-# Note that we must do double expansion here in order to support conditionally
-# compiling BPF object files only if one is present, as the wildcard Make
-# function doesn't support using implicit rules otherwise.
 $(testcase-targets): $(SCXOBJ_DIR)/%.o: %.c $(SCXOBJ_DIR)/runner.o $(all_test_bpfprogs) | $(SCXOBJ_DIR)
-	$(eval test=$(patsubst %.o,%.c,$(notdir $@)))
-	$(CC) $(CFLAGS) -c $< -o $@
+	$(call msg,CC,,$@)
+	$(Q)$(CC) $(CFLAGS) -c $< -o $@
 
 $(SCXOBJ_DIR)/util.o: util.c | $(SCXOBJ_DIR)
-	$(CC) $(CFLAGS) -c $< -o $@
+	$(call msg,CC,,$@)
+	$(Q)$(CC) $(CFLAGS) -c $< -o $@
 
 $(OUTPUT)/runner: $(SCXOBJ_DIR)/runner.o $(SCXOBJ_DIR)/util.o $(BPFOBJ) $(testcase-targets)
-	@echo "$(testcase-targets)"
-	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
+	$(call msg,BINARY,,$@)
+	$(Q)$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
 
 .DEFAULT_GOAL := all
 
-- 
2.53.0-Meta


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

* Re: [PATCH v5 3/4] selftests/hid: build the BPF program via the shared lib.bpf.mk
  2026-09-10 23:33 ` [PATCH v5 3/4] selftests/hid: build the BPF program via the shared lib.bpf.mk Ziyang Men
@ 2026-09-11  7:44   ` bot+bpf-ci
  0 siblings, 0 replies; 8+ messages in thread
From: bot+bpf-ci @ 2026-09-11  7:44 UTC (permalink / raw)
  To: ziyang.meme, shuah, tj, hannes, mkoutny, jikos, bentiss, void,
	eddyz87
  Cc: vmalik, arighi, changwoo, mhocko, roman.gushchin, shakeel.butt,
	muchun.song, akpm, inwardvessel, mykolal, nathan, ziyang.meme,
	kernel-team, linux-kselftest, cgroups, linux-input, sched-ext,
	linux-mm, bpf, llvm, linux-kernel, ast, andrii, daniel,
	martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai

[-- Attachment #1: Type: text/plain, Size: 1986 bytes --]

> diff --git a/tools/testing/selftests/hid/Makefile b/tools/testing/selftests/hid/Makefile
> index 2f423de83147..1190a747036d 100644
> --- a/tools/testing/selftests/hid/Makefile
> +++ b/tools/testing/selftests/hid/Makefile
> @@ -47,7 +47,6 @@ msg =
>  else
>  msg = @printf '  %-8s%s %s%s\n' "$(1)" "$(if $(2), [$(2)])" "$(notdir $(3))" "$(if $(4), $(4))";
>  MAKEFLAGS += --no-print-directory
> -submake_extras := feature_display=0
>  endif

This isn't a bug, but with submake_extras gone, does anything still need
this local msg/MAKEFLAGS block?  lib.mk defines both identically a few
lines below, so it looks like it could be dropped along with the rest of
the copied machinery.

> @@ -58,172 +57,25 @@ override define CLEAN
>  	$(Q)$(RM) -r $(EXTRA_CLEAN)
>  endef
>
> -include ../lib.mk
> -
> -TOOLSDIR := $(top_srcdir)/tools
> -LIBDIR := $(TOOLSDIR)/lib
> -BPFDIR := $(LIBDIR)/bpf
> -TOOLSINCDIR := $(TOOLSDIR)/include
> -BPFTOOLDIR := $(TOOLSDIR)/bpf/bpftool
> -SCRATCH_DIR := $(OUTPUT)/tools
> -BUILD_DIR := $(SCRATCH_DIR)/build
> -INCLUDE_DIR := $(SCRATCH_DIR)/include
> -BPFOBJ := $(BUILD_DIR)/libbpf/libbpf.a

[ ... ]

> +# Build the legacy progs/*.c layout with the shared BPF rules.
> +BPF_SRCS       := $(wildcard progs/*.c)
> +BPF_PROG_EXT   := .c
> +# BPFDIR is defined when this recursive variable is expanded.
> +BPF_EXTRA_HDRS  = $(wildcard progs/*.h) $(wildcard $(BPFDIR)/hid_bpf_*.h) \
> +		  $(wildcard $(BPFDIR)/*.bpf.h)
> +

This isn't a bug, but do the two $(BPFDIR) wildcards still match anything
here?  tools/lib/bpf has no hid_bpf_*.h, and lib.bpf.mk's
lib_bpf_header_sources already covers *.bpf.h - if so, this could become
a simple BPF_EXTRA_HDRS := $(wildcard progs/*.h) and lose the
recursive-expansion comment.


---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/34573210499

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

* Re: [PATCH v5 4/4] selftests/sched_ext: build BPF schedulers via the shared lib.bpf.mk
  2026-09-10 23:33 ` [PATCH v5 4/4] selftests/sched_ext: build BPF schedulers " Ziyang Men
@ 2026-09-11  7:44   ` bot+bpf-ci
  0 siblings, 0 replies; 8+ messages in thread
From: bot+bpf-ci @ 2026-09-11  7:44 UTC (permalink / raw)
  To: ziyang.meme, shuah, tj, hannes, mkoutny, jikos, bentiss, void,
	eddyz87
  Cc: vmalik, arighi, changwoo, mhocko, roman.gushchin, shakeel.butt,
	muchun.song, akpm, inwardvessel, mykolal, nathan, ziyang.meme,
	kernel-team, linux-kselftest, cgroups, linux-input, sched-ext,
	linux-mm, bpf, llvm, linux-kernel, ast, andrii, daniel,
	martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai

[-- Attachment #1: Type: text/plain, Size: 4013 bytes --]

> diff --git a/tools/testing/selftests/sched_ext/Makefile b/tools/testing/selftests/sched_ext/Makefile
> index 3cfe90e0f34f..dd3f5594af24 100644
> --- a/tools/testing/selftests/sched_ext/Makefile
> +++ b/tools/testing/selftests/sched_ext/Makefile
> @@ -14,48 +14,29 @@ CURDIR := $(abspath .)
>  REPOROOT := $(abspath ../../../..)
>  TOOLSDIR := $(REPOROOT)/tools
>  LIBDIR := $(TOOLSDIR)/lib
> -BPFDIR := $(LIBDIR)/bpf
>  TOOLSINCDIR := $(TOOLSDIR)/include
> -BPFTOOLDIR := $(TOOLSDIR)/bpf/bpftool
> -APIDIR := $(TOOLSINCDIR)/uapi
>  GENDIR := $(REPOROOT)/include/generated
>  GENHDR := $(GENDIR)/autoconf.h
> -SCXTOOLSDIR := $(TOOLSDIR)/sched_ext
>  SCXTOOLSINCDIR := $(TOOLSDIR)/sched_ext/include
>
> -OUTPUT_DIR := $(OUTPUT)/build
> -OBJ_DIR := $(OUTPUT_DIR)/obj
> -INCLUDE_DIR := $(OUTPUT_DIR)/include
> -BPFOBJ_DIR := $(OBJ_DIR)/libbpf
> -SCXOBJ_DIR := $(OBJ_DIR)/sched_ext
> -BPFOBJ := $(BPFOBJ_DIR)/libbpf.a
> -LIBBPF_OUTPUT := $(OBJ_DIR)/libbpf/libbpf.a
> -
> -DEFAULT_BPFTOOL := $(OUTPUT_DIR)/host/sbin/bpftool
> -HOST_OBJ_DIR := $(OBJ_DIR)/host/bpftool
> -HOST_LIBBPF_OUTPUT := $(OBJ_DIR)/host/libbpf/
> -HOST_LIBBPF_DESTDIR := $(OUTPUT_DIR)/host/
> -HOST_DESTDIR := $(OUTPUT_DIR)/host/
> -
> -VMLINUX_BTF_PATHS ?= $(if $(O),$(O)/vmlinux)					\
> -		     $(if $(KBUILD_OUTPUT),$(KBUILD_OUTPUT)/vmlinux)		\
> -		     ../../../../vmlinux					\
> -		     /sys/kernel/btf/vmlinux					\
> -		     /boot/vmlinux-$(shell uname -r)
> -VMLINUX_BTF ?= $(abspath $(firstword $(wildcard $(VMLINUX_BTF_PATHS))))
> -ifeq ($(VMLINUX_BTF),)
> -$(error Cannot find a vmlinux for VMLINUX_BTF at any of "$(VMLINUX_BTF_PATHS)")
> -endif
> +# Build scheduler skeletons and subskeletons with the shared BPF rules.
> +BPF_SRCS        := $(wildcard *.bpf.c)
> +BPF_SKEL_EXT    := .bpf.skel.h
> +BPF_GEN_SUBSKEL := 1

This isn't a bug, but is anything consuming the generated
*.bpf.subskel.h headers? Grepping the directory for "subskel" matches
only the Makefile itself - no .c or .h file under
tools/testing/selftests/sched_ext/ includes a subskeleton header. The
pre-patch rule generated them too, so this preserves existing behavior,
but the conversion makes the subskeleton an explicit one-line opt-in.
Would dropping BPF_GEN_SUBSKEL be a reasonable follow-up to save a
bpftool run per scheduler?

> +# Preserve the existing build/ layout.
> +BPF_OBJ_DIR  := $(OUTPUT)/build/obj/sched_ext
> +BPF_SKEL_DIR := $(OUTPUT)/build/include
> +SCXOBJ_DIR   := $(BPF_OBJ_DIR)
>
> -BPFTOOL ?= $(DEFAULT_BPFTOOL)
> +include ../lib.bpf.mk

[ ... ]

> @@ -64,102 +45,26 @@ endif
>
>  LDFLAGS = -lelf -lz -lpthread -lzstd

[ ... ]

> +EXTRA_CLEAN += $(OUTPUT)/build
>
>  ################
>  # C schedulers #
>  ################
>
> -override define CLEAN
> -	rm -rf $(OUTPUT_DIR)
> -	rm -f $(TEST_GEN_PROGS)
> -endef
> -
> -# Every testcase takes all of the BPF progs are dependencies by default. This
> -# allows testcases to load any BPF scheduler, which is useful for testcases
> -# that don't need their own prog to run their test.
> -all_test_bpfprogs := $(foreach prog,$(wildcard *.bpf.c),$(INCLUDE_DIR)/$(patsubst %.c,%.skel.h,$(prog)))
> +# Build every scheduler before each test.
> +all_test_bpfprogs := $(BPF_SKELS)

This isn't a bug, but since the variable assignment is otherwise a
straight substitution (the foreach expression becomes $(BPF_SKELS)),
would it be worth keeping the original comment wording? The previous
text explained the rationale - "Every testcase takes all of the BPF
progs as dependencies by default. This allows testcases to load any BPF
scheduler, which is useful for testcases that don't need their own prog
to run their test." - while the replacement restates what the following
line does and drops the reason a testcase depends on schedulers it does
not own.


---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/34573210499

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

* Re: [PATCH v5 2/4] selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush
  2026-09-10 23:33 ` [PATCH v5 2/4] selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush Ziyang Men
@ 2026-09-11  8:03   ` bot+bpf-ci
  0 siblings, 0 replies; 8+ messages in thread
From: bot+bpf-ci @ 2026-09-11  8:03 UTC (permalink / raw)
  To: ziyang.meme, shuah, tj, hannes, mkoutny, jikos, bentiss, void,
	eddyz87
  Cc: vmalik, arighi, changwoo, mhocko, roman.gushchin, shakeel.butt,
	muchun.song, akpm, inwardvessel, mykolal, nathan, ziyang.meme,
	kernel-team, linux-kselftest, cgroups, linux-input, sched-ext,
	linux-mm, bpf, llvm, linux-kernel, ast, andrii, daniel,
	martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai

[-- Attachment #1: Type: text/plain, Size: 18973 bytes --]

> diff --git a/tools/testing/selftests/cgroup/lib/cgroup_util.c b/tools/testing/selftests/cgroup/lib/cgroup_util.c
> index 2596c12cd864..ca12d794c092 100644
> --- a/tools/testing/selftests/cgroup/lib/cgroup_util.c
> +++ b/tools/testing/selftests/cgroup/lib/cgroup_util.c
> @@ -54,6 +54,88 @@ ssize_t write_text(const char *path, char *buf, ssize_t len)
>  	return len < 0 ? -errno : len;
>  }
>
> +/**
> + * cg_get_id - return a cgroup's kernfs ID
> + * @cgroup: absolute cgroup path
> + *
> + * This is cgrp->kn->id, not st_ino.
> + *
> + * Return: ID or 0 on failure.
> + */
> +unsigned long long cg_get_id(const char *cgroup)
> +{
> +	union {
> +		unsigned long long id;
> +		unsigned char raw[8];
> +	} handle;
> +	struct file_handle *fhp, *fhp2;
> +	int mount_id, fhsize, err;
> +	unsigned long long ret = 0;
> +
> +	fhsize = sizeof(*fhp);
> +	fhp = calloc(1, fhsize);
> +	if (!fhp)
> +		return 0;
> +
> +	/* The failed probe reports the cgroupfs handle size. */
> +	err = name_to_handle_at(AT_FDCWD, cgroup, fhp, &mount_id, 0);
> +	if (err >= 0 || fhp->handle_bytes != 8)
> +		goto out;
> +
> +	fhsize = sizeof(*fhp) + fhp->handle_bytes;
> +	fhp2 = realloc(fhp, fhsize);
> +	if (!fhp2)
> +		goto out;
> +	fhp = fhp2;
> +
> +	if (name_to_handle_at(AT_FDCWD, cgroup, fhp, &mount_id, 0) < 0)
> +		goto out;
> +
> +	memcpy(handle.raw, fhp->f_handle, 8);
> +	ret = handle.id;
> +out:
> +	free(fhp);
> +	return ret;
> +}

[ ... ]

> diff --git a/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.bpf.c b/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.bpf.c
> new file mode 100644
> index 000000000000..fff3b9256aa3
> --- /dev/null
> +++ b/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.bpf.c
> @@ -0,0 +1,101 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
> +#define BPF_NO_KFUNC_PROTOTYPES
> +#include <vmlinux.h>
> +#include <bpf/bpf_helpers.h>
> +#include <bpf/bpf_core_read.h>
> +#include "memcg_stat_cross_cpu.h"

A subsystem pattern flags this as potentially concerning: this new BPF
program is a narrow cross-CPU variant of a test that already exists in
tools/testing/selftests/bpf/progs/cgroup_iter_memcg.c. The two programs
are structurally near-identical - same SEC("iter.s/cgroup") attachment,
same 'css = &cgrp->self; memcg = bpf_get_mem_cgroup(css)' prologue, the
same bpf_mem_cgroup_flush_stats() call, the same five
bpf_core_enum_value() lookups for NR_ANON_MAPPED / NR_FILE_PAGES /
NR_SHMEM / NR_FILE_MAPPED / PGFAULT fed into
bpf_mem_cgroup_page_state() and bpf_mem_cgroup_vm_events(), and the same
release. The only differences are that the new program writes a
BPF_MAP_TYPE_HASH keyed by cgroup ID instead of a global variable, and
flushes only at seq_num == 0.

Placing the variant in tools/testing/selftests/cgroup/ rather than
extending the existing test costs roughly 40 lines of new BPF build
machinery in the Makefile and a new BPF/BTF config fragment, all of
which selftests/bpf already has.

Would it make sense for the cross-CPU correctness assertions to be an
additional case in the existing cgroup_iter_memcg test instead?

> +
> +/* Sleepable iterator: flush the root once, then snapshot each cgroup. */
> +SEC("iter.s/cgroup")
> +int cgroup_memcg_stat_cross_cpu(struct bpf_iter__cgroup *ctx)
> +{
> +	struct cgroup *cgrp = ctx->cgroup;
> +	struct memcg_stat_snapshot snap = {};
> +	struct cgroup_subsys_state *css;
> +	struct mem_cgroup *memcg;
> +	int idx_anon, idx_file, idx_shmem, idx_fmapped, idx_pgfault;
> +	__u64 cg_id;
> +
> +	/* Ignore the final post-processing call. */
> +	if (!cgrp)
> +		return 0;
> +
> +	css = &cgrp->self;
> +	memcg = bpf_get_mem_cgroup(css);
> +	if (!memcg)
> +		return 0;
> +
> +	/* DESCENDANTS_PRE visits the subtree root first. */
> +	if (ctx->meta->seq_num == 0)
> +		bpf_mem_cgroup_flush_stats(memcg);

The whole test is built on the assumption that this single call
actually flushes the subtree, but bpf_mem_cgroup_flush_stats() is not a
forced flush and can be a no-op.

Looking at mm/bpf_memcontrol.c, bpf_mem_cgroup_flush_stats() calls
mem_cgroup_flush_stats(memcg), which calls
__mem_cgroup_flush_stats(memcg, false), and mm/memcontrol.c contains:

    static void __mem_cgroup_flush_stats(struct mem_cgroup *memcg, bool force)
    {
            bool needs_flush = memcg_vmstats_needs_flush(memcg->vmstats);
            ...
            if (!force && !needs_flush)
                    return;
            ...
            css_rstat_flush(&memcg->css);
    }

with:

    static bool memcg_vmstats_needs_flush(struct memcg_vmstats *vmstats)
    {
            return atomic_long_read(&vmstats->stats_updates) >
                    MEMCG_CHARGE_BATCH * num_online_cpus();
    }

So css_rstat_flush() only runs when the subtree root has accumulated
more than MEMCG_CHARGE_BATCH (64) * num_online_cpus() pending stat
updates. The commit message states the test will "flush the subtree once
from a sleepable cgroup iterator, then compare every reported value with
memory.stat", and also says it will "Bound CPU fanout and aggregate
memory use so large systems skip cases that would exceed the limit".
Those two statements are in tension: the same bounding that keeps the
footprint small on a many-CPU machine also lowers the number of stat
updates generated relative to the num_online_cpus()-scaled threshold, so
on a large host the flush the test depends on can be skipped entirely.

This can cause two outcomes:

1. False pass. When the flush is skipped, the BPF program snapshots
whatever aggregate is already cached in memcg->vmstats. Those cached
values are self-consistent (they came from an earlier propagation), so
the 'root == sum of leaves' check and the comparison against memory.stat
can both succeed without the flush path ever having been exercised. The
test then reports PASS on a kernel whose cross-CPU aggregation is
broken, which is precisely the weakness the commit message says it
exists to fix ("The existing cgroup_iter_memcg test only checks that
selected values are non-zero").

2. False failure. The BPF snapshot and the userspace memory.stat read
happen at different times, and mm/memcontrol.c runs a periodic *forced*
flush of the whole tree every FLUSH_TIME:

       static void flush_memcg_stats_dwork(struct work_struct *w)
       {
               __mem_cgroup_flush_stats(root_mem_cgroup, true);
               queue_delayed_work(system_dfl_wq, &stats_flush_dwork, FLUSH_TIME);
       }

If the iterator's own flush was skipped (leaving per-CPU deltas
unpropagated) and the forced dwork lands between the iterator run and
the memory.stat read, memory.stat sees the newly propagated deltas that
the snapshot does not, and the exact-equality comparison fails on a
correct kernel.

Note that the kfunc's own kernel-doc ('Propagate memory cgroup's
statistics up the cgroup tree') reads as unconditional, which is likely
why the test relies on it; the implementation is conditional.

Making the comparison tolerant of a skipped flush, or generating enough
stat updates to clear MEMCG_CHARGE_BATCH * num_online_cpus() on the
largest host the test is bounded for, would close both holes.

[ ... ]

> diff --git a/tools/testing/selftests/cgroup/test_memcg_stat_cross_cpu.c b/tools/testing/selftests/cgroup/test_memcg_stat_cross_cpu.c
> new file mode 100644
> index 000000000000..96e4a4a958ed
> --- /dev/null
> +++ b/tools/testing/selftests/cgroup/test_memcg_stat_cross_cpu.c

[ ... ]

> +static int add_node(const char *path, bool is_leaf)
> +{
> +	struct cg_node *n = &nodes[n_nodes];
> +
> +	if (cg_create(path))
> +		return -1;
> +
> +	strncpy(n->path, path, sizeof(n->path) - 1);
> +	n->id = cg_get_id(path);

Does this code check for cg_get_id() failure? cg_get_id() is documented
and coded to return 0 on failure:

    /**
     * cg_get_id - return a cgroup's kernfs ID
     * ...
     * Return: ID or 0 on failure.
     */
    ...
    	err = name_to_handle_at(AT_FDCWD, cgroup, fhp, &mount_id, 0);
    	if (err >= 0 || fhp->handle_bytes != 8)
    		goto out;	/* ret is still 0 */

name_to_handle_at() fails with ENOSYS on a kernel built with
CONFIG_FHANDLE=n (init/Kconfig: 'bool "open by fhandle syscalls" if
EXPERT / select EXPORTFS / default y' -- it is a real, selectable
configuration, and tools/testing/selftests/cgroup/config does not list
CONFIG_FHANDLE). In that case fhp->handle_bytes stays 0 from the
calloc(), so cg_get_id() returns 0 for every node and add_node()
silently records id == 0 for all of them.

The failure only surfaces much later in read_bpf():

	for (i = 0; i < n_nodes; i++)
		if (bpf_map_lookup_elem(mfd, &nodes[i].id, &nodes[i].bpf)) {
			ksft_print_msg("no map entry for %s: %s (%d)\n", ...);

The BPF side keys the results map on BPF_CORE_READ(cgrp, kn, id), which
is never 0, so every lookup misses and run_case() returns KSFT_FAIL. A
missing kernel feature is therefore reported as 'not ok <case>' with the
misleading message 'no map entry for /sys/fs/cgroup/mcg_xcpu: No such
file or directory', instead of the skip the rest of this test is careful
to emit for absent prerequisites.

Checking the return in add_node() (return -1 when n->id == 0, which
build_tree() already propagates into the existing 'cannot build the
tree' path) makes the diagnosis immediate.

[ ... ]

> +/* Flush the subtree and collect each cgroup's kfunc values. */
> +static int read_bpf(int root_fd)
> +{
> +	DECLARE_LIBBPF_OPTS(bpf_iter_attach_opts, opts);
> +	struct memcg_stat_cross_cpu *skel = NULL;
> +	union bpf_iter_link_info linfo = {};
> +	struct bpf_link *link = NULL;
> +	int ret = -1, err, i, mfd, fd;
> +	char buf[4096];
> +	ssize_t r;
> +
> +	skel = memcg_stat_cross_cpu__open();
> +	if (!skel) {
> +		ksft_print_msg("skel open failed: %s (%d)\n",
> +			       strerror(errno), errno);
> +		return -1;
> +	}
> +	err = bpf_program__set_autoload(skel->progs.memcg_kfuncs_probe, false);
> +	if (err) {
> +		ksft_print_msg("disabling capability probe failed: %s (%d)\n",
> +			       strerror(-err), err);
> +		goto out;
> +	}
> +	err = bpf_map__set_max_entries(skel->maps.results, n_nodes + 8);
> +	if (err) {
> +		ksft_print_msg("set max_entries failed: %s (%d)\n",
> +			       strerror(-err), err);
> +		goto out;
> +	}
> +	err = memcg_stat_cross_cpu__load(skel);
> +	if (err) {
> +		ksft_print_msg("skel load failed: %s (%d)\n",
> +			       strerror(-err), err);
> +		goto out;
> +	}

A subsystem pattern flags this as potentially concerning: read_bpf()
converts capability-absent errors into a hard test failure, while the
same errno classes are treated as a skip elsewhere in the same file.

The three BPF setup steps in read_bpf() - memcg_stat_cross_cpu__load()
above, bpf_program__attach_iter() below, and bpf_iter_create() further
down - all fall through to `goto out` with ret == -1, which run_case()
turns into KSFT_FAIL and main() into ksft_test_result_fail().

The file already has the classifier for this:

	static bool unsupported_bpf_feature_error(int err)
	{
		return err == -EINVAL || err == -ENOENT || err == -EOPNOTSUPP;
	}

and probe_memcg_bpf_features() uses it on exactly these three calls to
return 0 so that main() can ksft_exit_skip("sleepable cgroup iterator
or memcg kfuncs are not available"). read_bpf() does not consult it, so
a capability gap that the up-front probe does not model surfaces as a
regression signal rather than a skip.

The probe and read_bpf() are not identical: the probe attaches
memcg_kfuncs_probe with BPF_CGROUP_ITER_SELF_ONLY, whereas read_bpf()
attaches cgroup_memcg_stat_cross_cpu with
BPF_CGROUP_ITER_DESCENDANTS_PRE and additionally resizes
skel->maps.results to n_nodes + 8 before loading.

[ ... ]

> +/* Equal file samples require an exact BPF match; otherwise accept their range. */
> +static bool bracketed(unsigned long long v, long a, long b)
> +{
> +	long lo = a < b ? a : b;
> +	long hi = a < b ? b : a;
> +
> +	return v >= (unsigned long long)lo && v <= (unsigned long long)hi;
> +}

Michal Koutny raised a concern in v2 about using the existing
values_close() helper from cgroup_util.h instead of implementing a
custom comparison function. You acknowledged this feedback and said you
would use values_close(), but the current code still uses a custom
bracketed() function. While this function is correct and appropriate for
the new design (bracketing BPF values between two file reads), it
differs from the acknowledged reviewer suggestion. Could you clarify the
design change that led to keeping the custom function?

> +static int run_case(const struct testcase *tc)
> +{
> +	int root_fd = -1, ret = KSFT_FAIL, err, k;
> +	size_t charged;
> +
> +	if (build_tree(tc->fanout, tc->depth, &root_fd)) {
> +		ksft_print_msg("cannot build the tree\n");
> +		goto out;
> +	}
> +
> +	k = tc->cpus_per_leaf;
> +	if (k <= 0)
> +		k = n_cpu < MAX_CPUS_PER_LEAF ? n_cpu : MAX_CPUS_PER_LEAF;
> +	else if (k > n_cpu)
> +		k = n_cpu;
> +	err = leaf_charge(tc->resident_bytes, k, &charged);
> +	if (err == -E2BIG) {
> +		ksft_print_msg("%s needs more than %luMB to trigger a flush on %ld online CPUs\n",
> +			       tc->name, MAX_TOTAL_CHARGE >> 20, n_online_cpu);
> +		ret = KSFT_SKIP;
> +		goto out;
> +	}
> +	if (err) {
> +		ksft_print_msg("cannot calculate the charge for %s: %s (%d)\n",
> +			       tc->name, strerror(-err), err);
> +		goto out;
> +	}
> +
> +	ksft_print_msg("%s: %d cgroups, %d leaves, %d/%d cpus, %zuKB per leaf\n",
> +		       tc->name, n_nodes, n_leaves, k, n_cpu, charged >> 10);
> +
> +	if (start_chargers(k, charged))
> +		goto out;
> +
> +	/* Read BPF first; memory.stat would consume the pending flush. */
> +	if (read_bpf(root_fd) || read_files(0) || read_files(1))
> +		goto out;

Can the ordering here actually bracket the BPF value? Both cgroupfs
samples are taken after the BPF sample, so they cannot bracket it and
the second sample adds no tolerance where tolerance is needed.

Ordering is fixed: read_bpf() first, then read_files(0), then
read_files(1). check_tree() then asks:

	if (!bracketed(b->anon, f0->anon, f1->anon) || ... ||
	    !bracketed(b->pgfault, f0->pgfault, f1->pgfault)) {

and bracketed() accepts v only inside [min(a,b), max(a,b)]:

	long lo = a < b ? a : b;
	long hi = a < b ? b : a;
	return v >= (unsigned long long)lo && v <= (unsigned long long)hi;

Because the BPF value was sampled strictly before f0, the interval
[f0,f1] only tolerates drift that begins after f0 was read. Any change
during the read_bpf()->read_files(0) window pushes both f0 and f1 to
the same side of the BPF value and the comparison fails even though the
kfunc returned the correct value at the time it was taken.

For a monotonically increasing counter the requirement collapses to
exact equality: pgfault only ever grows, so f0->pgfault >= b->pgfault
always, and the check can only pass when b->pgfault == f0->pgfault.

The exposed window is not small and grows with the tree. read_files(0)
issues six file reads per cgroup (five cg_read_key_long() reads of
memory.stat plus one memory.current), so for the deepest case (fanout 4,
depth 3 => 85 cgroups) the last node's f0 is read after roughly 500
cgroupfs reads, all of which sit inside the untolerated interval for
that node.

What mostly hides this today is that memory.stat's flush is threshold
gated (memcg_vmstats_needs_flush() requires more than MEMCG_CHARGE_BATCH
* num_online_cpus() pending updates), so after the BPF flush the
following memory.stat reads usually return the identical already-flushed
values. That makes the comparison an equality check in practice, which
is what the comment admits ("Equal file samples require an exact BPF
match"), but it also means the two-sample scheme provides no protection
against the one interval it was added for.

Sampling memory.stat once before read_bpf() and once after would
genuinely bracket the BPF value; alternatively drop the second sample
and document that the check is an equality check.

The file header comment ("then require two file samples to bracket each
BPF value") and the changelog ("then compare every reported value with
memory.stat") describe a tolerance the code does not provide.

[ ... ]

> +static bool unsupported_bpf_feature_error(int err)
> +{
> +	return err == -EINVAL || err == -ENOENT || err == -EOPNOTSUPP;
> +}
> +
> +/* Return 1 if supported, 0 if unavailable, or a negative error. */
> +static int probe_memcg_bpf_features(int root_fd)
> +{
> +	DECLARE_LIBBPF_OPTS(bpf_iter_attach_opts, opts);
> +	struct memcg_stat_cross_cpu *skel;
> +	union bpf_iter_link_info linfo = {};
> +	struct bpf_link *link = NULL;
> +	int ret, err, iter_fd = -1;
> +
> +	skel = memcg_stat_cross_cpu__open();
> +	if (!skel) {
> +		err = errno ? -errno : -EINVAL;
> +		ksft_print_msg("capability probe open failed: %s (%d)\n",
> +			       strerror(-err), err);
> +		return err;
> +	}
> +
> +	err = bpf_program__set_autoload(skel->progs.cgroup_memcg_stat_cross_cpu,
> +					false);
> +	if (err) {
> +		ksft_print_msg("disabling test program failed: %s (%d)\n",
> +			       strerror(-err), err);
> +		ret = err;
> +		goto out;
> +	}
> +
> +	err = memcg_stat_cross_cpu__load(skel);
> +	if (err) {
> +		ksft_print_msg("BPF capability probe load failed: %s (%d)\n",
> +			       strerror(-err), err);
> +		ret = unsupported_bpf_feature_error(err) ? 0 : err;

Does the capability-absent error set need to include -EPERM and -EACCES?
The classifier omits them, so running the new test without CAP_BPF turns
an environment difference into a hard TAP failure instead of a skip.

Path: main() -> probe_memcg_bpf_features() ->
memcg_stat_cross_cpu__load() -> bpf_object__load() ->
bpf_object__probe_loading() (tools/lib/bpf/libbpf.c), which does a
trivial bpf_prog_load() and returns -errno. Without CAP_BPF (or with
kernel.unprivileged_bpf_disabled set, the default on most distros) that
is -EPERM; loading the iter.s/cgroup program itself additionally needs
CAP_BPF+CAP_PERFMON. -EPERM is not in the set above, so:

    err = memcg_stat_cross_cpu__load(skel);        /* -EPERM */
    ret = unsupported_bpf_feature_error(err) ? 0 : err;   /* -> -EPERM */

and main() does:

    if (ret < 0)
        ksft_exit_fail_msg("cannot probe BPF capabilities: %s (%d)\n", ...);

so the binary reports 'not ok' / exits KSFT_FAIL rather than skipping.
Nothing earlier in main() gates on privilege: memcg_kfuncs_available()
only reads /sys/kernel/btf/vmlinux (world readable),
cg_find_unified_root() reads /proc/mounts, and open(root,
O_RDONLY|O_DIRECTORY) on /sys/fs/cgroup succeeds for any user.

Note that -EPERM from bpf() is unambiguous - it is a capability/sysctl
gate, never a verifier verdict (the verifier returns -EACCES/-EINVAL) -
so adding -EPERM (and -ENOSYS, for a kernel without the bpf syscall) to
the skip set does not weaken the test.


---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/34573210499

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

end of thread, other threads:[~2026-09-11  8:03 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-10 23:32 [PATCH v5 0/4] selftests: add shared Makefile for BPF selftests and a new memcg test Ziyang Men
2026-09-10 23:33 ` [PATCH v5 1/4] selftests: add shared lib.bpf.mk to build BPF progs and skeletons Ziyang Men
2026-09-10 23:33 ` [PATCH v5 2/4] selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush Ziyang Men
2026-09-11  8:03   ` bot+bpf-ci
2026-09-10 23:33 ` [PATCH v5 3/4] selftests/hid: build the BPF program via the shared lib.bpf.mk Ziyang Men
2026-09-11  7:44   ` bot+bpf-ci
2026-09-10 23:33 ` [PATCH v5 4/4] selftests/sched_ext: build BPF schedulers " Ziyang Men
2026-09-11  7:44   ` bot+bpf-ci

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox