Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH v2 0/4] selftests: shared lib.bpf.mk for building BPF progs and skeletons
@ 2026-07-21 17:48 Ziyang Men
  2026-07-21 17:48 ` [PATCH v2 1/4] selftests: add shared lib.bpf.mk to build " Ziyang Men
                   ` (3 more replies)
  0 siblings, 4 replies; 7+ messages in thread
From: Ziyang Men @ 2026-07-21 17:48 UTC (permalink / raw)
  To: Shuah Khan, Tejun Heo, Johannes Weiner, Michal Koutný,
	Jiri Kosina, Benjamin Tissoires, David Vernet, Eduard Zingerman
  Cc: Andrea Righi, Changwoo Min, Michal Hocko, Roman Gushchin,
	Shakeel Butt, Muchun Song, Andrew Morton, JP Kobryn,
	Mykola Lysenko, Nathan Chancellor, linux-kselftest, cgroups,
	linux-input, sched-ext, linux-mm, kernel-team, bpf, llvm,
	linux-kernel, Ziyang Men

This series re-factors the bpf-related toolchain for selftests, which
currently duplicated across selftests/{bpf,sched_ext,hid}/. Unify them
into a single includable fragment, tools/testing/selftests/lib.bpf.mk,
and wires up three consumers. We believe this will simplify the work for
configuraing selftests with the bpf, for both the existing and future ones.

Patch 1 adds lib.bpf.mk: it builds the in-tree libbpf.a and a host
bpftool, generates vmlinux.h from the kernel's BTF, compiles *.bpf.c into
BPF objects and skeletons, and exports the include paths, a bpf_link
helper, and BPF_LDLIBS.  Use can control the behavior by several knobs
(source layout/suffix, extra headers/cflags, skeleton suffix,
subskeletons, output dirs) that can be used commonly in general cases.

Patch 2 adds a new selftest, test_memcg_stat_cross_cpu, under
selftests/cgroup. This test is a memory-cgroup statistics correctness
test that compares the memcg BPF kfuncs against memory.stat over a whole
cgroup subtree charged across many CPUs, and checks the cross-CPU rstat
flush. It is the first in-tree consumer of lib.bpf.mk outside
selftests/bpf.

Patches 3 and 4 convert the duplicated parts in Makefile for two
existing duplicating bpf consumers, the selftests/hid and
selftests/sched_ext, to lib.bpf.mk.  The generated skeletons keep
a byte-identical public API and land in the same locations as before.
The folders build cleanly under both plain make and LLVM=1.

Changes since v1:
  - Generalized lib.bpf.mk (source layout/suffix, extra hdrs/cflags,
    skeleton suffix, subskeletons, configurable output dirs) so it can
    serve hid and sched_ext, not just cgroup.
  - Added patch 3 (hid) and patch 4 (sched_ext), converting those folders
    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     |   6 +
 tools/testing/selftests/cgroup/Makefile       |  21 +
 tools/testing/selftests/cgroup/config         |   4 +
 .../selftests/cgroup/lib/cgroup_util.c        |  48 ++
 .../cgroup/lib/include/cgroup_util.h          |   1 +
 .../cgroup/memcg_stat_cross_cpu.bpf.c         |  86 ++
 .../selftests/cgroup/memcg_stat_cross_cpu.h   |  27 +
 .../cgroup/test_memcg_stat_cross_cpu.c        | 780 ++++++++++++++++++
 tools/testing/selftests/hid/Makefile          | 182 +---
 tools/testing/selftests/lib.bpf.mk            | 247 ++++++
 tools/testing/selftests/sched_ext/Makefile    | 153 +---
 11 files changed, 1275 insertions(+), 280 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


base-commit: 6eb8711ece2ce27e52e327a5b7a628ed39b97f45
-- 
2.53.0-Meta


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

* [PATCH v2 1/4] selftests: add shared lib.bpf.mk to build BPF progs and skeletons
  2026-07-21 17:48 [PATCH v2 0/4] selftests: shared lib.bpf.mk for building BPF progs and skeletons Ziyang Men
@ 2026-07-21 17:48 ` Ziyang Men
  2026-07-21 17:58   ` sashiko-bot
  2026-07-21 17:48 ` [PATCH v2 2/4] selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush Ziyang Men
                   ` (2 subsequent siblings)
  3 siblings, 1 reply; 7+ messages in thread
From: Ziyang Men @ 2026-07-21 17:48 UTC (permalink / raw)
  To: Shuah Khan, Tejun Heo, Johannes Weiner, Michal Koutný,
	Jiri Kosina, Benjamin Tissoires, David Vernet, Eduard Zingerman
  Cc: Andrea Righi, Changwoo Min, Michal Hocko, Roman Gushchin,
	Shakeel Butt, Muchun Song, Andrew Morton, JP Kobryn,
	Mykola Lysenko, Nathan Chancellor, linux-kselftest, cgroups,
	linux-input, sched-ext, linux-mm, kernel-team, bpf, llvm,
	linux-kernel, Ziyang Men

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.

Add tools/testing/selftests/lib.bpf.mk, a single includable fragment that
provides the whole chain end-to-end. It builds the in-tree libbpf.a and a
host bpftool, generates vmlinux.h from the kernel's BTF, compiles *.bpf.c
into BPF objects (clang --target=bpf) and generates their skeletons. It
also provides what the user-space test binary needs to build: the header
search paths (so #includes resolve), a bpf_link macro that wraps the link
command, and BPF_LDLIBS, so the test can be statically linked against
libbpf.a.

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) as prerequisites of the test binary, e.g.,:

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

This saves much work for configuring selftests in other folder, such the cgroup.

net/bpf.mk (which only builds *.bpf.o, without skeleton or vmlinux.h
generation) is left unchanged; replacing the existing duplication
mentioned above is the next step.

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-4-8
Signed-off-by: Ziyang Men <ziyang.meme@gmail.com>
---
 tools/testing/selftests/lib.bpf.mk | 247 +++++++++++++++++++++++++++++
 1 file changed, 247 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..6f175c6568e9
--- /dev/null
+++ b/tools/testing/selftests/lib.bpf.mk
@@ -0,0 +1,247 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# Shared fragment for selftests that compile *.bpf.c into BPF objects +
+# skeletons and link them into userspace test binaries, without each
+# subsystem's Makefile re-implementing the libbpf/bpftool/vmlinux.h machinery.
+#
+# Caller contract (per-test Makefile):
+#
+#     BPF_SRCS         := foo.bpf.c bar.bpf.c
+#     TEST_GEN_PROGS   := foo_test
+#     OVERRIDE_TARGETS := 1              # MUST be set before lib.mk
+#     include ../lib.mk                  # defines OUTPUT, CC, Q, msg,
+#     include ../lib.bpf.mk              #   selfdir, top_srcdir; honours OVERRIDE
+#
+#     $(OUTPUT)/foo_test: foo_test.c $(BPF_SKELS)
+#         $(call bpf_link,$@,$<)
+#
+# Optional knobs (set before including lib.bpf.mk):
+#   BPF_PROG_EXT     - source suffix, default .bpf.c; set to .c for the legacy
+#                      "progs/foo.c -> foo.bpf.o" layout.
+#   BPF_EXTRA_HDRS   - extra prerequisites (headers) for the BPF objects.
+#   BPF_EXTRA_CFLAGS - appended to BPF_CFLAGS for the BPF compile.
+#   BPF_SKEL_EXT     - skeleton suffix, default .skel.h (e.g. .bpf.skel.h).
+#   BPF_GEN_SUBSKEL  - if set, also emit a subskeleton next to each skeleton.
+#   BPF_OBJ_DIR      - dir for generated *.bpf.o (default $(OUTPUT)).
+#   BPF_SKEL_DIR     - dir for generated skeletons (default $(OUTPUT)).
+# A caller with unusual compile needs may override BPF_CFLAGS wholesale after
+# the include (the object recipe expands it lazily).
+# BPF_SRCS entries may live in a subdirectory (e.g. progs/foo.bpf.c); the
+# objects and skeletons are always emitted flat under $(OUTPUT), keyed by the
+# source basename (foo.bpf.o / foo.skel.h).
+#
+# lib.mk MUST be included first: this fragment consumes the vars it defines
+# (OUTPUT, top_srcdir, CC, CLANG, Q, msg) and needs OVERRIDE_TARGETS to have
+# already suppressed lib.mk's default link rule.
+
+include $(top_srcdir)/tools/scripts/Makefile.arch	# ARCH / SRCARCH
+# Pull in the shared toolchain definitions (HOSTCC/HOSTLD/CLANG) so this
+# fragment picks the *same* host compiler the libbpf and bpftool sub-makes
+# will: in particular HOSTCC becomes clang under LLVM=1 (gcc otherwise).
+# Without this the host bpftool and its bootstrap libbpf can end up built with
+# a mix of gcc and clang, which trips clang on gcc-only flags (-Wstrict-aliasing=3).
+#
+# Makefile.include's allow-override resets CC to a bare "clang", which would
+# drop the --target=<arch> flag lib.mk set for LLVM=1 cross builds (make LLVM=1
+# ARCH=arm64).  lib.mk ran first and configured CC for the target, so save it
+# across the include and restore it afterwards.
+lib_bpf_mk_saved_cc := $(CC)
+include $(top_srcdir)/tools/scripts/Makefile.include
+CC := $(lib_bpf_mk_saved_cc)
+
+CLANG  ?= clang
+HOSTCC ?= gcc
+HOSTLD ?= ld
+ifneq ($(V),1)
+submake_extras := feature_display=0
+endif
+
+# ---- paths & tools --------------------------------------------------------
+TOOLSDIR    := $(top_srcdir)/tools
+LIBDIR      := $(TOOLSDIR)/lib
+BPFDIR      := $(LIBDIR)/bpf
+TOOLSINCDIR := $(TOOLSDIR)/include
+BPFTOOLDIR  := $(TOOLSDIR)/bpf/bpftool
+APIDIR      := $(TOOLSINCDIR)/uapi
+
+# Everything generated lives under $(OUTPUT) so O= and in-tree both work and
+# per-test builds (distinct $(OUTPUT)) never collide.
+SCRATCH_DIR := $(OUTPUT)/tools
+BUILD_DIR   := $(SCRATCH_DIR)/build
+INCLUDE_DIR := $(SCRATCH_DIR)/include
+BPFOBJ      := $(BUILD_DIR)/libbpf/libbpf.a
+
+# bpftool must run on the *host*; split the host toolchain out when cross-building.
+ifneq ($(CROSS_COMPILE),)
+HOST_BUILD_DIR   := $(BUILD_DIR)/host
+HOST_SCRATCH_DIR := $(OUTPUT)/host-tools
+else
+HOST_BUILD_DIR   := $(BUILD_DIR)
+HOST_SCRATCH_DIR := $(SCRATCH_DIR)
+endif
+HOST_BPFOBJ     := $(HOST_BUILD_DIR)/libbpf/libbpf.a
+DEFAULT_BPFTOOL := $(HOST_SCRATCH_DIR)/sbin/bpftool
+BPFTOOL         ?= $(DEFAULT_BPFTOOL)
+
+# ---- vmlinux BTF discovery -----------------------------------------------
+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))))
+ifeq ($(VMLINUX_BTF),)
+$(error Cannot find a vmlinux for VMLINUX_BTF at any of "$(VMLINUX_BTF_PATHS)")
+endif
+
+# ---- clang flags ----------------------------------------------------------
+# Clang's default system includes (not the ones seen under --target=bpf); fixes
+# "missing" asm/byteorder.h etc. '-idirafter' so we never shadow real includes.
+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))
+
+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)
+
+# Prefer -mcpu=v3; fall back to v2 on clang too old to know it (probed once).
+CLANG_BPF_CPU := $(shell $(CLANG) --target=bpf -mcpu=help 2>&1 | grep -q 'v3' \
+			 && echo v3 || echo v2)
+
+# -fms-extensions + -Wno-microsoft-anon-tag: required so clang accepts the
+# anonymous nested struct/union members bpftool emits into vmlinux.h.
+BPF_CFLAGS = -g -Wall -Werror -D__TARGET_ARCH_$(SRCARCH) $(MENDIAN)	\
+	     -I$(INCLUDE_DIR) -I$(APIDIR) -I$(TOOLSINCDIR)		\
+	     -std=gnu11							\
+	     -fno-strict-aliasing					\
+	     -fms-extensions -Wno-microsoft-anon-tag			\
+	     -Wno-compare-distinct-pointer-types			\
+	     $(CLANG_SYS_INCLUDES) $(BPF_EXTRA_CFLAGS)
+
+# $1 = src .bpf.c, $2 = dst .bpf.o
+define BPF_BUILD_RULE
+	$(call msg,CLNG-BPF,,$2)
+	$(Q)$(CLANG) $(BPF_CFLAGS) -O2 --target=bpf -mcpu=$(CLANG_BPF_CPU) -c $1 -o $2
+endef
+
+# ---- output dirs for generated objects/skeletons --------------------------
+# Default: flat under $(OUTPUT) (what bpf/, hid/, cgroup/ do).  A caller may
+# segregate the generated files into subdirs, e.g. BPF_SKEL_DIR := $(OUTPUT)/...
+BPF_OBJ_DIR  ?= $(OUTPUT)
+BPF_SKEL_DIR ?= $(OUTPUT)
+
+# ---- scratch dirs ---------------------------------------------------------
+MAKE_DIRS := $(sort $(BUILD_DIR)/libbpf $(HOST_BUILD_DIR)/libbpf		\
+		    $(HOST_BUILD_DIR)/bpftool $(INCLUDE_DIR)		\
+		    $(filter-out $(OUTPUT),$(BPF_OBJ_DIR) $(BPF_SKEL_DIR)))
+$(MAKE_DIRS):
+	$(call msg,MKDIR,,$@)
+	$(Q)mkdir -p $@
+
+# ---- libbpf (target) ------------------------------------------------------
+# Pass ARCH/CROSS_COMPILE/CC through: lib.mk's CC is file-origin and is not
+# exported, so without this the libbpf sub-make would rebuild for the host under
+# a pure-LLVM cross build (make LLVM=1 ARCH=<arch>).  -fPIC keeps the static
+# libbpf linkable into position-independent (PIE) test binaries.
+$(BPFOBJ): $(wildcard $(BPFDIR)/*.[ch] $(BPFDIR)/Makefile) \
+	   $(APIDIR)/linux/bpf.h | $(BUILD_DIR)/libbpf
+	$(Q)$(MAKE) $(submake_extras) -C $(BPFDIR) OUTPUT=$(BUILD_DIR)/libbpf/ \
+		    ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) CC="$(CC)"     \
+		    EXTRA_CFLAGS='-g -O0 -fPIC'				      \
+		    DESTDIR=$(SCRATCH_DIR) prefix= all install_headers
+
+# ---- libbpf (host) -- a distinct rule only when cross-compiling -----------
+ifneq ($(BPFOBJ),$(HOST_BPFOBJ))
+$(HOST_BPFOBJ): $(wildcard $(BPFDIR)/*.[ch] $(BPFDIR)/Makefile) \
+		| $(HOST_BUILD_DIR)/libbpf
+	$(Q)$(MAKE) $(submake_extras) -C $(BPFDIR) ARCH= CROSS_COMPILE=	      \
+		    OUTPUT=$(HOST_BUILD_DIR)/libbpf/ CC=$(HOSTCC) LD=$(HOSTLD) \
+		    EXTRA_CFLAGS='-g -O0'				      \
+		    DESTDIR=$(HOST_SCRATCH_DIR) prefix= all install_headers
+endif
+
+# ---- bpftool (host) -------------------------------------------------------
+$(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
+
+# ---- vmlinux.h ------------------------------------------------------------
+$(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
+
+# ---- BPF objects + skeletons ---------------------------------------------
+# Sources may sit in a subdir and use the modern *.bpf.c or the legacy *.c
+# suffix (BPF_PROG_EXT); objects go in $(BPF_OBJ_DIR) and skeletons in
+# $(BPF_SKEL_DIR) (both default to $(OUTPUT)), keyed by the source basename.
+BPF_PROG_EXT ?= .bpf.c
+bpf_stems := $(patsubst %$(BPF_PROG_EXT),%,$(notdir $(BPF_SRCS)))
+# The output namespace is flat, so two sources with the same basename would
+# collapse into one object/skeleton; fail loudly instead of silently dropping.
+ifneq ($(words $(bpf_stems)),$(words $(sort $(bpf_stems))))
+$(error lib.bpf.mk: BPF_SRCS has colliding basenames: $(BPF_SRCS))
+endif
+# BPF_SKEL_EXT lets a caller pick the skeleton suffix (default .skel.h; e.g.
+# .bpf.skel.h).  With BPF_GEN_SUBSKEL set, a matching subskeleton is emitted
+# alongside (foo.subskel.h / foo.bpf.subskel.h).
+BPF_SKEL_EXT    ?= .skel.h
+BPF_SUBSKEL_EXT := $(patsubst %skel.h,%subskel.h,$(BPF_SKEL_EXT))
+BPF_OBJS  := $(addprefix $(BPF_OBJ_DIR)/,$(addsuffix .bpf.o,$(bpf_stems)))
+BPF_SKELS := $(addprefix $(BPF_SKEL_DIR)/,$(addsuffix $(BPF_SKEL_EXT),$(bpf_stems)))
+
+# Locate the sources wherever the caller keeps them (e.g. progs/).
+vpath %$(BPF_PROG_EXT) $(sort $(dir $(BPF_SRCS)))
+
+$(BPF_OBJS): $(BPF_OBJ_DIR)/%.bpf.o: %$(BPF_PROG_EXT) $(BPF_EXTRA_HDRS) \
+	     $(wildcard *.bpf.h) $(INCLUDE_DIR)/vmlinux.h | $(BPF_OBJ_DIR) $(BPFOBJ)
+	$(call BPF_BUILD_RULE,$<,$@)
+
+$(BPF_SKELS): $(BPF_SKEL_DIR)/%$(BPF_SKEL_EXT): $(BPF_OBJ_DIR)/%.bpf.o $(BPFTOOL) | $(BPF_SKEL_DIR)
+	$(call msg,GEN-SKEL,,$@)
+	$(Q)$(BPFTOOL) gen object $(<:.o=.linked.o) $<
+	$(Q)$(BPFTOOL) gen skeleton $(<:.o=.linked.o) name $(notdir $(<:.bpf.o=)) > $@
+ifneq ($(BPF_GEN_SUBSKEL),)
+	$(Q)$(BPFTOOL) gen subskeleton $(<:.o=.linked.o) name $(notdir $(<:.bpf.o=)) > $(@:$(BPF_SKEL_EXT)=$(BPF_SUBSKEL_EXT))
+endif
+
+# ---- exports consumed by the caller --------------------------------------
+# -I$(OUTPUT)/-I$(BPF_SKEL_DIR): so the test .c can #include "foo.skel.h".
+# -I$(INCLUDE_DIR): so userspace can pull in the generated vmlinux.h if needed.
+CFLAGS += -I$(OUTPUT) -I$(BPF_SKEL_DIR) -I$(INCLUDE_DIR)
+
+# Static libbpf.a first, then its deps. libbpf may pull in zstd (BTF decompress)
+# only when built against it; link -lzstd only if libzstd is present.
+BPF_LDLIBS := $(BPFOBJ) -lelf -lz
+ifneq ($(shell pkg-config --exists libzstd 2>/dev/null && echo y),)
+BPF_LDLIBS += -lzstd
+endif
+
+TEST_GEN_FILES += $(BPF_OBJS)
+
+# Link helper: $1 = output binary, $2 = test .c (skels are the target's deps).
+define bpf_link
+	$(call msg,BINARY,,$1)
+	$(Q)$(CC) $(CFLAGS) $2 $(BPF_LDLIBS) $(LDLIBS) -o $1
+endef
+
+EXTRA_CLEAN += $(SCRATCH_DIR) $(HOST_SCRATCH_DIR)			\
+	       $(addprefix $(BPF_OBJ_DIR)/,*.bpf.o *.linked.o)		\
+	       $(addprefix $(BPF_SKEL_DIR)/,*$(BPF_SKEL_EXT) *$(BPF_SUBSKEL_EXT))
-- 
2.53.0-Meta


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

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

Add a test_progs selftest that verifies the memory-cgroup BPF kfuncs
return values that agree with what userspace reads from cgroupfs, across
a whole cgroup subtree that has been charged on many CPUs.

It complements the existing cgroup_iter_memcg test.  cgroup_iter_memcg
calls the memcg kfuncs on a single cgroup (BPF_CGROUP_ITER_SELF_ONLY) and
only asserts each value is greater than zero, so it never checks that a
value is actually correct.  This test compares the kfunc values against
what userspace reads from memory.stat, and checks every node in the
cgroup tree.

Moreover, cgroup_iter_memcg does not exercise the rstat flush
(mem_cgroup_flush_stats()).  This test does: it launches a process on
each leaf that charges and holds memory across multiple CPUs (so the
leaf's rstat is dirty on K per-cpu trees), then confirms the flush works
for both the BPF and the file path by checking two properties:

  1. after the flush, each leaf's anon is at least what the process
     charged there;

  2. after the flush, the sum of the leaves' charged memory equals the
     amount at the root.

The BPF reader and the file reader run in two separate rounds.  Each
round builds the same cgroup tree from scratch and charges the same
amount of memory, so both rounds start from the same state and the
numbers are comparable.  In detail:

  - round 1 (BPF) builds the subtree, forks one child per leaf that
    charges the leaf across K CPUs and then blocks holding the charge,
    walks the tree with a SEC("iter.s/cgroup") program that flushes the
    subtree at the root and reads each cgroup via the memcg kfuncs
    (bpf_get_mem_cgroup, bpf_mem_cgroup_flush_stats,
    bpf_mem_cgroup_page_state, bpf_mem_cgroup_vm_events,
    bpf_put_mem_cgroup) into a hash map;

  - round 2 (cgroupfs) builds and charges an identical tree the same way,
    then reads every cgroup's memory.stat / memory.current from
    userspace.

The subtests differ in how many CPUs each leaf is charged on (a single
CPU or across K CPUs), and run on two cgroup tree sizes.  The charging
children pin CPUs and there is one per leaf, so the test is registered
serial.

The traditional path reads memory.stat / memory.current through a new
read_cgroup_file() helper added to cgroup_helpers (the read counterpart
of write_cgroup_file).  When the memcg kfuncs are unavailable
(CONFIG_MEMCG=n) the test skips cleanly; the base selftest config now
selects CONFIG_MEMCG=y.

Suggested-by: Shakeel Butt <shakeel.butt@linux.dev>
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Ziyang Men <ziyang.meme@gmail.com>
---
 tools/testing/selftests/cgroup/.gitignore     |   6 +
 tools/testing/selftests/cgroup/Makefile       |  21 +
 tools/testing/selftests/cgroup/config         |   4 +
 .../selftests/cgroup/lib/cgroup_util.c        |  48 ++
 .../cgroup/lib/include/cgroup_util.h          |   1 +
 .../cgroup/memcg_stat_cross_cpu.bpf.c         |  86 ++
 .../selftests/cgroup/memcg_stat_cross_cpu.h   |  27 +
 .../cgroup/test_memcg_stat_cross_cpu.c        | 780 ++++++++++++++++++
 8 files changed, 973 insertions(+)
 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..9e6616af832e 100644
--- a/tools/testing/selftests/cgroup/.gitignore
+++ b/tools/testing/selftests/cgroup/.gitignore
@@ -6,7 +6,13 @@ 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
+*.bpf.o
+*.linked.o
+*.skel.h
diff --git a/tools/testing/selftests/cgroup/Makefile b/tools/testing/selftests/cgroup/Makefile
index e01584c2189a..daebd35aec79 100644
--- a/tools/testing/selftests/cgroup/Makefile
+++ b/tools/testing/selftests/cgroup/Makefile
@@ -14,14 +14,29 @@ TEST_GEN_PROGS += test_freezer
 TEST_GEN_PROGS += test_hugetlb_memcg
 TEST_GEN_PROGS += test_kill
 TEST_GEN_PROGS += test_kmem
+TEST_GEN_PROGS += test_memcg_stat_cross_cpu
 TEST_GEN_PROGS += test_memcontrol
 TEST_GEN_PROGS += test_pids
 TEST_GEN_PROGS += test_zswap
 
 LOCAL_HDRS += $(selfdir)/clone3/clone3_selftests.h $(selfdir)/pidfd/pidfd.h
 
+# test_memcg_stat_cross_cpu builds a BPF program + skeleton through lib.bpf.mk.
+# OVERRIDE_TARGETS suppresses lib.mk's default C link rule (re-supplied below);
+# it must be set before ../lib.mk is included.
+BPF_SRCS         := memcg_stat_cross_cpu.bpf.c
+OVERRIDE_TARGETS := 1
+
 include ../lib.mk
 include lib/libcgroup.mk
+include ../lib.bpf.mk
+
+# Re-supply the default C link rule that OVERRIDE_TARGETS removed, for the plain
+# cgroup tests.  test_memcg_stat_cross_cpu has its own recipe further below.
+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 $@
 
 $(OUTPUT)/test_core: $(LIBCGROUP_O)
 $(OUTPUT)/test_cpu: $(LIBCGROUP_O)
@@ -33,3 +48,9 @@ $(OUTPUT)/test_kmem: $(LIBCGROUP_O)
 $(OUTPUT)/test_memcontrol: $(LIBCGROUP_O)
 $(OUTPUT)/test_pids: $(LIBCGROUP_O)
 $(OUTPUT)/test_zswap: $(LIBCGROUP_O)
+
+# test_memcg_stat_cross_cpu links cgroup_util and the generated BPF skeleton
+# against the in-tree static libbpf that lib.bpf.mk built.
+$(OUTPUT)/test_memcg_stat_cross_cpu: test_memcg_stat_cross_cpu.c \
+					$(BPF_SKELS) $(LIBCGROUP_O)
+	$(call bpf_link,$@,$< $(LIBCGROUP_O))
diff --git a/tools/testing/selftests/cgroup/config b/tools/testing/selftests/cgroup/config
index 39f979690dd3..9457bf604b23 100644
--- a/tools/testing/selftests/cgroup/config
+++ b/tools/testing/selftests/cgroup/config
@@ -4,3 +4,7 @@ CONFIG_CGROUP_FREEZER=y
 CONFIG_CGROUP_SCHED=y
 CONFIG_MEMCG=y
 CONFIG_PAGE_COUNTER=y
+CONFIG_BPF=y
+CONFIG_BPF_SYSCALL=y
+CONFIG_CGROUP_BPF=y
+CONFIG_DEBUG_INFO_BTF=y
diff --git a/tools/testing/selftests/cgroup/lib/cgroup_util.c b/tools/testing/selftests/cgroup/lib/cgroup_util.c
index 2596c12cd864..3a30557855d3 100644
--- a/tools/testing/selftests/cgroup/lib/cgroup_util.c
+++ b/tools/testing/selftests/cgroup/lib/cgroup_util.c
@@ -54,6 +54,54 @@ ssize_t write_text(const char *path, char *buf, ssize_t len)
 	return len < 0 ? -errno : len;
 }
 
+/*
+ * cg_get_id - return the kernfs id of a cgroup directory
+ * @cgroup: absolute path to the cgroup directory
+ *
+ * Returns the cgroup's kernfs node id (cgrp->kn->id) -- the same value the
+ * kernel exposes to BPF as cgrp->kn->id and via bpf_get_current_cgroup_id().
+ * This is obtained from the cgroupfs file handle and is NOT the directory's
+ * st_ino.  Returns 0 (an invalid id) 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 probe call is expected to fail (EOVERFLOW) and report the real
+	 * handle size in fhp->handle_bytes; a cgroupfs handle is always 8 bytes.
+	 */
+	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;
+}
+
 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..832fcefe60a4 100644
--- a/tools/testing/selftests/cgroup/lib/include/cgroup_util.h
+++ b/tools/testing/selftests/cgroup/lib/include/cgroup_util.h
@@ -54,6 +54,7 @@ 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 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..3b8c716e8d01
--- /dev/null
+++ b/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.bpf.c
@@ -0,0 +1,86 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#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";
+
+/*
+ * Per-cgroup results, keyed by cgroup id.  The BPF-side id (cgrp->kn->id)
+ * equals the userspace get_cgroup_id() value, so the test can correlate map
+ * entries back to the cgroups it created.  max_entries is resized by userspace
+ * (bpf_map__set_max_entries) to the size of the subtree 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");
+
+/*
+ * Sleepable cgroup iterator: flush the subtree's rstat once at the root, then
+ * for every cgroup in the walked subtree read a fixed set of memcg statistics
+ * through the memcg kfuncs and stash them in the hash map for userspace to
+ * compare against memory.stat.
+ *
+ * The flush kfunc may sleep, hence SEC("iter.s/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;
+
+	/*
+	 * DESCENDANTS_PRE ends with a terminal element where cgroup == NULL.
+	 * Return 0 (not 1) so the walk runs to completion.
+	 */
+	if (!cgrp)
+		return 0;
+
+	css = &cgrp->self;
+	memcg = bpf_get_mem_cgroup(css);
+	if (!memcg)
+		return 0;
+
+	/*
+	 * Flush once, at the subtree root -- the first element visited in
+	 * DESCENDANTS_PRE order (seq_num == 0).  css_rstat_flush() is
+	 * subtree-wide, so this one flush brings the whole walked subtree
+	 * up to date and every descendant read afterwards is accurate;
+	 * flushing again per-cgroup would only hit the no-op threshold gate.
+	 */
+	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);
+
+	/* page_counter fields need no kfunc; read them off the trusted ptr. */
+	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..1583cafdab7e
--- /dev/null
+++ b/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.h
@@ -0,0 +1,27 @@
+/* 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
+
+/*
+ * One per-cgroup snapshot, produced by the BPF cgroup iterator and read back
+ * from a BPF hash map keyed by cgroup id.  After the charge has quiesced the
+ * test compares every field against what userspace parses from
+ * memory.stat / memory.current / memory.max, so the two must agree.
+ *
+ * Page-state values are in bytes (already unit-scaled by the kernel), so they
+ * compare directly against memory.stat.  usage_pages / max_pages come straight
+ * off the page_counter and are in PAGES.
+ */
+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..673b3da467ba
--- /dev/null
+++ b/tools/testing/selftests/cgroup/test_memcg_stat_cross_cpu.c
@@ -0,0 +1,780 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+/*
+ * memcg_stat_cross_cpu
+ * ====================
+ * A memory-cgroup statistics correctness test.  It compares the memcg
+ * statistics read through the BPF memcg kfuncs against what userspace reads
+ * from memory.stat, over a whole cgroup tree that has been charged across many
+ * CPUs.  Where a plain kfunc smoke test only checks that a single cgroup's
+ * values are non-zero, this test checks the values are actually correct and
+ * that a cross-CPU rstat flush aggregates every per-CPU slice.
+ *
+ * The BPF reader and the file reader are run in two separate rounds, each on
+ * its own freshly built and charged tree:
+ *
+ *   round 1: build the subtree; fork one process per leaf that charges the leaf
+ *            across K CPUs and then blocks holding the charge; walk the tree
+ *            with a SEC("iter.s/cgroup") program that flushes and reads each
+ *            cgroup via the memcg kfuncs into a hash map; record one snapshot
+ *            per node; clean the tree.
+ *   round 2: build and charge an identical tree the same way, then read every
+ *            cgroup's memory.stat / memory.current from userspace.
+ *
+ * The correctness is ensured as follows: each leaf was charged a known amount
+ * of anon spread over K CPUs, so the flushed anon must be at least that amount,
+ * and the subtree root's recursive anon must equal the sum of the leaves' anon.
+ *
+ * The charging children are CPU-pinning and there is one per leaf.
+ */
+#define _GNU_SOURCE
+
+#include <linux/limits.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/mman.h>
+#include <sys/wait.h>
+#include <sys/prctl.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"
+
+static char root[PATH_MAX];	/* cgroup2 mount root */
+static char *subtree_root;	/* <root>/mcg_xcpu, from cg_name() */
+
+struct cg_node {
+	char path[PATH_MAX];
+	__u64 id;
+	bool is_leaf;
+};
+
+/* Field subset parsed from memory.stat / memory.current. */
+struct file_snap {
+	__u64 anon, file, shmem, file_mapped, pgfault;
+	__u64 current;		/* memory.current, bytes */
+	__u64 max;		/* memory.max, bytes (valid unless max_is_max) */
+	bool max_is_max;
+};
+
+static long page_size;
+
+/* ---- allowed CPU set --------------------------------------------------- */
+
+static int *cpu_list; /* ids of the CPUs this process may run on */
+static int n_cpu; /* number of such CPUs */
+
+/*
+ * Collect the CPUs the test is allowed to run on.
+ * Bounded by CPU_SETSIZE (1024).
+ */
+static int collect_cpus(void)
+{
+	cpu_set_t set;
+	int i, want, n = 0;
+
+	CPU_ZERO(&set);
+	if (sched_getaffinity(0, sizeof(set), &set))
+		return -1;
+	want = CPU_COUNT(&set);
+	if (want <= 0)
+		return -1;
+	cpu_list = calloc(want, sizeof(*cpu_list));
+	if (!cpu_list)
+		return -1;
+	for (i = 0; i < CPU_SETSIZE && n < want; i++)
+		if (CPU_ISSET(i, &set))
+			cpu_list[n++] = i;
+	n_cpu = n;
+	return 0;
+}
+
+/* Pin the calling task to a single CPU. */
+static int pin_cpu(int cpu)
+{
+	cpu_set_t set;
+
+	CPU_ZERO(&set);
+	CPU_SET(cpu, &set);
+	return sched_setaffinity(0, sizeof(set), &set);
+}
+
+/* ---- tree construction ------------------------------------------------- */
+
+static struct cg_node *nodes;
+static int n_nodes;
+static int n_leaves;
+
+static int add_node(const char *path, bool is_leaf, int *keep_fd)
+{
+	if (cg_create(path))
+		return -1;
+	if (keep_fd) {
+		*keep_fd = open(path, O_RDONLY);
+		if (*keep_fd < 0)
+			return -1;
+	}
+
+	strncpy(nodes[n_nodes].path, path, sizeof(nodes[n_nodes].path) - 1);
+	nodes[n_nodes].path[sizeof(nodes[n_nodes].path) - 1] = '\0';
+	nodes[n_nodes].id = cg_get_id(path);
+	nodes[n_nodes].is_leaf = is_leaf;
+	if (is_leaf)
+		n_leaves++;
+	n_nodes++;
+	return 0;
+}
+
+/* Recursively create children of @path. @path must already exist and be recorded. */
+static int build_children(const char *path, int fanout, int depth)
+{
+	char child[PATH_MAX];
+	int i;
+
+	if (depth == 0)
+		return 0;
+
+	/* Enable memory on this interior node so its children get 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, depth == 1, NULL))
+			return -1;
+		if (build_children(child, fanout, depth - 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;
+}
+
+/* The tree is arranged in the DFS order within an array */
+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, root_fd))
+		return -1;
+	return build_children(subtree_root, fanout, depth);
+}
+
+/* ---- cross-CPU charge (one pinning child per leaf) --------------------- */
+
+static pid_t *charger_pids;
+static int n_chargers;
+/* parent pid, for the children's PR_SET_PDEATHSIG race check */
+static pid_t test_pid;
+static int charge_ready[2] = { -1, -1 };	/* child -> parent "ready" barrier */
+static int charge_ctrl[2] = { -1, -1 };	/* parent -> child "exit" (close to signal) */
+
+/*
+ * One charging child, dedicated to a single leaf and spread over K CPUs.  It
+ * joins its leaf, maps a resident anon region, then faults the region in K
+ * slices, each on a different CPU, so this leaf's rstat ends up dirty on K
+ * per-cpu trees.  The region stays mapped, so the charge persists while the
+ * parent reads.  After signalling readiness the child blocks (holding the
+ * charge) until the parent closes the control pipe.  Never returns.
+ *
+ * @base is this child's starting index into cpu_list; its K CPUs are
+ * (base + 0..K-1) mod n_cpu.
+ */
+static void charger_child(const struct cg_node *leaf, int base, int k,
+			  size_t resident_bytes)
+{
+	size_t per, off;
+	char *region;
+	char c;
+	int j;
+
+	/*
+	 * If the parent dies without running the cleanup that closes the control
+	 * pipe -- e.g. a CI timeout SIGKILLs the whole test -- ask the kernel to
+	 * SIGKILL this child too, so it can never hang as an orphan holding a
+	 * charge.
+	 */
+	prctl(PR_SET_PDEATHSIG, SIGKILL);
+	if (getppid() != test_pid)
+		_exit(0);
+
+	close(charge_ready[0]);
+	close(charge_ctrl[1]);
+
+	if (cg_enter_current(leaf->path))
+		_exit(1);
+
+	region = mmap(NULL, resident_bytes, PROT_READ | PROT_WRITE,
+		      MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+	if (region == MAP_FAILED)
+		_exit(2);
+
+	/*
+	 * Fault the region in K slices, each on a different CPU, so the charge
+	 * for this leaf is scattered across K per-cpu rstat trees.  A correct
+	 * flush must gather all K slices.
+	 */
+	per = resident_bytes / k;
+	for (j = 0; j < k; j++) {
+		off = (size_t)j * per;
+		if (pin_cpu(cpu_list[(base + j) % n_cpu]))
+			_exit(3);
+		memset(region + off, 1,
+		       (j == k - 1) ? resident_bytes - off : per);
+	}
+
+	/* Ready: the charge is in place and spread across K CPUs. */
+	if (write(charge_ready[1], "x", 1) != 1)
+		_exit(4);
+	close(charge_ready[1]);
+
+	/* Hold the charge (region stays mapped) until the parent tells
+	 * us to exit by closing the control pipe.
+	 */
+	while (read(charge_ctrl[0], &c, 1) > 0)
+		;
+
+	munmap(region, resident_bytes);
+	_exit(0);
+}
+
+/*
+ * Fork one charging child per leaf, each spread over K CPUs.  Returns 0 once
+ * every child has charged its leaf and is holding the charge, so the tree is
+ * under a steady, quiesced load.  On failure the caller's cleanup path calls
+ * stop_chargers().
+ */
+static int start_chargers(int cpus_per_leaf, size_t resident_bytes)
+{
+	int k_eff, i, h = 0;
+
+	k_eff = (cpus_per_leaf > 0 && cpus_per_leaf <= n_cpu) ? cpus_per_leaf :
+								n_cpu;
+
+	if (pipe(charge_ready) || pipe(charge_ctrl)) {
+		ksft_print_msg("pipe: %s\n", strerror(errno));
+		return -1;
+	}
+
+	charger_pids = calloc(n_leaves, sizeof(*charger_pids));
+	if (!charger_pids) {
+		ksft_print_msg("calloc charger_pids failed\n");
+		return -1;
+	}
+
+	/* recorded before the fork so each child can PR_SET_PDEATHSIG against us */
+	test_pid = getpid();
+
+	for (i = 0; i < n_nodes; i++) {
+		pid_t pid;
+
+		if (!nodes[i].is_leaf)
+			continue;
+
+		pid = fork();
+		if (pid < 0) {
+			ksft_print_msg("fork charger: %s\n", strerror(errno));
+			return -1;
+		}
+		if (pid == 0)
+			charger_child(&nodes[i], h * k_eff, k_eff,
+				      resident_bytes);
+
+		charger_pids[n_chargers++] = pid;
+		h++;
+	}
+
+	/* parent: keeps only the ready-read end and the ctrl-write end */
+	close(charge_ready[1]);
+	charge_ready[1] = -1;
+	close(charge_ctrl[0]);
+	charge_ctrl[0] = -1;
+
+	/* wait until every child has charged its leaf and is holding it */
+	for (i = 0; i < n_chargers; i++) {
+		char c;
+		ssize_t r = read(charge_ready[0], &c, 1);
+
+		if (r != 1) {
+			ksft_print_msg("charger exited before ready (setup failed?)\n");
+			return -1;
+		}
+	}
+	return 0;
+}
+
+static void stop_chargers(void)
+{
+	int i, status;
+
+	/* closing the ctrl write end unblocks every child -> they munmap + exit */
+	if (charge_ctrl[1] >= 0) {
+		close(charge_ctrl[1]);
+		charge_ctrl[1] = -1;
+	}
+	if (charge_ctrl[0] >= 0) {
+		close(charge_ctrl[0]);
+		charge_ctrl[0] = -1;
+	}
+	if (charge_ready[0] >= 0) {
+		close(charge_ready[0]);
+		charge_ready[0] = -1;
+	}
+	if (charge_ready[1] >= 0) {
+		close(charge_ready[1]);
+		charge_ready[1] = -1;
+	}
+
+	for (i = 0; i < n_chargers; i++) {
+		if (!charger_pids || charger_pids[i] <= 0)
+			continue;
+		if (waitpid(charger_pids[i], &status, 0) == charger_pids[i] &&
+		    (!WIFEXITED(status) || WEXITSTATUS(status) != 0))
+			ksft_print_msg("charger %d exited abnormally (status=0x%x)\n",
+				       charger_pids[i], status);
+	}
+
+	free(charger_pids);
+	charger_pids = NULL;
+	n_chargers = 0;
+}
+
+/* ---- file (traditional) reader ----------------------------------------- */
+
+static void parse_stat(char *buf, struct file_snap *o)
+{
+	char *save, *line;
+
+	for (line = strtok_r(buf, "\n", &save); line;
+	     line = strtok_r(NULL, "\n", &save)) {
+		unsigned long long val;
+		char name[64];
+
+		if (sscanf(line, "%63s %llu", name, &val) != 2)
+			continue;
+		if (!strcmp(name, "anon"))
+			o->anon = val;
+		else if (!strcmp(name, "file"))
+			o->file = val;
+		else if (!strcmp(name, "shmem"))
+			o->shmem = val;
+		else if (!strcmp(name, "file_mapped"))
+			o->file_mapped = val;
+		else if (!strcmp(name, "pgfault"))
+			o->pgfault = val;
+	}
+}
+
+static int file_read_node(const char *path, struct file_snap *o)
+{
+	char buf[8192];
+
+	memset(o, 0, sizeof(*o));
+
+	if (cg_read(path, "memory.stat", buf, sizeof(buf)))
+		return -1;
+	parse_stat(buf, o);
+
+	if (!cg_read(path, "memory.current", buf, sizeof(buf)))
+		o->current = strtoull(buf, NULL, 10);
+	if (!cg_read(path, "memory.max", buf, sizeof(buf))) {
+		if (!strncmp(buf, "max", 3))
+			o->max_is_max = true;
+		else
+			o->max = strtoull(buf, NULL, 10);
+	}
+	return 0;
+}
+
+/* ---- BPF reader -------------------------------------------------------- */
+
+static int bpf_walk_once(struct bpf_link *link)
+{
+	char buf[4096];
+	ssize_t r;
+	int fd;
+
+	fd = bpf_iter_create(bpf_link__fd(link));
+	if (fd < 0)
+		return -1;
+	while ((r = read(fd, buf, sizeof(buf))) > 0)
+		;
+	close(fd);
+	return r == 0 ? 0 : -1;
+}
+
+/* ---- correctness comparison -------------------------------------------- */
+
+static bool close_enough(__u64 a, __u64 b, __u64 tol)
+{
+	return (a > b ? a - b : b - a) <= tol;
+}
+
+/* Dump one node's bpf-vs-file stats; called when a mismatch is detected. */
+static void dump_node(int i, const struct memcg_stat_snapshot *b,
+		      const struct file_snap *f)
+{
+	ksft_print_msg("node %d bpf : anon=%llu file=%llu shmem=%llu fmapped=%llu pgfault=%llu\n",
+		       i, b->anon, b->file, b->shmem, b->file_mapped, b->pgfault);
+	ksft_print_msg("node %d file: anon=%llu file=%llu shmem=%llu fmapped=%llu pgfault=%llu\n",
+		       i, f->anon, f->file, f->shmem, f->file_mapped, f->pgfault);
+}
+
+/*
+ * Compare the BPF kfunc snapshots (round 1) against the memory.stat values
+ * (round 2), node by node.  The two rounds are independent, equivalently
+ * charged trees, so the flushed stats are compared within a small tolerance
+ * that absorbs per-round overhead (i.e., a charging child's own stack pages).
+ * A wrong unit, enum or field in the kfunc path would miss by far more.
+ *
+ * Two per-round checks verify the flush itself: each leaf was charged
+ * resident_bytes of anon spread over K CPUs, so its flushed anon must be at
+ * least that; and the root's recursive anon must equal the sum of the leaves'
+ * anon (rstat propagated the charge up the tree).
+ *
+ * Returns 0 if every check passes, -1 otherwise.
+ */
+static int check_correctness(const struct memcg_stat_snapshot *bpf,
+			     const struct file_snap *file, const bool *is_leaf,
+			     int n, size_t resident_bytes)
+{
+	__u64 stat_tol = 64 * page_size;
+	__u64 pgf_tol = 1024;
+	__u64 broot = 0, bsum = 0, froot = 0, fsum = 0;
+	int i, mism = 0, flush_bad = 0;
+
+	for (i = 0; i < n; i++) {
+		const struct memcg_stat_snapshot *b = &bpf[i];
+		const struct file_snap *f = &file[i];
+		__u64 bcur = b->usage_pages * page_size;
+
+		/* kfunc path (round 1) compared with memory.stat path (round 2) */
+		if (!close_enough(b->anon, f->anon, stat_tol) ||
+		    !close_enough(b->file, f->file, stat_tol) ||
+		    !close_enough(b->shmem, f->shmem, stat_tol) ||
+		    !close_enough(b->file_mapped, f->file_mapped, stat_tol) ||
+		    !close_enough(b->pgfault, f->pgfault, pgf_tol)) {
+			mism++;
+			dump_node(i, b, f);
+		}
+
+		/* memory.current is live (no flush) and must bound the flushed anon */
+		if (b->anon == 0 || b->anon > bcur) {
+			flush_bad++;
+			ksft_print_msg("node %d: anon=%llu exceeds current=%llu\n",
+				       i, b->anon, bcur);
+		}
+
+		/* each leaf's flush must have gathered the full cross-CPU charge */
+		if (is_leaf[i]) {
+			if (b->anon < resident_bytes || f->anon < resident_bytes) {
+				flush_bad++;
+				ksft_print_msg("node %d: short flush bpf=%llu file=%llu\n",
+					       i, b->anon, f->anon);
+			}
+			bsum += b->anon;
+			fsum += f->anon;
+		}
+		if (i == 0) { /* nodes[0] == subtree_root */
+			broot = b->anon;
+			froot = f->anon;
+		}
+	}
+
+	if (mism) {
+		ksft_print_msg("bpf (round 1) disagrees with memory.stat (round 2)\n");
+		return -1;
+	}
+	if (flush_bad) {
+		ksft_print_msg("flush did not aggregate the cross-cpu charge\n");
+		return -1;
+	}
+	if (broot != bsum || froot != fsum) {
+		ksft_print_msg("root anon != sum of leaf anon: bpf %llu/%llu file %llu/%llu\n",
+			       broot, bsum, froot, fsum);
+		return -1;
+	}
+	if (bsum == 0) {
+		ksft_print_msg("tree carries no anon\n");
+		return -1;
+	}
+	return 0;
+}
+
+/* ---- one case ---------------------------------------------------------- */
+
+struct testcase {
+	const char *name;
+	int fanout;
+	int depth;
+	int cpus_per_leaf; /* K: CPUs each leaf is charged on; 0 = all CPUs */
+	size_t resident_bytes; /* anon charged per leaf */
+};
+
+/*
+ * Remove the subtree in reverse creation order.  Nodes are recorded in DFS
+ * order (a parent precedes all its descendants), so iterating backwards
+ * removes every child before its parent.
+ */
+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;
+}
+
+/*
+ * Round 1: build and charge a fresh tree, walk it with the BPF iterator (which
+ * flushes and reads each cgroup via the memcg kfuncs), and capture one snapshot
+ * per node into @snap.  @is_leaf records the tree shape so the later comparison
+ * can run after the tree is gone.  Returns the node count, or -1 on failure.
+ * The tree is always torn down before returning.
+ */
+static int capture_bpf_round(const struct testcase *tc,
+			     struct memcg_stat_snapshot *snap, bool *is_leaf)
+{
+	struct memcg_stat_cross_cpu *skel = NULL;
+	struct bpf_link *link = NULL;
+	int root_fd = -1, ret = -1, i, mfd;
+
+	if (build_tree(tc->fanout, tc->depth, &root_fd)) {
+		ksft_print_msg("build tree (bpf) failed\n");
+		goto out;
+	}
+	if (start_chargers(tc->cpus_per_leaf, tc->resident_bytes))
+		goto out;
+
+	skel = memcg_stat_cross_cpu__open();
+	if (!skel) {
+		ksft_print_msg("skel open failed\n");
+		goto out;
+	}
+	if (bpf_map__set_max_entries(skel->maps.results, n_nodes + 8)) {
+		ksft_print_msg("set max_entries failed\n");
+		goto out;
+	}
+	if (memcg_stat_cross_cpu__load(skel)) {
+		ksft_print_msg("skel load failed\n");
+		goto out;
+	}
+
+	DECLARE_LIBBPF_OPTS(bpf_iter_attach_opts, opts);
+	union bpf_iter_link_info linfo = {};
+
+	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);
+	if (!link) {
+		ksft_print_msg("attach iter failed\n");
+		goto out;
+	}
+
+	/* bpf walk through the cgroup tree and fetch result */
+	if (bpf_walk_once(link)) {
+		ksft_print_msg("bpf walk failed\n");
+		goto out;
+	}
+
+	mfd = bpf_map__fd(skel->maps.results);
+	for (i = 0; i < n_nodes; i++) {
+		/* Save the position for the leaf, used for later correctness check */
+		is_leaf[i] = nodes[i].is_leaf;
+		if (bpf_map_lookup_elem(mfd, &nodes[i].id, &snap[i])) {
+			ksft_print_msg("map lookup failed for node %d\n", i);
+			goto out;
+		}
+	}
+	ret = n_nodes;
+out:
+	bpf_link__destroy(link);
+	memcg_stat_cross_cpu__destroy(skel);
+	if (root_fd >= 0)
+		close(root_fd);
+	stop_chargers();
+	destroy_tree();
+	return ret;
+}
+
+/*
+ * Round 2: build and charge an identical fresh tree, then read every cgroup via
+ * memory.stat / memory.current.  The tree is reset after bpf read, so each
+ * read does a real rstat flush.  Returns the node count, or -1 on failure.
+ * The tree is always clear before returning.
+ */
+static int capture_file_round(const struct testcase *tc, struct file_snap *snap)
+{
+	int root_fd = -1, ret = -1, i;
+
+	if (build_tree(tc->fanout, tc->depth, &root_fd)) {
+		ksft_print_msg("build tree (file) failed\n");
+		goto out;
+	}
+	if (start_chargers(tc->cpus_per_leaf, tc->resident_bytes))
+		goto out;
+
+	for (i = 0; i < n_nodes; i++)
+		if (file_read_node(nodes[i].path, &snap[i])) {
+			ksft_print_msg("file read failed for node %d\n", i);
+			goto out;
+		}
+	ret = n_nodes;
+out:
+	if (root_fd >= 0)
+		close(root_fd);
+	stop_chargers();
+	destroy_tree();
+	return ret;
+}
+
+static int run_case(const struct testcase *tc)
+{
+	struct memcg_stat_snapshot *bpf = NULL;
+	struct file_snap *file = NULL;
+	bool *is_leaf = NULL;
+	size_t cap = tree_capacity(tc->fanout, tc->depth);
+	int nb, nf, ret = KSFT_FAIL;
+
+	bpf = calloc(cap, sizeof(*bpf));
+	file = calloc(cap, sizeof(*file));
+	is_leaf = calloc(cap, sizeof(*is_leaf));
+	if (!bpf || !file || !is_leaf) {
+		ksft_print_msg("calloc failed\n");
+		goto out;
+	}
+
+	ksft_print_msg("%s: fanout=%d depth=%d cpus=%d/%d resident=%zuKB/leaf\n",
+		       tc->name, tc->fanout, tc->depth, n_cpu, tc->cpus_per_leaf,
+		       tc->resident_bytes >> 10);
+
+	/* round 1: BPF reader flushes and reads its own charged tree */
+	nb = capture_bpf_round(tc, bpf, is_leaf);
+	if (nb < 0)
+		goto out;
+
+	/* round 2: file reader flushes and reads a fresh, equivalent tree */
+	nf = capture_file_round(tc, file);
+	if (nf < 0)
+		goto out;
+
+	if (nb != nf) {
+		ksft_print_msg("node count differs between rounds: %d vs %d\n",
+			       nb, nf);
+		goto out;
+	}
+
+	if (!check_correctness(bpf, file, is_leaf, nb, tc->resident_bytes))
+		ret = KSFT_PASS;
+out:
+	free(bpf);
+	free(file);
+	free(is_leaf);
+	return ret;
+}
+
+static const struct testcase cases[] = {
+	/* name, fan, depth, K, resident anon */
+	{ "single_cpu_small_tree", 4, 2, 1, 2 << 20 },
+	{ "cross_cpu_small_tree", 4, 2, 0, 2 << 20 },
+	{ "single_cpu_large_tree", 10, 3, 1, 256 << 10 },
+	{ "cross_cpu_large_tree", 10, 3, 0, 256 << 10 },
+};
+
+static bool memcg_kfuncs_available(void)
+{
+	struct btf *btf;
+	bool ok;
+
+	btf = btf__load_vmlinux_btf();
+	if (!btf)
+		return false;
+	ok = btf__find_by_name_kind(btf, "bpf_get_mem_cgroup", BTF_KIND_FUNC) > 0;
+	btf__free(btf);
+	return ok;
+}
+
+int main(int argc, char **argv)
+{
+	int i;
+
+	ksft_print_header();
+	ksft_set_plan(ARRAY_SIZE(cases));
+
+	/* Feature gate first: a read-only BTF probe, no privilege needed. */
+	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");
+
+	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");
+
+	if (collect_cpus())
+		ksft_exit_skip("cannot read CPU affinity\n");
+
+	page_size = sysconf(_SC_PAGESIZE);
+	subtree_root = cg_name(root, SUBTREE_NAME);
+	if (!subtree_root)
+		ksft_exit_skip("cannot build subtree root path\n");
+
+	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(cpu_list);
+	cpu_list = NULL;
+	n_cpu = 0;
+
+	ksft_finished();
+}
-- 
2.53.0-Meta


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

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

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.

hid keeps the legacy progs/<name>.c layout, so it sets BPF_PROG_EXT := .c
and passes its shared BPF headers through BPF_EXTRA_HDRS.  It also gains
the fragment's -Wall on the BPF compile, which its old Makefile did not
set; hid.c has a few pre-existing unused locals, so
BPF_EXTRA_CFLAGS := -Wno-unused-variable keeps the previous behaviour
without touching the program source.

The conversion drops two pieces of dead machinery hid had copied from
selftests/bpf: a resolve_btfids build that was never a prerequisite of
any target, and a CLANG_CFLAGS variable that was never passed to any
compile.

No functional change: the generated hid.skel.h public API is
byte-identical before and after, hid_bpf and hidraw are built and linked
the same way, and the folder builds cleanly under both plain make and
LLVM=1.

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-4-8
Signed-off-by: Ziyang Men <ziyang.meme@gmail.com>
---
 tools/testing/selftests/hid/Makefile | 182 +++------------------------
 1 file changed, 20 insertions(+), 162 deletions(-)

diff --git a/tools/testing/selftests/hid/Makefile b/tools/testing/selftests/hid/Makefile
index 2f423de83147..48def484703c 100644
--- a/tools/testing/selftests/hid/Makefile
+++ b/tools/testing/selftests/hid/Makefile
@@ -58,172 +58,31 @@ override define CLEAN
 	$(Q)$(RM) -r $(EXTRA_CLEAN)
 endef
 
-include ../lib.mk
+# The BPF program (progs/hid.c) is compiled into a skeleton by the shared
+# tools/testing/selftests/lib.bpf.mk fragment.  hid keeps the legacy
+# progs/<name>.c layout, so point BPF_PROG_EXT at .c and hand the fragment
+# hid's shared BPF headers as extra prerequisites.  BPF_EXTRA_HDRS is deferred
+# because it references $(BPFDIR), which lib.bpf.mk defines.
+BPF_SRCS       := progs/hid.c
+BPF_PROG_EXT   := .c
+BPF_EXTRA_HDRS  = $(wildcard progs/*.h) $(wildcard $(BPFDIR)/hid_bpf_*.h) \
+		  $(wildcard $(BPFDIR)/*.bpf.h)
+# hid.c predates the shared -Wall (hid's old Makefile didn't set it) and has a
+# few unused locals; tolerate that pre-existing warning without touching the
+# program source.
+BPF_EXTRA_CFLAGS := -Wno-unused-variable
 
-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
+include ../lib.mk
+include ../lib.bpf.mk
 
-# Define simple and short `make test_progs`, `make test_sysctl`, etc targets
-# to build individual tests.
+# Define simple and short `make hid_bpf`, `make hidraw` targets.
 # 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 $@
-
-DEFAULT_BPFTOOL := $(HOST_SCRATCH_DIR)/sbin/bpftool
-
-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)
-
-$(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=)) > $@
+# Each test binary links against the in-tree static libbpf that lib.bpf.mk
+# built (lib.mk has already prefixed TEST_GEN_PROGS with $(OUTPUT)/).
+$(TEST_GEN_PROGS): $(BPFOBJ)
 
 $(OUTPUT)/%.o: %.c $(BPF_SKELS) hid_common.h
 	$(call msg,CC,,$@)
@@ -233,5 +92,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 += feature bpftool $(addprefix $(OUTPUT)/,*.o no_alu32)
-- 
2.53.0-Meta


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

* [PATCH v2 4/4] selftests/sched_ext: build BPF schedulers via the shared lib.bpf.mk
  2026-07-21 17:48 [PATCH v2 0/4] selftests: shared lib.bpf.mk for building BPF progs and skeletons Ziyang Men
                   ` (2 preceding siblings ...)
  2026-07-21 17:48 ` [PATCH v2 3/4] selftests/hid: build the BPF program via the shared lib.bpf.mk Ziyang Men
@ 2026-07-21 17:48 ` Ziyang Men
  3 siblings, 0 replies; 7+ messages in thread
From: Ziyang Men @ 2026-07-21 17:48 UTC (permalink / raw)
  To: Shuah Khan, Tejun Heo, Johannes Weiner, Michal Koutný,
	Jiri Kosina, Benjamin Tissoires, David Vernet, Eduard Zingerman
  Cc: Andrea Righi, Changwoo Min, Michal Hocko, Roman Gushchin,
	Shakeel Butt, Muchun Song, Andrew Morton, JP Kobryn,
	Mykola Lysenko, Nathan Chancellor, linux-kselftest, cgroups,
	linux-input, sched-ext, linux-mm, kernel-team, bpf, llvm,
	linux-kernel, Ziyang Men

sched_ext carried 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.

sched_ext emits a skeleton and a subskeleton per scheduler with a
.bpf.skel.h suffix, so it sets BPF_SKEL_EXT := .bpf.skel.h and
BPF_GEN_SUBSKEL := 1.  Its BPF programs need scheduler-specific include
paths and are not -Werror clean, so it overrides BPF_CFLAGS wholesale
after the include; the userspace runner/testcase rules are kept and
repointed at the fragment's flat $(OUTPUT) skeletons.

No functional change: all 28 skeletons and 28 subskeletons keep a
byte-identical public API before and after, the runner builds and links
the same way, and the folder builds cleanly under both plain make and
LLVM=1.

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-4-8
Signed-off-by: Ziyang Men <ziyang.meme@gmail.com>
---
 tools/testing/selftests/sched_ext/Makefile | 153 +++++----------------
 1 file changed, 35 insertions(+), 118 deletions(-)

diff --git a/tools/testing/selftests/sched_ext/Makefile b/tools/testing/selftests/sched_ext/Makefile
index 5d2dffca0e91..67ff96bf34e8 100644
--- a/tools/testing/selftests/sched_ext/Makefile
+++ b/tools/testing/selftests/sched_ext/Makefile
@@ -14,48 +14,36 @@ 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
-
-BPFTOOL ?= $(DEFAULT_BPFTOOL)
+# The BPF schedulers are compiled into skeletons + subskeletons by the shared
+# lib.bpf.mk fragment.  They use the modern *.bpf.c layout with a .bpf.skel.h
+# suffix and need subskeletons.
+BPF_SRCS        := $(wildcard *.bpf.c)
+BPF_SKEL_EXT    := .bpf.skel.h
+BPF_GEN_SUBSKEL := 1
+BPF_EXTRA_HDRS   = $(wildcard $(CURDIR)/include/scx/*.h) \
+		   $(wildcard $(CURDIR)/include/*.h)
+# Keep generated files under build/ as before (skeletons in build/include,
+# objects in build/obj) rather than flat in the source dir.  lib.bpf.mk creates
+# these dirs; SCXOBJ_DIR (userspace objects) reuses the BPF object dir.
+BPF_OBJ_DIR  := $(OUTPUT)/build/obj/sched_ext
+BPF_SKEL_DIR := $(OUTPUT)/build/include
+SCXOBJ_DIR   := $(BPF_OBJ_DIR)
+
+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 +52,29 @@ 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)		\
+# The BPF schedulers need scheduler-specific includes and are not -Werror
+# clean, so override the shared BPF_CFLAGS wholesale (lib.bpf.mk expands it
+# lazily in the object recipe; -O2/--target/-mcpu are added by the rule).
+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
+	     -fms-extensions
 
-$(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 $@
-
-$(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
+# 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.
-all_test_bpfprogs := $(foreach prog,$(wildcard *.bpf.c),$(INCLUDE_DIR)/$(patsubst %.c,%.skel.h,$(prog)))
+all_test_bpfprogs := $(BPF_SKELS)
 
 auto-test-targets :=			\
 	create_dsq			\
@@ -195,7 +110,8 @@ 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.
@@ -204,15 +120,16 @@ $(SCXOBJ_DIR)/runner.o: runner.c | $(SCXOBJ_DIR) $(BPFOBJ)
 # 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] 7+ messages in thread

* Re: [PATCH v2 1/4] selftests: add shared lib.bpf.mk to build BPF progs and skeletons
  2026-07-21 17:48 ` [PATCH v2 1/4] selftests: add shared lib.bpf.mk to build " Ziyang Men
@ 2026-07-21 17:58   ` sashiko-bot
  0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-07-21 17:58 UTC (permalink / raw)
  To: Ziyang Men
  Cc: sched-ext, mkoutny, linux-input, dmitry.torokhov, bpf, hannes, tj

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] The `bpf_link` macro drops `LDFLAGS` and `CPPFLAGS` when linking user-space test binaries.
- [Medium] Hardcoded `pkg-config` breaks target feature detection for `libzstd` when cross-compiling.
- [Medium] Missing compiler-generated dependency tracking for BPF objects leads to stale builds.
- [Medium] The documented caller contract omits `$(BPFOBJ)` as a prerequisite for test binaries, breaking incremental relinks.
--

--- Patch [1]: [PATCH v2 1/4] selftests: add shared lib.bpf.mk to build BPF progs and skeletons ---
commit 5037b1678e6c542784cb0b94b1da28ba597740a3
Author: Ziyang Men <ziyang.meme@gmail.com>

selftests: add shared lib.bpf.mk to build BPF progs and skeletons
    
This patch introduces tools/testing/selftests/lib.bpf.mk, a shared Makefile
fragment to standardize building BPF programs, skeletons, and the libbpf
toolchain. It provides a common configuration and a bpf_link macro for
test binaries to easily link against libbpf.

> diff --git a/tools/testing/selftests/lib.bpf.mk b/tools/testing/selftests/lib.bpf.mk
> new file mode 100644
> --- /dev/null
> +++ b/tools/testing/selftests/lib.bpf.mk
> @@ -0,0 +1,247 @@
> +# SPDX-License-Identifier: GPL-2.0
> +#
> +# Shared fragment for selftests that compile *.bpf.c into BPF objects +
> +# skeletons and link them into userspace test binaries, without each
> +# subsystem's Makefile re-implementing the libbpf/bpftool/vmlinux.h machinery.
> +#
> +# Caller contract (per-test Makefile):
> +#
> +#     BPF_SRCS         := foo.bpf.c bar.bpf.c
> +#     TEST_GEN_PROGS   := foo_test
> +#     OVERRIDE_TARGETS := 1              # MUST be set before lib.mk
> +#     include ../lib.mk                  # defines OUTPUT, CC, Q, msg,
> +#     include ../lib.bpf.mk              #   selfdir, top_srcdir; honours OVERRIDE
> +#
> +#     $(OUTPUT)/foo_test: foo_test.c $(BPF_SKELS)
> +#         $(call bpf_link,$@,$<)

[Severity: Medium]
Should $(BPFOBJ) be included as a prerequisite in this caller contract
template? If a user follows this example and libbpf.a is rebuilt, make
will not relink the test binaries that depend on it.

[ ... ]

> +# Locate the sources wherever the caller keeps them (e.g. progs/).
> +vpath %$(BPF_PROG_EXT) $(sort $(dir $(BPF_SRCS)))
> +
> +$(BPF_OBJS): $(BPF_OBJ_DIR)/%.bpf.o: %$(BPF_PROG_EXT) $(BPF_EXTRA_HDRS) \
> +	     $(wildcard *.bpf.h) $(INCLUDE_DIR)/vmlinux.h | $(BPF_OBJ_DIR) $(BPFOBJ)
> +	$(call BPF_BUILD_RULE,$<,$@)

[Severity: Medium]
Will relying on this wildcard approach fail to track included headers if they
are located alongside sources in subdirectories like progs/?

Should this use compiler-generated dependency tracking (-MD/-MMD) instead
so developers don't end up testing stale code?

[ ... ]

> +# Static libbpf.a first, then its deps. libbpf may pull in zstd (BTF decompress)
> +# only when built against it; link -lzstd only if libzstd is present.
> +BPF_LDLIBS := $(BPFOBJ) -lelf -lz
> +ifneq ($(shell pkg-config --exists libzstd 2>/dev/null && echo y),)
> +BPF_LDLIBS += -lzstd
> +endif

[Severity: Medium]
Can this hardcoded pkg-config break target feature detection when
cross-compiling? 

This queries the host's pkg-config environment instead of the target's
sysroot, which might cause link errors if libzstd availability differs
between the host and target. Should it use $(PKG_CONFIG) instead?

[ ... ]

> +TEST_GEN_FILES += $(BPF_OBJS)
> +
> +# Link helper: $1 = output binary, $2 = test .c (skels are the target's deps).
> +define bpf_link
> +	$(call msg,BINARY,,$1)
> +	$(Q)$(CC) $(CFLAGS) $2 $(BPF_LDLIBS) $(LDLIBS) -o $1
> +endef

[Severity: Medium]
Does this compilation command drop $(LDFLAGS) and $(CPPFLAGS)? 

These are normally included in lib.mk's default link rule. Skipping them
might ignore user-specified flags like static linking, RPATHs, or sanitizers.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260721174833.1232771-1-ziyang.meme@gmail.com?part=1

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

* Re: [PATCH v2 2/4] selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush
  2026-07-21 17:48 ` [PATCH v2 2/4] selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush Ziyang Men
@ 2026-07-21 18:01   ` sashiko-bot
  0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-07-21 18:01 UTC (permalink / raw)
  To: Ziyang Men
  Cc: sched-ext, tj, hannes, bpf, dmitry.torokhov, mkoutny, linux-input

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

New issues:
- [Medium] Cross-CPU charge scattering fails to trigger page faults on all K CPUs when resident_bytes / k < PAGE_SIZE.
- [Medium] Commit message contains inaccurate claims regarding the test framework and utility helpers.
- [Low] Multi-line comment does not follow the required BPF subsystem style.

Pre-existing issues:
- [High] Missing bounds checks for negative enum values in kfuncs bpf_mem_cgroup_vm_events and bpf_mem_cgroup_memory_events allow out-of-bounds array access.
--

--- Patch [2]: [PATCH v2 2/4] selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush ---
commit c0bc72e5d098a9cdecf85dcaee9fd0bb8671e354
Author: Ziyang Men <ziyang.meme@gmail.com>

selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush

This commit adds a memory-cgroup BPF selftest that verifies if the
memory-cgroup BPF kfuncs return values that match what userspace reads
from cgroupfs. It builds a cgroup tree, charges memory across multiple
CPUs, and compares the flushed kfunc stats to the traditional memory.stat
reads.

> Add a test_progs selftest that verifies the memory-cgroup BPF kfuncs
[ ... ]
> The traditional path reads memory.stat / memory.current through a new
> read_cgroup_file() helper added to cgroup_helpers (the read counterpart
> of write_cgroup_file).

[Severity: Medium]
The commit message states that a test_progs selftest was added and a new
read_cgroup_file() helper was introduced. However, the patch implements a
standalone kselftest via TEST_GEN_PROGS and relies on the pre-existing
cg_read() helper instead. Could the commit message be updated to accurately
reflect the implemented code?

[ ... ]
> diff --git a/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.bpf.c b/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.bpf.c
[ ... ]
> +	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);

[Severity: High]
This is a pre-existing issue, but the called kfuncs
bpf_mem_cgroup_vm_events() and bpf_mem_cgroup_memory_events() in
mm/bpf_memcontrol.c lack negative bounds checks for their enum arguments.

Since the BPF verifier does not enforce enum ranges, a BPF program could
pass a negative enum value which bypasses the upper-bound check (e.g.,
idx >= NR_VM_EVENT_ITEMS). This happens because the C code evaluates the
signed integer, leading to an out-of-bounds array access.

Can we add the required negative bounds checks to these kfuncs?

> +
> +	/* page_counter fields need no kfunc; read them off the trusted ptr. */
[ ... ]
> diff --git a/tools/testing/selftests/cgroup/test_memcg_stat_cross_cpu.c b/tools/testing/selftests/cgroup/test_memcg_stat_cross_cpu.c
[ ... ]
> +	/*
> +	 * Fault the region in K slices, each on a different CPU, so the charge
> +	 * for this leaf is scattered across K per-cpu rstat trees.  A correct
> +	 * flush must gather all K slices.
> +	 */
> +	per = resident_bytes / k;
> +	for (j = 0; j < k; j++) {
> +		off = (size_t)j * per;
> +		if (pin_cpu(cpu_list[(base + j) % n_cpu]))
> +			_exit(3);
> +		memset(region + off, 1,
> +		       (j == k - 1) ? resident_bytes - off : per);
> +	}

[Severity: Medium]
If the system has a large number of CPUs, can per (resident_bytes / k)
become smaller than the page size?

For example, in the cross_cpu_large_tree test case with 256KB
resident_bytes and >64 CPUs, multiple CPUs would be assigned offsets
within the same 4KB page. The first CPU writing to the page causes a page
fault, but subsequent CPUs writing to the same page will find it already
mapped and won't trigger faults, defeating the cross-CPU scattering
mechanism.

> +
> +	/* Ready: the charge is in place and spread across K CPUs. */
> +	if (write(charge_ready[1], "x", 1) != 1)
> +		_exit(4);
> +	close(charge_ready[1]);
> +
> +	/* Hold the charge (region stays mapped) until the parent tells
> +	 * us to exit by closing the control pipe.
> +	 */

[Severity: Low]
Does this multi-line comment format match the BPF subsystem style? The BPF
subsystem requires multi-line comments to have the opening /* on its own
line, even in selftests.

> +	while (read(charge_ctrl[0], &c, 1) > 0)
> +		;

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260721174833.1232771-1-ziyang.meme@gmail.com?part=2

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

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

Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-21 17:48 [PATCH v2 0/4] selftests: shared lib.bpf.mk for building BPF progs and skeletons Ziyang Men
2026-07-21 17:48 ` [PATCH v2 1/4] selftests: add shared lib.bpf.mk to build " Ziyang Men
2026-07-21 17:58   ` sashiko-bot
2026-07-21 17:48 ` [PATCH v2 2/4] selftests/cgroup: add memcg_stat_cross_cpu correctness test for flush Ziyang Men
2026-07-21 18:01   ` sashiko-bot
2026-07-21 17:48 ` [PATCH v2 3/4] selftests/hid: build the BPF program via the shared lib.bpf.mk Ziyang Men
2026-07-21 17:48 ` [PATCH v2 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