* [PATCH v4 0/4] selftests: add shared Makefile for BPF selftests and new memcg test
@ 2026-09-08 23:55 Ziyang Men
2026-09-08 23:55 ` [PATCH v4 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-08 23:55 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 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 | 95 +++
.../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, 1382 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 v4 1/4] selftests: add shared lib.bpf.mk to build BPF progs and skeletons
2026-09-08 23:55 [PATCH v4 0/4] selftests: add shared Makefile for BPF selftests and new memcg test Ziyang Men
@ 2026-09-08 23:55 ` Ziyang Men
2026-09-09 0:02 ` sashiko-bot
2026-09-09 18:26 ` Tejun Heo
2026-09-08 23:55 ` [PATCH v4 2/4] selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush Ziyang Men
` (2 subsequent siblings)
3 siblings, 2 replies; 8+ messages in thread
From: Ziyang Men @ 2026-09-08 23:55 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..f27cd1889e7f
--- /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 an order-only edge so the stamp's mtime does not propagate.
+$(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 v4 2/4] selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush
2026-09-08 23:55 [PATCH v4 0/4] selftests: add shared Makefile for BPF selftests and new memcg test Ziyang Men
2026-09-08 23:55 ` [PATCH v4 1/4] selftests: add shared lib.bpf.mk to build BPF progs and skeletons Ziyang Men
@ 2026-09-08 23:55 ` Ziyang Men
2026-09-09 0:05 ` sashiko-bot
2026-09-08 23:55 ` [PATCH v4 3/4] selftests/hid: build the BPF program via the shared lib.bpf.mk Ziyang Men
2026-09-08 23:55 ` [PATCH v4 4/4] selftests/sched_ext: build BPF schedulers " Ziyang Men
3 siblings, 1 reply; 8+ messages in thread
From: Ziyang Men @ 2026-09-08 23:55 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 | 95 +++
.../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, 1016 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..c3c69ecd888f
--- /dev/null
+++ b/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.bpf.c
@@ -0,0 +1,95 @@
+// 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";
+
+/* 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 v4 3/4] selftests/hid: build the BPF program via the shared lib.bpf.mk
2026-09-08 23:55 [PATCH v4 0/4] selftests: add shared Makefile for BPF selftests and new memcg test Ziyang Men
2026-09-08 23:55 ` [PATCH v4 1/4] selftests: add shared lib.bpf.mk to build BPF progs and skeletons Ziyang Men
2026-09-08 23:55 ` [PATCH v4 2/4] selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush Ziyang Men
@ 2026-09-08 23:55 ` Ziyang Men
2026-09-08 23:55 ` [PATCH v4 4/4] selftests/sched_ext: build BPF schedulers " Ziyang Men
3 siblings, 0 replies; 8+ messages in thread
From: Ziyang Men @ 2026-09-08 23:55 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 v4 4/4] selftests/sched_ext: build BPF schedulers via the shared lib.bpf.mk
2026-09-08 23:55 [PATCH v4 0/4] selftests: add shared Makefile for BPF selftests and new memcg test Ziyang Men
` (2 preceding siblings ...)
2026-09-08 23:55 ` [PATCH v4 3/4] selftests/hid: build the BPF program via the shared lib.bpf.mk Ziyang Men
@ 2026-09-08 23:55 ` Ziyang Men
3 siblings, 0 replies; 8+ messages in thread
From: Ziyang Men @ 2026-09-08 23:55 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 v4 1/4] selftests: add shared lib.bpf.mk to build BPF progs and skeletons
2026-09-08 23:55 ` [PATCH v4 1/4] selftests: add shared lib.bpf.mk to build BPF progs and skeletons Ziyang Men
@ 2026-09-09 0:02 ` sashiko-bot
2026-09-09 18:26 ` Tejun Heo
1 sibling, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-09-09 0:02 UTC (permalink / raw)
To: Ziyang Men
Cc: sched-ext, linux-input, tj, hannes, mkoutny, dmitry.torokhov, bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Order-only prerequisite on stamp file prevents rebuilding dependent objects when vmlinux.h changes.
--
commit f7b71013493f3d0a8611d16b18e727d7b737042f
Author: Ziyang Men <ziyang.meme@gmail.com>
selftests: add shared lib.bpf.mk to build BPF progs and skeletons
This patch adds a shared Makefile fragment (lib.bpf.mk) to standardize the
build tool-chain for BPF programs and skeletons across selftests. It
centralizes the build logic to reduce duplication as more subsystems adopt
BPF-based tests.
> diff --git a/tools/testing/selftests/lib.bpf.mk b/tools/testing/selftests/lib.bpf.mk
> new file mode 100644
> index 0000000000000..f27cd1889e7ff
> --- /dev/null
> +++ b/tools/testing/selftests/lib.bpf.mk
[ ... ]
> +# ---- 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 an order-only edge so the stamp's mtime does not propagate.
> +$(lib_bpf_vmlinux_h): | $(lib_bpf_vmlinux_stamp) ;
[Severity: Low]
Does using an order-only prerequisite here prevent rebuilding dependent
objects when vmlinux.h changes?
Using an order-only prerequisite means GNU Make might not re-stat
vmlinux.h after the stamp recipe updates it. To allow single-pass rebuilds
while avoiding redundant rebuilds when the contents haven't changed, would
it be better to use a normal dependency with the empty recipe `;`?
> +
> +$(lib_bpf_vmlinux_stamp): $(lib_bpf_vmlinux_deps) | $(INCLUDE_DIR)
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260908235552.2610256-1-ziyang.meme@gmail.com?part=1
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v4 2/4] selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush
2026-09-08 23:55 ` [PATCH v4 2/4] selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush Ziyang Men
@ 2026-09-09 0:05 ` sashiko-bot
0 siblings, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-09-09 0:05 UTC (permalink / raw)
To: Ziyang Men
Cc: tj, bpf, mkoutny, sched-ext, dmitry.torokhov, linux-input, hannes
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [High] Missing bounds check for negative enum values in memcg BPF kfuncs (`bpf_mem_cgroup_memory_events` and `bpf_mem_cgroup_vm_events`), leading to potential out-of-bounds array reads.
--
commit 75dfb2e14e4c5552066e9fdeb8f96969135f991d
Author: Ziyang Men <ziyang.meme@gmail.com>
selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush
Add test_memcg_stat_cross_cpu, which verifies that flushing a memcg
subtree produces the same statistics through the BPF memcg kfuncs and
cgroupfs.
> 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 0000000000000..c3c69ecd888f0
> --- /dev/null
> +++ b/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.bpf.c
> @@ -0,0 +1,95 @@
[ ... ]
> +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);
[Severity: High]
This isn't a bug introduced by this patch, but can this kfunc and related
memcg kfuncs be exploited by passing negative enum values to read
out-of-bounds memory?
In mm/bpf_memcontrol.c, the bpf_mem_cgroup_vm_events() kfunc takes an enum
event parameter:
mm/bpf_memcontrol.c:bpf_mem_cgroup_vm_events() {
if (unlikely(!memcg_vm_event_item_valid(event)))
return (unsigned long)-1;
...
}
The helper memcg_vm_event_item_valid() in mm/memcontrol.c uses a signed
comparison that doesn't catch negative values:
mm/memcontrol.c:memcg_vm_event_item_valid() {
if (idx >= NR_VM_EVENT_ITEMS)
return false;
return !BAD_STAT_IDX(memcg_events_index(idx));
}
Similarly, bpf_mem_cgroup_memory_events() lacks a negative check:
mm/bpf_memcontrol.c:bpf_mem_cgroup_memory_events() {
if (unlikely(event >= MEMCG_NR_MEMORY_EVENTS))
return (unsigned long)-1;
return atomic_long_read(&memcg->memory_events[event]);
}
Since the BPF verifier treats enums as plain integers and doesn't enforce
their valid ranges, a negative index can bypass these validations and lead to
a backward out-of-bounds read from the target array.
> + bpf_put_mem_cgroup(memcg);
> + return 0;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260908235552.2610256-1-ziyang.meme@gmail.com?part=2
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v4 1/4] selftests: add shared lib.bpf.mk to build BPF progs and skeletons
2026-09-08 23:55 ` [PATCH v4 1/4] selftests: add shared lib.bpf.mk to build BPF progs and skeletons Ziyang Men
2026-09-09 0:02 ` sashiko-bot
@ 2026-09-09 18:26 ` Tejun Heo
1 sibling, 0 replies; 8+ messages in thread
From: Tejun Heo @ 2026-09-09 18:26 UTC (permalink / raw)
To: Ziyang Men
Cc: Shuah Khan, Tejun Heo, Johannes Weiner, Michal Koutný,
Jiri Kosina, Benjamin Tissoires, David Vernet, Eduard Zingerman,
Viktor Malik, Andrea Righi, Changwoo Min, Michal Hocko,
Roman Gushchin, Shakeel Butt, Muchun Song, Andrew Morton,
JP Kobryn, Mykola Lysenko, Nathan Chancellor, kernel-team,
linux-kselftest, cgroups, linux-input, sched-ext, linux-mm, bpf,
llvm, linux-kernel, Emil Tsalapatis
Hello, Ziyang.
This is an AI review. The finding below was reproduced with a local BPF
build using the posted lib.bpf.mk.
On Tue, Sep 08, 2026 at 04:55:49PM -0700, Ziyang Men wrote:
> +$(lib_bpf_vmlinux_h): | $(lib_bpf_vmlinux_stamp) ;
Please make the stamp a normal prerequisite, keeping the empty recipe:
$(lib_bpf_vmlinux_h): $(lib_bpf_vmlinux_stamp) ;
Changing a macro in a supplied VMLINUX_H updated vmlinux.h, but the same
build left the BPF object and skeleton unchanged. Disassembly still showed
the old constant. With the normal prerequisite, both rebuilt in one
invocation. Touching the input without changing its contents did not rebuild
either.
Thanks.
--
tejun
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-09-09 18:43 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-08 23:55 [PATCH v4 0/4] selftests: add shared Makefile for BPF selftests and new memcg test Ziyang Men
2026-09-08 23:55 ` [PATCH v4 1/4] selftests: add shared lib.bpf.mk to build BPF progs and skeletons Ziyang Men
2026-09-09 0:02 ` sashiko-bot
2026-09-09 18:26 ` Tejun Heo
2026-09-08 23:55 ` [PATCH v4 2/4] selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush Ziyang Men
2026-09-09 0:05 ` sashiko-bot
2026-09-08 23:55 ` [PATCH v4 3/4] selftests/hid: build the BPF program via the shared lib.bpf.mk Ziyang Men
2026-09-08 23:55 ` [PATCH v4 4/4] selftests/sched_ext: build BPF schedulers " Ziyang Men
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox