From 00e05495c5727b0f7027ad6a2e235da62357d957 Mon Sep 17 00:00:00 2001 From: Sohil Mehta Date: Mon, 6 Apr 2026 18:42:26 -0700 Subject: scripts/x86/intel: Add a script to update the old microcode list The kernel maintains a table of minimum expected microcode revisions for Intel CPUs in intel-ucode-defs.h. Systems with microcode older than these revisions are flagged with X86_BUG_OLD_MICROCODE. The static list of microcode revisions needs to be updated periodically in response to releases of the official microcode at: https://github.com/intel/Intel-Linux-Processor-Microcode-Data-Files.git. Introduce a simple script to extract the revision information from the microcode files and print it in the precise format expected by the microcode header. Maintaining the script in the kernel tree ensures a central location that a submitter can use to generate the kernel-specific update. This not only reduces the possibility of errors but also makes it easier to validate the changes for reviewers and maintainers. Typically, someone at Intel would see a new public release, wait for at least three months to ensure the update is stable, run this script to refresh the intel-ucode-defs.h file, and send a patch upstream to update the mainline and stable versions. Having a standard update script and a defined process minimizes the ambiguity when refreshing the old microcode list. As always, there can be exceptions to this process which should be supported with appropriate justification. Originally-by: Dave Hansen Signed-off-by: Sohil Mehta Signed-off-by: Dave Hansen Link: https://patch.msgid.link/20260407014226.1169040-3-sohil.mehta@intel.com --- scripts/update-intel-ucode-defs.py | 130 +++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100755 scripts/update-intel-ucode-defs.py (limited to 'scripts') diff --git a/scripts/update-intel-ucode-defs.py b/scripts/update-intel-ucode-defs.py new file mode 100755 index 000000000000..9d6cc2c6075f --- /dev/null +++ b/scripts/update-intel-ucode-defs.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +import argparse +import re +import shutil +import subprocess +import sys +import os + +script = os.path.relpath(__file__) + +DESCRIPTION = f""" +For Intel CPUs, update the microcode revisions that determine +X86_BUG_OLD_MICROCODE. + +This script is intended to be run in response to releases of the +official Intel microcode GitHub repository: +https://github.com/intel/Intel-Linux-Processor-Microcode-Data-Files.git + +It takes the Intel microcode files as input and uses iucode-tool to +extract the revision information. It prints the output in the format +expected by intel-ucode-defs.h. + +Usage: + ./{script} /path/to/microcode/files > /path/to/intel-ucode-defs.h + +Typically, someone at Intel would see a new public release, wait for at +least three months to ensure the update is stable, run this script to +refresh the intel-ucode-defs.h file, and send a patch upstream to update +the mainline and stable versions. + +Any exception to this process should be supported with an appropriate +justification. +""" + +SIG_RE = re.compile(r'sig (0x[0-9a-fA-F]+)') +PFM_RE = re.compile(r'pf_mask (0x[0-9a-fA-F]+)') +REV_RE = re.compile(r'rev (0x[0-9a-fA-F]+)') + +# Functions to extract family, model, and stepping +def bits(val, bottom, top): + mask = (1 << (top + 1 - bottom)) - 1 + return (val >> bottom) & mask + +def family(sig): + if bits(sig, 8, 11) == 0xf: + return bits(sig, 8, 11) + bits(sig, 20, 27) + return bits(sig, 8, 11) + +def model(sig): + return bits(sig, 4, 7) | (bits(sig, 16, 19) << 4) + +def step(sig): + return bits(sig, 0, 3) + +class Ucode: + def __init__(self, sig, pfm, rev): + self.family = family(sig) + self.model = model(sig) + self.steppings = 1 << step(sig) + self.platforms = pfm + self.rev = rev + + self.key = (self.family, self.model, self.steppings, self.platforms) + + def __eq__(self, other): + return self.key == other.key + + def __hash__(self): + return hash(self.key) + + def __str__(self): + return "{ .flags = X86_CPU_ID_FLAG_ENTRY_VALID, .vendor = X86_VENDOR_INTEL, .family = 0x%x, .model = 0x%02x, .steppings = 0x%04x, .platform_mask = 0x%02x, .driver_data = 0x%x }," % \ + (self.family, self.model, self.steppings, self.platforms, self.rev) + +def main(): + parser = argparse.ArgumentParser(description=DESCRIPTION, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('ucode_files', nargs='+', help='Path(s) to the microcode files') + + args = parser.parse_args() + + # Process the microcode files using iucode-tool + iucode_tool = shutil.which("iucode-tool") or shutil.which("iucode_tool") + if iucode_tool is None: + print("Error: iucode-tool not found, please install it", file=sys.stderr) + sys.exit(1) + + cmd = [iucode_tool, '--list-all'] + args.ucode_files + + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print("Error: iucode-tool ran into an error, exiting", file=sys.stderr) + if result.stderr: + print(result.stderr, file=sys.stderr, end='') + sys.exit(1) + + ucodes = set() + + # Parse the output of iucode-tool + for line in result.stdout.splitlines(): + sig_match = SIG_RE.search(line) + pfm_match = PFM_RE.search(line) + rev_match = REV_RE.search(line) + + if not (sig_match and pfm_match and rev_match): + continue + + sig = int(sig_match.group(1), 16) + pfm = int(pfm_match.group(1), 16) + rev = int(rev_match.group(1), 16) + debug_rev = bits(rev, 31, 31) + if debug_rev != 0: + print("Error: Debug ucode file found, exiting", file=sys.stderr) + sys.exit(1) + + ucodes.add(Ucode(sig, pfm, rev)) + + if not ucodes: + print("Error: No valid microcode files found, exiting", file=sys.stderr) + sys.exit(1) + + # Sort and print the microcode entries + print("/* SPDX-License-Identifier: GPL-2.0 */") + print("/* Auto-generated by scripts/update-intel-ucode-defs.py */") + for u in sorted(ucodes, key=lambda x: x.key): + print(u) + +if __name__ == "__main__": + main() -- cgit v1.2.3 From 7abef41afad05be1a4b2a3303b9ecf62403463a1 Mon Sep 17 00:00:00 2001 From: Petr Pavlu Date: Fri, 10 Apr 2026 15:13:29 +0200 Subject: kbuild/btf: Remove broken module relinking exclusion Commit 5f9ae91f7c0d ("kbuild: Build kernel module BTFs if BTF is enabled and pahole supports it") in 2020 introduced CONFIG_DEBUG_INFO_BTF_MODULES to enable generation of split BTF for kernel modules. This change required the %.ko Makefile rule to additionally depend on vmlinux, which is used as a base for deduplication. The regular ld_ko_o command executed by the rule was then modified to be skipped if only vmlinux changes. This was done by introducing a new if_changed_except command and updating the original call to '+$(call if_changed_except,ld_ko_o,vmlinux)'. Later, commit 214c0eea43b2 ("kbuild: add $(objtree)/ prefix to some in-kernel build artifacts") in 2024 updated the rule's reference to vmlinux from 'vmlinux' to '$(objtree)/vmlinux'. This accidentally broke the previous logic to skip relinking modules if only vmlinux changes. The issue is that '$(objtree)' is typically '.' and GNU Make normalizes the resulting prerequisite './vmlinux' to just 'vmlinux', while the exclusion logic retains the raw './vmlinux'. As a result, if_changed_except doesn't correctly filter out vmlinux. Consequently, with CONFIG_DEBUG_INFO_BTF_MODULES=y, modules are relinked even if only vmlinux changes. It is possible to fix this Makefile issue. However, having the %.ko rule update the resulting file in place without starting from the original inputs is rather fragile. The logic is harder to debug if something breaks during a subsequent .ko update because the old input is lost due to the overwrite. Additionally, it requires that the BTF processing is idempotent. For example, sorting id+flags BTF_SET8 pairs in .BTF_ids by resolve_btfids currently doesn't have this property. One option is to split the %.ko target into two rules: the first for partial linking and the second one for generating the BTF data. However, this approach runs into an issue with requiring additional intermediate files, which increases the size of the build directory. On my system, when using a large distribution config with ~5500 modules, the size of the build directory with debuginfo enabled is already ~25 GB, with .ko files occupying ~8 GB. Duplicating these .ko files doesn't seem practical. Measuring the speed of the %.ko processing shows that the link step is actually relatively fast. It takes about 20% of the overall rule time, while the BTF processing accounts for 80%. Moreover, skipping the link part becomes relevant only during local development. In such cases, developers typically use configs that enable a limited number of modules, so having the %.ko rule slightly slower doesn't significantly impact the total rebuild time. This is supported by the fact that no one has complained about this optimization being broken for the past two years. Therefore, remove the logic that prevents module relinking when only vmlinux changes and simplify Makefile.modfinal. Signed-off-by: Petr Pavlu Reviewed-by: Alan Maguire Tested-by: Alan Maguire Acked-by: Ihor Solodrai Link: https://patch.msgid.link/20260410131343.2519532-1-petr.pavlu@suse.com Signed-off-by: Nathan Chancellor --- scripts/Makefile.modfinal | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) (limited to 'scripts') diff --git a/scripts/Makefile.modfinal b/scripts/Makefile.modfinal index adcbcde16a07..01a37ec872b9 100644 --- a/scripts/Makefile.modfinal +++ b/scripts/Makefile.modfinal @@ -46,17 +46,9 @@ quiet_cmd_btf_ko = BTF [M] $@ $(CONFIG_SHELL) $(srctree)/scripts/gen-btf.sh --btf_base $(objtree)/vmlinux $@; \ fi; -# Same as newer-prereqs, but allows to exclude specified extra dependencies -newer_prereqs_except = $(filter-out $(PHONY) $(1),$?) - -# Same as if_changed, but allows to exclude specified extra dependencies -if_changed_except = $(if $(call newer_prereqs_except,$(2))$(cmd-check), \ - $(cmd); \ - printf '%s\n' 'savedcmd_$@ := $(make-cmd)' > $(dot-target).cmd, @:) - # Re-generate module BTFs if either module's .ko or vmlinux changed %.ko: %.o %.mod.o .module-common.o $(objtree)/scripts/module.lds $(and $(CONFIG_DEBUG_INFO_BTF_MODULES),$(KBUILD_BUILTIN),$(objtree)/vmlinux) FORCE - +$(call if_changed_except,ld_ko_o,$(objtree)/vmlinux) + +$(call if_changed,ld_ko_o) ifdef CONFIG_DEBUG_INFO_BTF_MODULES +$(if $(newer-prereqs),$(call cmd,btf_ko)) endif -- cgit v1.2.3 From b9d21c32dca2167a614e66c9e27999b9e1c33d55 Mon Sep 17 00:00:00 2001 From: Xingjing Deng Date: Fri, 6 Mar 2026 02:17:09 +0000 Subject: kconfig: fix potential NULL pointer dereference in conf_askvalue In conf_askvalue(), the 'def' argument (retrieved via sym_get_string_value) can be NULL. While current call sites ensure that 'def' is valid, calling printf("%s\n", def) is technically undefined behavior and could lead to a segmentation fault on certain libc implementations if the function were called with a NULL pointer in the future. Improve the robustness of conf_askvalue() by providing an empty string as a fallback. Additionally, remove the redundant re-initialization of the 'line' buffer inside the !sym_is_changeable(sym) block, as it is already properly initialized at the function entry. Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Signed-off-by: Xingjing Deng Reviewed-by: Nathan Chancellor Link: https://patch.msgid.link/20260306021709.27068-1-micro6947@gmail.com Signed-off-by: Nathan Chancellor --- scripts/kconfig/conf.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'scripts') diff --git a/scripts/kconfig/conf.c b/scripts/kconfig/conf.c index a7b44cd8ae14..c368bec5ab60 100644 --- a/scripts/kconfig/conf.c +++ b/scripts/kconfig/conf.c @@ -297,9 +297,7 @@ static int conf_askvalue(struct symbol *sym, const char *def) line[1] = 0; if (!sym_is_changeable(sym)) { - printf("%s\n", def); - line[0] = '\n'; - line[1] = 0; + printf("%s\n", def ?: ""); return 0; } @@ -307,7 +305,7 @@ static int conf_askvalue(struct symbol *sym, const char *def) case oldconfig: case syncconfig: if (sym_has_value(sym)) { - printf("%s\n", def); + printf("%s\n", def ?: ""); return 0; } /* fall through */ -- cgit v1.2.3 From 51fa14ab9705f52f63ca4b26e3fa66c2c93ea15a Mon Sep 17 00:00:00 2001 From: Thomas Weißschuh Date: Mon, 27 Apr 2026 18:04:56 +0200 Subject: gen_compile_commands: Ignore libgcc.a MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some architectures link libgcc.a from the toolchain into the kernel. gen_compile_commands trie to read the kbuild .cmd files of its constituent object files, which are not available. Flat out ignore libgcc.a, as it is not built as part of the kernel anyways. Link: https://lore.kernel.org/r/20260427-kunit-or1k-v1-1-9d3109e991e8@weissschuh.net Signed-off-by: Thomas Weißschuh Acked-by: Nathan Chancellor Signed-off-by: Shuah Khan --- scripts/clang-tools/gen_compile_commands.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'scripts') diff --git a/scripts/clang-tools/gen_compile_commands.py b/scripts/clang-tools/gen_compile_commands.py index 96e6e46ad1a7..8d14b81efd73 100755 --- a/scripts/clang-tools/gen_compile_commands.py +++ b/scripts/clang-tools/gen_compile_commands.py @@ -201,6 +201,8 @@ def main(): # Modules are listed in modules.order. if os.path.isdir(path): cmdfiles = cmdfiles_in_dir(path) + elif os.path.basename(path) == 'libgcc.a': + cmdfiles = [] elif path.endswith('.a'): cmdfiles = cmdfiles_for_a(path, ar) elif path.endswith('modules.order'): -- cgit v1.2.3 From cc39ccce7d5bc623100f07dcda070cef1bf690f6 Mon Sep 17 00:00:00 2001 From: Josh Poimboeuf Date: Thu, 2 Apr 2026 19:47:10 -0700 Subject: klp-build: Fix hang on out-of-date .config If .config is out of date with the kernel source, 'make syncconfig' hangs while waiting for user input on new config options. Detect the mismatch and return an error. Fixes: 6f93f7b06810 ("livepatch/klp-build: Fix inconsistent kernel version") Acked-by: Song Liu Signed-off-by: Josh Poimboeuf --- scripts/livepatch/klp-build | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build index 0ad7e6631314..e19d93b78fcb 100755 --- a/scripts/livepatch/klp-build +++ b/scripts/livepatch/klp-build @@ -306,7 +306,12 @@ set_kernelversion() { stash_file "$file" - kernelrelease="$(cd "$SRC" && make syncconfig &>/dev/null && make -s kernelrelease)" + if [[ -n "$(make -s listnewconfig 2>/dev/null)" ]]; then + die ".config mismatch, check your .config or run 'make olddefconfig'" + fi + make syncconfig &>/dev/null || die "make syncconfig failed" + + kernelrelease="$(make -s kernelrelease)" [[ -z "$kernelrelease" ]] && die "failed to get kernel version" sed -i "2i echo $kernelrelease; exit 0" scripts/setlocalversion -- cgit v1.2.3 From ba77fe55781a2464f68b6c13b4b31d05abd2abcf Mon Sep 17 00:00:00 2001 From: Josh Poimboeuf Date: Fri, 10 Apr 2026 21:49:56 -0700 Subject: klp-build: Fix checksum comparison for changed offsets The klp-build -f/--show-first-changed feature uses diff to compare checksum log lines between original and patched objects. However, diff compares entire lines, including the offset field. When a function is at a different section offset, the offset field differs even though the instruction checksum is identical, causing the wrong instruction to be printed. Only compare the checksum field when looking for the first changed instruction. Also print both the original and patched offsets when they differ. Fixes: 78be9facfb5e ("livepatch/klp-build: Add --show-first-changed option to show function divergence") Acked-by: Song Liu Signed-off-by: Josh Poimboeuf --- scripts/livepatch/klp-build | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) (limited to 'scripts') diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build index e19d93b78fcb..8f0ea56f2640 100755 --- a/scripts/livepatch/klp-build +++ b/scripts/livepatch/klp-build @@ -727,13 +727,29 @@ diff_checksums() { ) for func in ${funcs[$file]}; do - diff <( grep0 -E "^DEBUG: .*checksum: $func " "$orig_log" | sed "s|$ORIG_DIR/||") \ - <( grep0 -E "^DEBUG: .*checksum: $func " "$patched_log" | sed "s|$PATCHED_DIR/||") \ - | gawk '/^< DEBUG: / { - gsub(/:/, "") - printf "%s: %s: %s\n", $3, $5, $6 - exit - }' || true + local -a orig patched + paste <(grep0 -E "^DEBUG: .*checksum: $func " "$orig_log") \ + <(grep0 -E "^DEBUG: .*checksum: $func " "$patched_log") | + while IFS= read -r line; do + read -ra orig <<< "${line%%$'\t'*}" + read -ra patched <<< "${line#*$'\t'}" + + if [[ ${#patched[@]} -eq 0 ]]; then + printf "%s: %s: %s (removed)\n" "${orig[1]%:}" "${orig[3]}" "${orig[-2]}" + break + elif [[ ${#orig[@]} -eq 0 ]]; then + printf "%s: %s: %s (added)\n" "${patched[1]%:}" "${patched[3]}" "${patched[-2]}" + break + fi + + [[ "${orig[-1]}" == "${patched[-1]}" ]] && continue + + printf "%s: %s: %s" "${orig[1]%:}" "${orig[3]}" "${orig[-2]}" + [[ "${orig[-2]}" != "${patched[-2]}" ]] && \ + printf " (patched: %s)" "${patched[-2]}" + printf "\n" + break + done || true done done } -- cgit v1.2.3 From 946d3510fe19dca5cf8c1ea5d5eedff47a72ddc3 Mon Sep 17 00:00:00 2001 From: Josh Poimboeuf Date: Sun, 5 Apr 2026 16:17:08 -0700 Subject: klp-build: Don't use errexit The errtrace option (combined with the ERR trap) already serves the same function (and more) as errexit, so errexit is redundant. And it has more pitfalls. Remove it. Acked-by: Song Liu Reviewed-by: Miroslav Benes Signed-off-by: Josh Poimboeuf --- scripts/livepatch/klp-build | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'scripts') diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build index 8f0ea56f2640..68d61b72f39a 100755 --- a/scripts/livepatch/klp-build +++ b/scripts/livepatch/klp-build @@ -3,7 +3,7 @@ # # Build a livepatch module -# shellcheck disable=SC1090,SC2155 +# shellcheck disable=SC1090,SC2155,SC2164 if (( BASH_VERSINFO[0] < 4 || \ (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4) )); then @@ -11,13 +11,12 @@ if (( BASH_VERSINFO[0] < 4 || \ exit 1 fi -set -o errexit set -o errtrace set -o pipefail set -o nounset # Allow doing 'cmd | mapfile -t array' instead of 'mapfile -t array < <(cmd)'. -# This helps keep execution in pipes so pipefail+errexit can catch errors. +# This helps keep execution in pipes so pipefail+ERR trap can catch errors. shopt -s lastpipe unset DEBUG_CLONE DIFF_CHECKSUM SKIP_CLEANUP XTRACE -- cgit v1.2.3 From b3ece3019e8ebcda5e8451580e34bfbc97ef33e3 Mon Sep 17 00:00:00 2001 From: Josh Poimboeuf Date: Thu, 2 Apr 2026 19:48:45 -0700 Subject: klp-build: Validate patch file existence Make sure all patch files actually exist. Otherwise there can be confusing errors later. Acked-by: Song Liu Reviewed-by: Miroslav Benes Signed-off-by: Josh Poimboeuf --- scripts/livepatch/klp-build | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'scripts') diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build index 68d61b72f39a..13709d20e295 100755 --- a/scripts/livepatch/klp-build +++ b/scripts/livepatch/klp-build @@ -157,6 +157,7 @@ process_args() { local short local long local args + local patch short="hfj:o:vdS:T" long="help,show-first-changed,jobs:,output:,no-replace,verbose,debug,short-circuit:,keep-tmp" @@ -235,6 +236,10 @@ process_args() { KEEP_TMP="$keep_tmp" PATCHES=("$@") + + for patch in "${PATCHES[@]}"; do + [[ -f "$patch" ]] || die "$patch doesn't exist" + done } # temporarily disable xtrace for especially verbose code -- cgit v1.2.3 From 96524543740ead39a79cf36bb4361f93cde1629c Mon Sep 17 00:00:00 2001 From: Josh Poimboeuf Date: Fri, 3 Apr 2026 16:17:31 -0700 Subject: klp-build: Suppress excessive fuzz output by default When a patch applies with fuzz, the detailed output from the patch tool can be very noisy, especially for big patches. Suppress the fuzz details by default, while keeping the "applied with fuzz" warning. The noise can be restored with '--verbose'. Acked-by: Song Liu Reviewed-by: Miroslav Benes Signed-off-by: Josh Poimboeuf --- scripts/livepatch/klp-build | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) (limited to 'scripts') diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build index 13709d20e295..dc2c5c33d1db 100755 --- a/scripts/livepatch/klp-build +++ b/scripts/livepatch/klp-build @@ -19,12 +19,11 @@ set -o nounset # This helps keep execution in pipes so pipefail+ERR trap can catch errors. shopt -s lastpipe -unset DEBUG_CLONE DIFF_CHECKSUM SKIP_CLEANUP XTRACE +unset DEBUG_CLONE DIFF_CHECKSUM SKIP_CLEANUP VERBOSE XTRACE REPLACE=1 SHORT_CIRCUIT=0 JOBS="$(getconf _NPROCESSORS_ONLN)" -VERBOSE="-s" shopt -o xtrace | grep -q 'on' && XTRACE=1 # Avoid removing the previous $TMP_DIR until args have been fully processed. @@ -194,7 +193,7 @@ process_args() { shift ;; -v | --verbose) - VERBOSE="V=1" + VERBOSE=1 shift ;; -d | --debug) @@ -381,7 +380,7 @@ apply_patch() { echo "$output" >&2 die "$patch did not apply" elif [[ "$output" =~ $drift_regex ]]; then - echo "$output" >&2 + [[ -v VERBOSE ]] && echo "$output" >&2 warn "${patch} applied with fuzz" fi @@ -544,7 +543,11 @@ build_kernel() { # cmd+=("KBUILD_MODPOST_WARN=1") - cmd+=("$VERBOSE") + if [[ -v VERBOSE ]]; then + cmd+=("V=1") + else + cmd+=("-s") + fi cmd+=("-j$JOBS") cmd+=("KCFLAGS=-ffunction-sections -fdata-sections") cmd+=("OBJTOOL_ARGS=${objtool_args[*]}") @@ -805,7 +808,11 @@ build_patch_module() { [[ $REPLACE -eq 0 ]] && cflags+=("-DKLP_NO_REPLACE") cmd=("make") - cmd+=("$VERBOSE") + if [[ -v VERBOSE ]]; then + cmd+=("V=1") + else + cmd+=("-s") + fi cmd+=("-j$JOBS") cmd+=("--directory=.") cmd+=("M=$KMOD_DIR") -- cgit v1.2.3 From f3048888ea62ac1c573db91e74e0dcabe058e89f Mon Sep 17 00:00:00 2001 From: Josh Poimboeuf Date: Thu, 2 Apr 2026 19:08:39 -0700 Subject: klp-build: Fix patch cleanup on interrupt If a build error occurs and the user hits Ctrl-C while a large patch is being reverted during cleanup, the cleanup EXIT trap gets re-triggered and tries to re-revert the already partially-reverted patch. That causes 'patch -R' to repeatedly prompt "Unreversed patch detected! Ignore -R? [n]" for each already-reverted hunk, with no way to break out. Fix it by adding '--force' to the patch revert command in revert_patch(), which causes it to silently ignore already-reverted hunks. And ignore errors, as the cleanup is always best-effort. For similar reasons, add to APPLIED_PATCHES before (rather than after) applying the patch in apply_patch() so an interrupted apply will also get cleaned up. Fixes: d36a7343f4ba ("livepatch/klp-build: switch to GNU patch and recountdiff") Acked-by: Song Liu Signed-off-by: Josh Poimboeuf --- scripts/livepatch/klp-build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build index dc2c5c33d1db..9970e1f274ef 100755 --- a/scripts/livepatch/klp-build +++ b/scripts/livepatch/klp-build @@ -384,15 +384,15 @@ apply_patch() { warn "${patch} applied with fuzz" fi - patch -d "$SRC" -p1 --no-backup-if-mismatch -r /dev/null "${extra_args[@]}" --silent < "$patch" APPLIED_PATCHES+=("$patch") + patch -d "$SRC" -p1 --no-backup-if-mismatch -r /dev/null "${extra_args[@]}" --silent < "$patch" } revert_patch() { local patch="$1" local tmp=() - patch -d "$SRC" -p1 -R --silent --no-backup-if-mismatch -r /dev/null < "$patch" + patch -d "$SRC" -p1 -R --force --no-backup-if-mismatch -r /dev/null &> /dev/null < "$patch" || true for p in "${APPLIED_PATCHES[@]}"; do [[ "$p" == "$patch" ]] && continue -- cgit v1.2.3 From d8c3e262361b04984f0322ce5b88ea52dab37318 Mon Sep 17 00:00:00 2001 From: Josh Poimboeuf Date: Sun, 12 Apr 2026 12:09:39 -0700 Subject: klp-build: Reject patches to vDSO vDSO code runs in userspace and can't be livepatched. Such patches also cause spurious "new function" errors due to generated files like vdso*-image.c having unstable line numbers across builds. Acked-by: Song Liu Signed-off-by: Josh Poimboeuf --- scripts/livepatch/klp-build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build index 9970e1f274ef..a70d48d98453 100755 --- a/scripts/livepatch/klp-build +++ b/scripts/livepatch/klp-build @@ -357,7 +357,7 @@ check_unsupported_patches() { for file in "${files[@]}"; do case "$file" in - lib/*|*.S) + lib/*|*/vdso/*|*.S) die "${patch}: unsupported patch to $file" ;; esac -- cgit v1.2.3 From df0d7bb04a27e48a0fb5fd32223f5ab248876cab Mon Sep 17 00:00:00 2001 From: Josh Poimboeuf Date: Sun, 19 Apr 2026 19:57:48 -0700 Subject: klp-build: Reject patches to realmode Realmode code is compiled as a separate 16-bit binary and embedded into the kernel image via rmpiggy.S. It can't be livepatched. Acked-by: Song Liu Signed-off-by: Josh Poimboeuf --- scripts/livepatch/klp-build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build index a70d48d98453..2bb35de5db75 100755 --- a/scripts/livepatch/klp-build +++ b/scripts/livepatch/klp-build @@ -357,7 +357,7 @@ check_unsupported_patches() { for file in "${files[@]}"; do case "$file" in - lib/*|*/vdso/*|*.S) + lib/*|*/vdso/*|*/realmode/rm/*|*.S) die "${patch}: unsupported patch to $file" ;; esac -- cgit v1.2.3 From e950d2a10a30aa5f3ebb92d4bf807d1d5dd96de1 Mon Sep 17 00:00:00 2001 From: Josh Poimboeuf Date: Fri, 3 Apr 2026 16:17:46 -0700 Subject: klp-build: Print "objtool klp diff" command in verbose mode Print the full objtool command line when '--verbose' is given to help with debugging. Acked-by: Song Liu Signed-off-by: Josh Poimboeuf --- scripts/livepatch/klp-build | 1 + 1 file changed, 1 insertion(+) (limited to 'scripts') diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build index 2bb35de5db75..327aef4d9cf8 100755 --- a/scripts/livepatch/klp-build +++ b/scripts/livepatch/klp-build @@ -681,6 +681,7 @@ diff_objects() { ( cd "$ORIG_DIR" + [[ -v VERBOSE ]] && echo "cd $ORIG_DIR && ${cmd[*]}" "${cmd[@]}" \ 1> >(tee -a "$log") \ 2> >(tee -a "$log" | "${filter[@]}" >&2) || \ -- cgit v1.2.3 From b6480aaedf3cbdd0a8e4b75c77434423eb52e3b8 Mon Sep 17 00:00:00 2001 From: Josh Poimboeuf Date: Thu, 30 Apr 2026 08:54:57 -0700 Subject: klp-build: Remove redundant SRC and OBJ variables SRC and OBJ are both set to $(pwd) and are always identical. The script already enforces that klp-build runs from the kernel root directory, and builds are done in-place, making these variables unnecessary. Suggested-by: Song Liu Acked-by: Song Liu Signed-off-by: Josh Poimboeuf --- scripts/livepatch/klp-build | 67 +++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 39 deletions(-) (limited to 'scripts') diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build index 327aef4d9cf8..215d2301510b 100755 --- a/scripts/livepatch/klp-build +++ b/scripts/livepatch/klp-build @@ -33,11 +33,9 @@ SCRIPT="$(basename "$0")" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" FIX_PATCH_LINES="$SCRIPT_DIR/fix-patch-lines" -SRC="$(pwd)" -OBJ="$(pwd)" - -CONFIG="$OBJ/.config" -TMP_DIR="$OBJ/klp-tmp" +OBJTOOL="$PWD/tools/objtool/objtool" +CONFIG="$PWD/.config" +TMP_DIR="$PWD/klp-tmp" ORIG_DIR="$TMP_DIR/orig" PATCHED_DIR="$TMP_DIR/patched" @@ -88,7 +86,7 @@ declare -a STASHED_FILES stash_file() { local file="$1" - local rel_file="${file#"$SRC"/}" + local rel_file="${file#"$PWD"/}" [[ ! -e "$file" ]] && die "no file to stash: $file" @@ -102,7 +100,7 @@ restore_files() { local file for file in "${STASHED_FILES[@]}"; do - mv -f "$STASH_DIR/$file" "$SRC/$file" || warn "can't restore file: $file" + mv -f "$STASH_DIR/$file" "$PWD/$file" || warn "can't restore file: $file" done STASHED_FILES=() @@ -304,7 +302,7 @@ set_module_name() { # Hardcode the value printed by the localversion script to prevent patch # application from appending it with '+' due to a dirty working tree. set_kernelversion() { - local file="$SRC/scripts/setlocalversion" + local file="$PWD/scripts/setlocalversion" local kernelrelease stash_file "$file" @@ -375,7 +373,7 @@ apply_patch() { [[ ! -f "$patch" ]] && die "$patch doesn't exist" status=0 - output=$(patch -d "$SRC" -p1 --dry-run --no-backup-if-mismatch -r /dev/null "${extra_args[@]}" < "$patch" 2>&1) || status=$? + output=$(patch -p1 --dry-run --no-backup-if-mismatch -r /dev/null "${extra_args[@]}" < "$patch" 2>&1) || status=$? if [[ "$status" -ne 0 ]]; then echo "$output" >&2 die "$patch did not apply" @@ -385,14 +383,14 @@ apply_patch() { fi APPLIED_PATCHES+=("$patch") - patch -d "$SRC" -p1 --no-backup-if-mismatch -r /dev/null "${extra_args[@]}" --silent < "$patch" + patch -p1 --no-backup-if-mismatch -r /dev/null "${extra_args[@]}" --silent < "$patch" } revert_patch() { local patch="$1" local tmp=() - patch -d "$SRC" -p1 -R --force --no-backup-if-mismatch -r /dev/null &> /dev/null < "$patch" || true + patch -p1 -R --force --no-backup-if-mismatch -r /dev/null &> /dev/null < "$patch" || true for p in "${APPLIED_PATCHES[@]}"; do [[ "$p" == "$patch" ]] && continue @@ -430,8 +428,7 @@ validate_patches() { do_init() { # We're not yet smart enough to handle anything other than in-tree # builds in pwd. - [[ ! "$SRC" -ef "$SCRIPT_DIR/../.." ]] && die "please run from the kernel root directory" - [[ ! "$OBJ" -ef "$SCRIPT_DIR/../.." ]] && die "please run from the kernel root directory" + [[ ! "$PWD" -ef "$SCRIPT_DIR/../.." ]] && die "please run from the kernel root directory" (( SHORT_CIRCUIT <= 1 )) && rm -rf "$TMP_DIR" mkdir -p "$TMP_DIR" @@ -462,11 +459,11 @@ refresh_patch() { get_patch_output_files "$patch" | mapfile -t output_files # Copy orig source files to 'a' - ( cd "$SRC" && echo "${input_files[@]}" | xargs cp --parents --target-directory="$tmpdir/a" ) + echo "${input_files[@]}" | xargs cp --parents --target-directory="$tmpdir/a" # Copy patched source files to 'b' apply_patch "$patch" "--silent" - ( cd "$SRC" && echo "${output_files[@]}" | xargs cp --parents --target-directory="$tmpdir/b" ) + echo "${output_files[@]}" | xargs cp --parents --target-directory="$tmpdir/b" revert_patch "$patch" # Diff 'a' and 'b' to make a clean patch @@ -510,10 +507,7 @@ clean_kernel() { cmd+=("-j$JOBS") cmd+=("clean") - ( - cd "$SRC" - "${cmd[@]}" - ) + "${cmd[@]}" } build_kernel() { @@ -554,12 +548,10 @@ build_kernel() { cmd+=("vmlinux") cmd+=("modules") - ( - cd "$SRC" - "${cmd[@]}" \ - 1> >(tee -a "$log") \ - 2> >(tee -a "$log" | grep0 -v "modpost.*undefined!" >&2) - ) || die "$build kernel build failed" + "${cmd[@]}" \ + 1> >(tee -a "$log") \ + 2> >(tee -a "$log" | grep0 -v "modpost.*undefined!" >&2) \ + || die "$build kernel build failed" } find_objects() { @@ -567,9 +559,9 @@ find_objects() { # Find root-level vmlinux.o and non-root-level .ko files, # excluding klp-tmp/ and .git/ - find "$OBJ" \( -path "$TMP_DIR" -o -path "$OBJ/.git" -o -regex "$OBJ/[^/][^/]*\.ko" \) -prune -o \ + find "$PWD" \( -path "$TMP_DIR" -o -path "$PWD/.git" -o -regex "$PWD/[^/][^/]*\.ko" \) -prune -o \ -type f "${opts[@]}" \ - \( -name "*.ko" -o -path "$OBJ/vmlinux.o" \) \ + \( -name "*.ko" -o -path "$PWD/vmlinux.o" \) \ -printf '%P\n' } @@ -585,7 +577,7 @@ copy_orig_objects() { xtrace_save "copying orig objects" for _file in "${files[@]}"; do local rel_file="${_file/.ko/.o}" - local file="$OBJ/$rel_file" + local file="$PWD/$rel_file" local orig_file="$ORIG_DIR/$rel_file" local orig_dir="$(dirname "$orig_file")" @@ -618,7 +610,7 @@ copy_patched_objects() { xtrace_save "copying changed objects" for _file in "${files[@]}"; do local rel_file="${_file/.ko/.o}" - local file="$OBJ/$rel_file" + local file="$PWD/$rel_file" local orig_file="$ORIG_DIR/$rel_file" local patched_file="$PATCHED_DIR/$rel_file" local patched_dir="$(dirname "$patched_file")" @@ -663,7 +655,7 @@ diff_objects() { mkdir -p "$(dirname "$out_file")" - cmd=("$SRC/tools/objtool/objtool") + cmd=("$OBJTOOL") cmd+=("klp") cmd+=("diff") (( ${#opts[@]} > 0 )) && cmd+=("${opts[@]}") @@ -716,7 +708,7 @@ diff_checksums() { fi done - cmd=("$SRC/tools/objtool/objtool") + cmd=("$OBJTOOL") cmd+=("--checksum") cmd+=("--link") cmd+=("--dry-run") @@ -774,7 +766,7 @@ build_patch_module() { rm -rf "$KMOD_DIR" mkdir -p "$KMOD_DIR" - cp -f "$SRC/scripts/livepatch/init.c" "$KMOD_DIR" + cp -f "$SCRIPT_DIR/init.c" "$KMOD_DIR" echo "obj-m := $NAME.o" > "$makefile" echo -n "$NAME-y := init.o" >> "$makefile" @@ -820,12 +812,9 @@ build_patch_module() { cmd+=("KCFLAGS=${cflags[*]}") # Build a "normal" kernel module with init.c and the diffed objects - ( - cd "$SRC" - "${cmd[@]}" \ - 1> >(tee -a "$log") \ - 2> >(tee -a "$log" >&2) - ) + "${cmd[@]}" \ + 1> >(tee -a "$log") \ + 2> >(tee -a "$log" >&2) kmod_file="$KMOD_DIR/$NAME.ko" @@ -836,7 +825,7 @@ build_patch_module() { objcopy --remove-section=.BTF "$kmod_file" # Fix (and work around) linker wreckage for klp syms / relocs - "$SRC/tools/objtool/objtool" klp post-link "$kmod_file" || die "objtool klp post-link failed" + "$OBJTOOL" klp post-link "$kmod_file" || die "objtool klp post-link failed" cp -f "$kmod_file" "$OUTFILE" } -- cgit v1.2.3 From d4888d58041d1a61d66f2c81cb398f9685bc7576 Mon Sep 17 00:00:00 2001 From: Josh Poimboeuf Date: Fri, 3 Apr 2026 13:09:34 -0700 Subject: klp-build: Use "objtool klp checksum" subcommand Use the new "objtool klp checksum" subcommand instead of injecting --checksum into every objtool invocation via OBJTOOL_ARGS during the kernel build. This decouples checksum generation from the build, running it in separate post-build passes, making the code (and the patch generation pipeline itself) more modular. Acked-by: Song Liu Signed-off-by: Josh Poimboeuf --- scripts/livepatch/klp-build | 95 +++++++++++++++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 30 deletions(-) (limited to 'scripts') diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build index 215d2301510b..6103345cd2a9 100755 --- a/scripts/livepatch/klp-build +++ b/scripts/livepatch/klp-build @@ -37,10 +37,12 @@ OBJTOOL="$PWD/tools/objtool/objtool" CONFIG="$PWD/.config" TMP_DIR="$PWD/klp-tmp" -ORIG_DIR="$TMP_DIR/orig" -PATCHED_DIR="$TMP_DIR/patched" -DIFF_DIR="$TMP_DIR/diff" -KMOD_DIR="$TMP_DIR/kmod" +ORIG_DIR="$TMP_DIR/1-orig" +PATCHED_DIR="$TMP_DIR/2-patched" +ORIG_CSUM_DIR="$TMP_DIR/3-checksum-orig" +PATCHED_CSUM_DIR="$TMP_DIR/3-checksum-patched" +DIFF_DIR="$TMP_DIR/4-diff" +KMOD_DIR="$TMP_DIR/5-kmod" STASH_DIR="$TMP_DIR/stash" TIMESTAMP="$TMP_DIR/timestamp" @@ -136,10 +138,11 @@ Options: Advanced Options: -d, --debug Show symbol/reloc cloning decisions -S, --short-circuit=STEP Start at build step (requires prior --keep-tmp) - 1|orig Build original kernel (default) - 2|patched Build patched kernel - 3|diff Diff objects - 4|kmod Build patch module + 1|orig Build original kernel (default) + 2|patched Build patched kernel + 3|checksum Generate checksums + 4|diff Diff objects + 5|kmod Build patch module -T, --keep-tmp Preserve tmp dir on exit EOF @@ -203,10 +206,11 @@ process_args() { [[ ! -d "$TMP_DIR" ]] && die "--short-circuit requires preserved klp-tmp dir" keep_tmp=1 case "$2" in - 1 | orig) SHORT_CIRCUIT=1; ;; - 2 | patched) SHORT_CIRCUIT=2; ;; - 3 | diff) SHORT_CIRCUIT=3; ;; - 4 | mod) SHORT_CIRCUIT=4; ;; + 1 | orig) SHORT_CIRCUIT=1; ;; + 2 | patched) SHORT_CIRCUIT=2; ;; + 3 | checksum) SHORT_CIRCUIT=3; ;; + 4 | diff) SHORT_CIRCUIT=4; ;; + 5 | kmod) SHORT_CIRCUIT=5; ;; *) die "invalid short-circuit step '$2'" ;; esac shift 2 @@ -513,11 +517,8 @@ clean_kernel() { build_kernel() { local build="$1" local log="$TMP_DIR/build.log" - local objtool_args=() local cmd=() - objtool_args=("--checksum") - cmd=("make") # When a patch to a kernel module references a newly created unexported @@ -544,7 +545,6 @@ build_kernel() { fi cmd+=("-j$JOBS") cmd+=("KCFLAGS=-ffunction-sections -fdata-sections") - cmd+=("OBJTOOL_ARGS=${objtool_args[*]}") cmd+=("vmlinux") cmd+=("modules") @@ -574,7 +574,7 @@ copy_orig_objects() { find_objects | mapfile -t files - xtrace_save "copying orig objects" + xtrace_save "copying original objects" for _file in "${files[@]}"; do local rel_file="${_file/.ko/.o}" local file="$PWD/$rel_file" @@ -630,6 +630,35 @@ copy_patched_objects() { mv -f "$TMP_DIR/build.log" "$PATCHED_DIR" } +# Copy .o files to a separate directory and run "objtool klp checksum" on each +# copy. The checksums are written to a .discard.sym_checksum section. +# +# If match_dir is given, only process files which also exist there. +generate_checksums() { + local src_dir="$1" + local dest_dir="$2" + local match_dir="${3:-}" + local files=() + local file + + rm -rf "$dest_dir" + mkdir -p "$dest_dir" + + find "$src_dir" -type f -name "*.o" | mapfile -t files + for file in "${files[@]}"; do + local rel="${file#"$src_dir"/}" + local dest="$dest_dir/$rel" + + [[ -n "$match_dir" && ! -f "$match_dir/$rel" ]] && continue + + mkdir -p "$(dirname "$dest")" + cp -f "$file" "$dest" + "$OBJTOOL" klp checksum "$dest" + done + + touch "$dest_dir/.complete" +} + # Diff changed objects, writing output object to $DIFF_DIR diff_objects() { local log="$KLP_DIFF_LOG" @@ -639,16 +668,16 @@ diff_objects() { rm -rf "$DIFF_DIR" mkdir -p "$DIFF_DIR" - find "$PATCHED_DIR" -type f -name "*.o" | mapfile -t files + find "$PATCHED_CSUM_DIR" -type f -name "*.o" | mapfile -t files [[ ${#files[@]} -eq 0 ]] && die "no changes detected" [[ -v DEBUG_CLONE ]] && opts=("--debug") # Diff all changed objects for file in "${files[@]}"; do - local rel_file="${file#"$PATCHED_DIR"/}" + local rel_file="${file#"$PATCHED_CSUM_DIR"/}" local orig_file="$rel_file" - local patched_file="$PATCHED_DIR/$rel_file" + local patched_file="$PATCHED_CSUM_DIR/$rel_file" local out_file="$DIFF_DIR/$rel_file" local filter=() local cmd=() @@ -672,8 +701,8 @@ diff_objects() { fi ( - cd "$ORIG_DIR" - [[ -v VERBOSE ]] && echo "cd $ORIG_DIR && ${cmd[*]}" + cd "$ORIG_CSUM_DIR" + [[ -v VERBOSE ]] && echo "cd $ORIG_CSUM_DIR && ${cmd[*]}" "${cmd[@]}" \ 1> >(tee -a "$log") \ 2> >(tee -a "$log" | "${filter[@]}" >&2) || \ @@ -682,9 +711,9 @@ diff_objects() { done } -# For each changed object, run objtool with --debug-checksum to get the -# per-instruction checksums, and then diff those to find the first changed -# instruction for each function. +# For each changed object, run "objtool klp checksum" with --debug-checksum to +# get the per-instruction checksums, and then diff those to find the first +# changed instruction for each function. diff_checksums() { local orig_log="$ORIG_DIR/checksum.log" local patched_log="$PATCHED_DIR/checksum.log" @@ -709,8 +738,7 @@ diff_checksums() { done cmd=("$OBJTOOL") - cmd+=("--checksum") - cmd+=("--link") + cmd+=("klp" "checksum") cmd+=("--dry-run") for file in "${!funcs[@]}"; do @@ -719,11 +747,11 @@ diff_checksums() { ( cd "$ORIG_DIR" "${cmd[@]}" "$opt" "$file" &> "$orig_log" || \ - ( cat "$orig_log" >&2; die "objtool --debug-checksum failed" ) + ( cat "$orig_log" >&2; die "objtool klp checksum failed" ) cd "$PATCHED_DIR" "${cmd[@]}" "$opt" "$file" &> "$patched_log" || \ - ( cat "$patched_log" >&2; die "objtool --debug-checksum failed" ) + ( cat "$patched_log" >&2; die "objtool klp checksum failed" ) ) for func in ${funcs[$file]}; do @@ -861,6 +889,13 @@ if (( SHORT_CIRCUIT <= 2 )); then fi if (( SHORT_CIRCUIT <= 3 )); then + status "Generating original checksums" + generate_checksums "$ORIG_DIR" "$ORIG_CSUM_DIR" "$PATCHED_DIR" + status "Generating patched checksums" + generate_checksums "$PATCHED_DIR" "$PATCHED_CSUM_DIR" +fi + +if (( SHORT_CIRCUIT <= 4 )); then status "Diffing objects" diff_objects if [[ -v DIFF_CHECKSUM ]]; then @@ -869,7 +904,7 @@ if (( SHORT_CIRCUIT <= 3 )); then fi fi -if (( SHORT_CIRCUIT <= 4 )); then +if (( SHORT_CIRCUIT <= 5 )); then status "Building patch module: $OUTFILE" build_patch_module fi -- cgit v1.2.3 From 3b8e56b86faa3e44745aa764c2e1fd9dbbd16104 Mon Sep 17 00:00:00 2001 From: Josh Poimboeuf Date: Fri, 3 Apr 2026 13:10:29 -0700 Subject: objtool/klp: Remove "objtool --checksum" The checksum functionality has been moved to "objtool klp checksum" which is now used by klp-build. Remove the now-dead --checksum and --debug-checksum options from the default objtool command. Acked-by: Song Liu Signed-off-by: Josh Poimboeuf --- scripts/livepatch/klp-build | 3 +++ tools/objtool/builtin-check.c | 17 +---------------- tools/objtool/check.c | 10 ---------- 3 files changed, 4 insertions(+), 26 deletions(-) (limited to 'scripts') diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build index 6103345cd2a9..c1475c003298 100755 --- a/scripts/livepatch/klp-build +++ b/scripts/livepatch/klp-build @@ -275,6 +275,9 @@ validate_config() { [[ "$CONFIG_AS_VERSION" -lt 200000 ]] && \ die "Clang assembler version < 20 not supported" + [[ -x "$OBJTOOL" ]] && "$OBJTOOL" klp 2>&1 | command grep -q "not implemented" && \ + die "objtool not built with KLP support; install xxhash-devel/libxxhash-dev (version >= 0.8) and recompile" + return 0 } diff --git a/tools/objtool/builtin-check.c b/tools/objtool/builtin-check.c index b780df513715..ec7f10a5ef19 100644 --- a/tools/objtool/builtin-check.c +++ b/tools/objtool/builtin-check.c @@ -73,7 +73,6 @@ static int parse_hacks(const struct option *opt, const char *str, int unset) static const struct option check_options[] = { OPT_GROUP("Actions:"), - OPT_BOOLEAN(0, "checksum", &opts.checksum, "generate per-function checksums"), OPT_BOOLEAN(0, "cfi", &opts.cfi, "annotate kernel control flow integrity (kCFI) function preambles"), OPT_STRING_OPTARG('d', "disas", &opts.disas, "function-pattern", "disassemble functions", "*"), OPT_CALLBACK_OPTARG('h', "hacks", NULL, NULL, "jump_label,noinstr,skylake", "patch toolchain bugs/limitations", parse_hacks), @@ -95,7 +94,6 @@ static const struct option check_options[] = { OPT_GROUP("Options:"), OPT_BOOLEAN(0, "backtrace", &opts.backtrace, "unwind on error"), OPT_BOOLEAN(0, "backup", &opts.backup, "create backup (.orig) file on warning/error"), - OPT_STRING(0, "debug-checksum", &opts.debug_checksum, "funcs", "enable checksum debug output"), OPT_BOOLEAN(0, "dry-run", &opts.dryrun, "don't write modifications"), OPT_BOOLEAN(0, "link", &opts.link, "object is a linked object"), OPT_BOOLEAN(0, "module", &opts.module, "object is part of a kernel module"), @@ -165,20 +163,7 @@ static bool opts_valid(void) return false; } -#ifndef BUILD_KLP - if (opts.checksum) { - ERROR("--checksum not supported; install xxhash-devel/libxxhash-dev (version >= 0.8) and recompile"); - return false; - } -#endif - - if (opts.debug_checksum && !opts.checksum) { - ERROR("--debug-checksum requires --checksum"); - return false; - } - - if (opts.checksum || - opts.disas || + if (opts.disas || opts.hack_jump_label || opts.hack_noinstr || opts.ibt || diff --git a/tools/objtool/check.c b/tools/objtool/check.c index 3e5d335d0e29..ae047be919c5 100644 --- a/tools/objtool/check.c +++ b/tools/objtool/check.c @@ -18,7 +18,6 @@ #include #include #include -#include #include #include @@ -4946,15 +4945,6 @@ int check(struct objtool_file *file) if (opts.noabs) warnings += check_abs_references(file); - if (opts.checksum) { - ret = calculate_checksums(file); - if (ret) - goto out; - ret = create_sym_checksum_section(file); - if (ret) - goto out; - } - if (opts.orc && nr_insns) { ret = orc_create(file); if (ret) -- cgit v1.2.3 From 225d16dd510d92c8eaba8e6496cfaa7881a24827 Mon Sep 17 00:00:00 2001 From: Josh Poimboeuf Date: Fri, 17 Apr 2026 13:33:55 -0700 Subject: klp-build: Validate short-circuit prerequisites The --short-circuit option implicitly requires that certain directories are already in klp-tmp. Enforce that to prevent confusing errors. Acked-by: Song Liu Signed-off-by: Josh Poimboeuf --- scripts/livepatch/klp-build | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'scripts') diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build index c1475c003298..c4a7acf8edc3 100755 --- a/scripts/livepatch/klp-build +++ b/scripts/livepatch/klp-build @@ -437,6 +437,20 @@ do_init() { # builds in pwd. [[ ! "$PWD" -ef "$SCRIPT_DIR/../.." ]] && die "please run from the kernel root directory" + if (( SHORT_CIRCUIT >= 2 )); then + [[ -f "$ORIG_DIR/.complete" ]] || die "-S $SHORT_CIRCUIT requires completed $ORIG_DIR" + fi + if (( SHORT_CIRCUIT >= 3 )); then + [[ -f "$PATCHED_DIR/.complete" ]] || die "-S $SHORT_CIRCUIT requires completed $PATCHED_DIR" + fi + if (( SHORT_CIRCUIT >= 4 )); then + [[ -f "$ORIG_CSUM_DIR/.complete" ]] || die "-S $SHORT_CIRCUIT requires completed $ORIG_CSUM_DIR" + [[ -f "$PATCHED_CSUM_DIR/.complete" ]] || die "-S $SHORT_CIRCUIT requires completed $PATCHED_CSUM_DIR" + fi + if (( SHORT_CIRCUIT >= 5 )); then + [[ -f "$DIFF_DIR/.complete" ]] || die "-S $SHORT_CIRCUIT requires completed $DIFF_DIR" + fi + (( SHORT_CIRCUIT <= 1 )) && rm -rf "$TMP_DIR" mkdir -p "$TMP_DIR" @@ -593,6 +607,7 @@ copy_orig_objects() { mv -f "$TMP_DIR/build.log" "$ORIG_DIR" touch "$TIMESTAMP" + touch "$ORIG_DIR/.complete" } # Copy all changed objects to $PATCHED_DIR @@ -631,6 +646,7 @@ copy_patched_objects() { (( found == 0 )) && die "no changes detected" mv -f "$TMP_DIR/build.log" "$PATCHED_DIR" + touch "$PATCHED_DIR/.complete" } # Copy .o files to a separate directory and run "objtool klp checksum" on each @@ -712,6 +728,8 @@ diff_objects() { die "objtool klp diff failed" ) done + + touch "$DIFF_DIR/.complete" } # For each changed object, run "objtool klp checksum" with --debug-checksum to -- cgit v1.2.3 From fc0bb9915bce0c333f918ca76958d804ccd79f89 Mon Sep 17 00:00:00 2001 From: Josh Poimboeuf Date: Thu, 23 Apr 2026 15:53:17 -0700 Subject: objtool: Grow __cfi_* prefix symbols for all CFI+CALL_PADDING For all CONFIG_CFI+CONFIG_CALL_PADDING configs, for C functions, the __cfi_ symbols only cover the 5-byte kCFI type hash. After that there also N bytes of NOP padding between the hash and the function entry which aren't associated with any symbol. The NOPs can be replaced with actual code at runtime. Without a symbol, unwinders and tooling have no way of knowing where those bytes belong. Grow the existing __cfi_* symbols to fill that gap. Note that assembly functions with SYM_TYPED_FUNC_START() aren't affected by this issue, their __cfi_ symbols also cover the padding. Also, CONFIG_PREFIX_SYMBOLS has no reason to exist: CONFIG_CALL_PADDING is what causes the compiler to emit NOP padding before function entry (via -fpatchable-function-entry), so it's the right condition for creating prefix symbols. Remove CONFIG_PREFIX_SYMBOLS, as it's no longer needed. Simplify the LONGEST_SYM_KUNIT_TEST dependency accordingly. Rework objtool's arguments a bit to handle the variety of prefix/cfi-related cases. Suggested-by: Peter Zijlstra Signed-off-by: Josh Poimboeuf --- arch/x86/Kconfig | 4 --- lib/Kconfig.debug | 2 +- scripts/Makefile.lib | 7 +++-- tools/objtool/builtin-check.c | 15 ++++++++-- tools/objtool/check.c | 49 +++++++++++++++++++++++++++------ tools/objtool/elf.c | 20 ++++++++++++++ tools/objtool/include/objtool/builtin.h | 7 +++-- tools/objtool/include/objtool/elf.h | 1 + 8 files changed, 84 insertions(+), 21 deletions(-) (limited to 'scripts') diff --git a/arch/x86/Kconfig b/arch/x86/Kconfig index f3f7cb01d69d..3eb3c48d764a 100644 --- a/arch/x86/Kconfig +++ b/arch/x86/Kconfig @@ -2437,10 +2437,6 @@ config CALL_THUNKS def_bool n select CALL_PADDING -config PREFIX_SYMBOLS - def_bool y - depends on CALL_PADDING && !CFI - menuconfig CPU_MITIGATIONS bool "Mitigations for CPU vulnerabilities" default y diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug index 8ff5adcfe1e0..4f7496b3268d 100644 --- a/lib/Kconfig.debug +++ b/lib/Kconfig.debug @@ -3070,7 +3070,7 @@ config FORTIFY_KUNIT_TEST config LONGEST_SYM_KUNIT_TEST tristate "Test the longest symbol possible" if !KUNIT_ALL_TESTS depends on KUNIT && KPROBES - depends on !PREFIX_SYMBOLS && !CFI && !GCOV_KERNEL + depends on !CALL_PADDING && !CFI && !GCOV_KERNEL default KUNIT_ALL_TESTS help Tests the longest symbol possible diff --git a/scripts/Makefile.lib b/scripts/Makefile.lib index 0718e39cedda..7e216d82e988 100644 --- a/scripts/Makefile.lib +++ b/scripts/Makefile.lib @@ -187,7 +187,11 @@ objtool-args-$(CONFIG_HAVE_JUMP_LABEL_HACK) += --hacks=jump_label objtool-args-$(CONFIG_HAVE_NOINSTR_HACK) += --hacks=noinstr objtool-args-$(CONFIG_MITIGATION_CALL_DEPTH_TRACKING) += --hacks=skylake objtool-args-$(CONFIG_X86_KERNEL_IBT) += --ibt -objtool-args-$(CONFIG_FINEIBT) += --cfi +objtool-args-$(CONFIG_CALL_PADDING) += --prefix=$(CONFIG_FUNCTION_PADDING_BYTES) +ifdef CONFIG_CALL_PADDING +objtool-args-$(CONFIG_CFI) += --cfi +objtool-args-$(CONFIG_FINEIBT) += --fineibt +endif objtool-args-$(CONFIG_FTRACE_MCOUNT_USE_OBJTOOL) += --mcount ifdef CONFIG_FTRACE_MCOUNT_USE_OBJTOOL objtool-args-$(CONFIG_HAVE_OBJTOOL_NOP_MCOUNT) += --mnop @@ -200,7 +204,6 @@ objtool-args-$(CONFIG_STACK_VALIDATION) += --stackval objtool-args-$(CONFIG_HAVE_STATIC_CALL_INLINE) += --static-call objtool-args-$(CONFIG_HAVE_UACCESS_VALIDATION) += --uaccess objtool-args-$(or $(CONFIG_GCOV_KERNEL),$(CONFIG_KCOV)) += --no-unreachable -objtool-args-$(CONFIG_PREFIX_SYMBOLS) += --prefix=$(CONFIG_FUNCTION_PADDING_BYTES) objtool-args-$(CONFIG_OBJTOOL_WERROR) += --werror objtool-args = $(objtool-args-y) \ diff --git a/tools/objtool/builtin-check.c b/tools/objtool/builtin-check.c index ec7f10a5ef19..118c3de2f293 100644 --- a/tools/objtool/builtin-check.c +++ b/tools/objtool/builtin-check.c @@ -73,7 +73,6 @@ static int parse_hacks(const struct option *opt, const char *str, int unset) static const struct option check_options[] = { OPT_GROUP("Actions:"), - OPT_BOOLEAN(0, "cfi", &opts.cfi, "annotate kernel control flow integrity (kCFI) function preambles"), OPT_STRING_OPTARG('d', "disas", &opts.disas, "function-pattern", "disassemble functions", "*"), OPT_CALLBACK_OPTARG('h', "hacks", NULL, NULL, "jump_label,noinstr,skylake", "patch toolchain bugs/limitations", parse_hacks), OPT_BOOLEAN('i', "ibt", &opts.ibt, "validate and annotate IBT"), @@ -84,7 +83,7 @@ static const struct option check_options[] = { OPT_BOOLEAN('r', "retpoline", &opts.retpoline, "validate and annotate retpoline usage"), OPT_BOOLEAN(0, "rethunk", &opts.rethunk, "validate and annotate rethunk usage"), OPT_BOOLEAN(0, "unret", &opts.unret, "validate entry unret placement"), - OPT_INTEGER(0, "prefix", &opts.prefix, "generate prefix symbols"), + OPT_INTEGER(0, "prefix", &opts.prefix, "generate or grow prefix symbols for N-byte function padding"), OPT_BOOLEAN('l', "sls", &opts.sls, "validate straight-line-speculation mitigations"), OPT_BOOLEAN('s', "stackval", &opts.stackval, "validate frame pointer rules"), OPT_BOOLEAN('t', "static-call", &opts.static_call, "annotate static calls"), @@ -92,6 +91,8 @@ static const struct option check_options[] = { OPT_CALLBACK_OPTARG(0, "dump", NULL, NULL, "orc", "dump metadata", parse_dump), OPT_GROUP("Options:"), + OPT_BOOLEAN(0, "cfi", &opts.cfi, "grow kCFI preamble symbols (use with --prefix)"), + OPT_BOOLEAN(0, "fineibt", &opts.fineibt, "create .cfi_sites section for FineIBT"), OPT_BOOLEAN(0, "backtrace", &opts.backtrace, "unwind on error"), OPT_BOOLEAN(0, "backup", &opts.backup, "create backup (.orig) file on warning/error"), OPT_BOOLEAN(0, "dry-run", &opts.dryrun, "don't write modifications"), @@ -163,6 +164,16 @@ static bool opts_valid(void) return false; } + if (opts.cfi && !opts.prefix) { + ERROR("--cfi requires --prefix"); + return false; + } + + if (opts.fineibt && !opts.cfi) { + ERROR("--fineibt requires --cfi"); + return false; + } + if (opts.disas || opts.hack_jump_label || opts.hack_noinstr || diff --git a/tools/objtool/check.c b/tools/objtool/check.c index 4242582e941a..73e99bdfaaf6 100644 --- a/tools/objtool/check.c +++ b/tools/objtool/check.c @@ -881,6 +881,31 @@ static int create_ibt_endbr_seal_sections(struct objtool_file *file) return 0; } +/* +* Grow __cfi_ symbols to fill the NOP gap between the 'mov , %rax' and +* the start of the function. +*/ +static int grow_cfi_symbols(struct objtool_file *file) +{ + struct symbol *sym; + + for_each_sym(file->elf, sym) { + if (!is_func_sym(sym) || !strstarts(sym->name, "__cfi_") || + sym->len != 5) + continue; + + if (!find_func_by_offset(sym->sec, sym->offset + sym->len + opts.prefix)) + continue; + + sym->len += opts.prefix; + sym->sym.st_size = sym->len; + if (elf_write_symbol(file->elf, sym)) + return -1; + } + + return 0; +} + static int create_cfi_sections(struct objtool_file *file) { struct section *sec; @@ -4903,12 +4928,6 @@ int check(struct objtool_file *file) goto out; } - if (opts.cfi) { - ret = create_cfi_sections(file); - if (ret) - goto out; - } - if (opts.rethunk) { ret = create_return_sites_sections(file); if (ret) @@ -4928,9 +4947,21 @@ int check(struct objtool_file *file) } if (opts.prefix) { - ret = create_prefix_symbols(file); - if (ret) - goto out; + if (!opts.cfi) { + ret = create_prefix_symbols(file); + if (ret) + goto out; + } else { + ret = grow_cfi_symbols(file); + if (ret) + goto out; + + if (opts.fineibt) { + ret = create_cfi_sections(file); + if (ret) + goto out; + } + } } if (opts.ibt) { diff --git a/tools/objtool/elf.c b/tools/objtool/elf.c index d9cee8d5d9e8..33c95a74a51b 100644 --- a/tools/objtool/elf.c +++ b/tools/objtool/elf.c @@ -997,6 +997,26 @@ non_local: return sym; } +int elf_write_symbol(struct elf *elf, struct symbol *sym) +{ + struct section *symtab, *symtab_shndx; + + symtab = find_section_by_name(elf, ".symtab"); + if (!symtab) { + ERROR("no .symtab"); + return -1; + } + + symtab_shndx = find_section_by_name(elf, ".symtab_shndx"); + + if (elf_update_symbol(elf, symtab, symtab_shndx, sym)) + return -1; + + mark_sec_changed(elf, symtab, true); + + return 0; +} + struct symbol *elf_create_section_symbol(struct elf *elf, struct section *sec) { struct symbol *sym = calloc(1, sizeof(*sym)); diff --git a/tools/objtool/include/objtool/builtin.h b/tools/objtool/include/objtool/builtin.h index b9e229ed4dc0..e844e9c82b7b 100644 --- a/tools/objtool/include/objtool/builtin.h +++ b/tools/objtool/include/objtool/builtin.h @@ -9,8 +9,8 @@ struct opts { /* actions: */ - bool cfi; bool checksum; + const char *disas; bool dump_orc; bool hack_jump_label; bool hack_noinstr; @@ -20,6 +20,7 @@ struct opts { bool noabs; bool noinstr; bool orc; + int prefix; bool retpoline; bool rethunk; bool unret; @@ -27,14 +28,14 @@ struct opts { bool stackval; bool static_call; bool uaccess; - int prefix; - const char *disas; /* options: */ bool backtrace; bool backup; + bool cfi; const char *debug_checksum; bool dryrun; + bool fineibt; bool link; bool mnop; bool module; diff --git a/tools/objtool/include/objtool/elf.h b/tools/objtool/include/objtool/elf.h index e452784df702..305183f30a33 100644 --- a/tools/objtool/include/objtool/elf.h +++ b/tools/objtool/include/objtool/elf.h @@ -199,6 +199,7 @@ struct reloc *elf_init_reloc_data_sym(struct elf *elf, struct section *sec, struct symbol *sym, s64 addend); +int elf_write_symbol(struct elf *elf, struct symbol *sym); int elf_write_insn(struct elf *elf, struct section *sec, unsigned long offset, unsigned int len, const char *insn); -- cgit v1.2.3 From cab0cd0130eb6c884982ede3f70aca0392a7fc57 Mon Sep 17 00:00:00 2001 From: Frederic Weisbecker Date: Thu, 23 Apr 2026 18:53:54 +0200 Subject: scripts/timers: Add timer_migration_tree.py Introduce a script that provides a simple ascii representation of the timer migration tree on top of boot trace events. First boot with: trace_event==tmigr_connect_cpu_parent,tmigr_connect_child_parent Then parse the result with: scripts/timer_migration_tree.py < /sys/kernel/tracing/trace On a system with 8 CPUs, this produces the following output: Tree for capacity 1024 /-0, node 0, lvl:-1 | |--1, node 0, lvl:-1 | |--2, node 0, lvl:-1 | |--3, node 0, lvl:-1 -- /00000000dcebac8b, node 0, lvl:0 |--4, node 0, lvl:-1 | |--5, node 0, lvl:-1 | |--6, node 0, lvl:-1 | \-7, node 0, lvl:-1 Signed-off-by: Frederic Weisbecker Signed-off-by: Thomas Gleixner Link: https://patch.msgid.link/20260423165354.95152-7-frederic@kernel.org --- scripts/timer_migration_tree.py | 122 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100755 scripts/timer_migration_tree.py (limited to 'scripts') diff --git a/scripts/timer_migration_tree.py b/scripts/timer_migration_tree.py new file mode 100755 index 000000000000..faac9de854bd --- /dev/null +++ b/scripts/timer_migration_tree.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +""" +Draw the timer migration tree. + +1) Boot with trace_event==tmigr_connect_cpu_parent,tmigr_connect_child_parent +2) ./timer_migration_tree.py < /sys/kernel/tracing/trace +""" + +import re, sys +from ete3 import Tree + +class Node: + def __init__(self, group): + self.group = group + self.children = [] + self.parent = None + self.num_children = 0 + self.groupmask = 0 + self.lvl = -1 + + def set_groupmask(self, groupmask): + self.groupmask = groupmask + + def set_parent(self, parent): + self.parent = parent + + def add_child(self, child): + self.children.append(child) + + def set_lvl(self, lvl): + self.lvl = lvl + + def set_numa(self, numa): + self.numa = numa + + def set_num_children(self, num_children): + self.num_children = num_children + + def __repr__(self): + if self.parent: + parent_grp = self.parent.group + else: + parent_grp = "-" + return "Group: %s mask: %s parent: %s lvl: %d numa: %d num_children: %d" % (self.group, self.groupmask, parent_grp, self.lvl, self.numa, self.num_children) + +hierarchies = { } + +def get_hierarchy(capacity): + if capacity not in hierarchies: + hierarchies[capacity] = {} + return hierarchies[capacity] + +def get_node(capacity, group): + hier = get_hierarchy(capacity) + if group in hier: + return hier[group] + else: + n = Node(group) + hier[group] = n + return n + +def tmigr_connect_cpu_parent(ts, line): + s = re.search("tmigr_connect_cpu_parent: cpu=([0-9]+) groupmask=([0-9a-zA-Z]+) parent=([0-9a-zA-Z]+) lvl=([0-9]+) numa=([-]?[0-9]+) capacity=([-]?[0-9]+) num_children=([0-9]+)", line) + if s is None: + return False + (cpu, groupmask, parent, lvl, numa, capacity, num_children) = (int(s.group(1)), s.group(2), s.group(3), int(s.group(4)), int(s.group(5)), int(s.group(6)), int(s.group(7))) + n = get_node(capacity, cpu) + p = get_node(capacity, parent) + n.set_parent(p) + n.set_groupmask(groupmask) + n.set_lvl(-1) + p.set_lvl(lvl) + p.set_numa(numa) + n.set_numa(numa) + p.set_num_children(num_children) + p.add_child(n) + +def tmigr_connect_child_parent(ts, line): + s = re.search("tmigr_connect_child_parent: group=([0-9a-zA-Z]+) groupmask=([0-9a-zA-Z]+) parent=([0-9a-zA-Z]+) lvl=([0-9]+) numa=([-]?[0-9]+) capacity=([-]?[0-9]+) num_children=([0-9]+)", line) + if s is None: + return False + (group, groupmask, parent, lvl, numa, capacity, num_children) = (s.group(1), s.group(2), s.group(3), int(s.group(4)), int(s.group(5)), int(s.group(6)), int(s.group(7))) + n = get_node(capacity, group) + p = get_node(capacity, parent) + n.set_parent(p) + n.set_groupmask(groupmask) + p.set_lvl(lvl) + p.set_numa(numa) + p.set_num_children(num_children) + p.add_child(n) + +def populate(enode, node): + enode = enode.add_child(name = node.group) + enode.add_feature("groupmask", "m:%s" % node.groupmask) + enode.add_feature("lvl", "lvl:%d" % node.lvl) + enode.add_feature("numa", "node %d" % node.numa) + enode.add_feature("num_children", "c=%d" % node.num_children) + for child in node.children: + populate(enode, child) + +if __name__ == "__main__": + for line in sys.stdin: + s = re.search("([0-9]+[.][0-9]{6}): (.+?)$", line, re.S) + if s is not None: + if tmigr_connect_cpu_parent(float(s.group(1)), s.group(2)): + continue + if tmigr_connect_child_parent(float(s.group(1)), s.group(2)): + continue + + for cap in hierarchies: + h = hierarchies[cap] + print("Tree for capacity %d" % cap) + for k in h: + n = h[k] + while n.parent != None: + n = n.parent + root = Tree() + populate(root, n) + print(root.get_ascii(show_internal=True, attributes=["name", "numa", "lvl"])) + break -- cgit v1.2.3 From c416aee7e7d04fec2d2d30786b3c8393108b85d2 Mon Sep 17 00:00:00 2001 From: Illia Ostapyshyn Date: Mon, 27 Apr 2026 16:24:47 +0200 Subject: scripts/gdb: mm: cast untyped symbols in x86_page_ops The symbols phys_base, _text, and _end, used in x86_page_ops are either defined in assembly or implicitly by the linker. Thus, they lack type information and cause a conversion error after gdb.parse_and_eval. Explicitly cast these expressions to unsigned long. Link: https://lore.kernel.org/20260427142448.666117-2-illia@yshyn.com Fixes: 55f8b4518d14 ("scripts/gdb: implement x86_page_ops in mm.py") Signed-off-by: Illia Ostapyshyn Cc: Florian Fainelli Cc: Jan Kiszka Cc: Kieran Bingham Cc: Vlastimil Babka Cc: Hao Li Cc: Harry Yoo Cc: Seongjun Hong Cc: Signed-off-by: Andrew Morton --- scripts/gdb/linux/mm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'scripts') diff --git a/scripts/gdb/linux/mm.py b/scripts/gdb/linux/mm.py index d78908f6664d..dffadccbb01d 100644 --- a/scripts/gdb/linux/mm.py +++ b/scripts/gdb/linux/mm.py @@ -40,11 +40,11 @@ class x86_page_ops(): self.PAGE_OFFSET = int(gdb.parse_and_eval("page_offset_base")) self.VMEMMAP_START = int(gdb.parse_and_eval("vmemmap_base")) - self.PHYS_BASE = int(gdb.parse_and_eval("phys_base")) + self.PHYS_BASE = int(gdb.parse_and_eval("(unsigned long) phys_base")) self.START_KERNEL_map = 0xffffffff80000000 - self.KERNEL_START = gdb.parse_and_eval("_text") - self.KERNEL_END = gdb.parse_and_eval("_end") + self.KERNEL_START = gdb.parse_and_eval("(unsigned long) &_text") + self.KERNEL_END = gdb.parse_and_eval("(unsigned long) &_end") self.VMALLOC_START = int(gdb.parse_and_eval("vmalloc_base")) if self.VMALLOC_START == 0xffffc90000000000: -- cgit v1.2.3 From 228e25e33325865ebe589da5366449a8ecf7d0da Mon Sep 17 00:00:00 2001 From: Illia Ostapyshyn Date: Mon, 27 Apr 2026 16:24:48 +0200 Subject: scripts/gdb: slab: update field names of struct kmem_cache The commit 5ba6bc27b1f9 ("slab: decouple pointer to barn from kmem_cache_node") reorganized the struct kmem_cache to factor out the per-node fields to the new struct kmem_cache_per_node_ptrs. This causes the gdb scripts for lx-slabinfo and lx-slabtrace fail as they still reference the old structure. Adjust the gdb scripts to match the current state of struct kmem_cache. Link: https://lore.kernel.org/20260427142448.666117-3-illia@yshyn.com Fixes: 5ba6bc27b1f9 ("slab: decouple pointer to barn from kmem_cache_node") Signed-off-by: Illia Ostapyshyn Acked-by: Harry Yoo (Oracle) Acked-by: Vlastimil Babka (SUSE) Cc: Florian Fainelli Cc: Hao Li Cc: Jan Kiszka Cc: Kieran Bingham Cc: Seongjun Hong Signed-off-by: Andrew Morton --- scripts/gdb/linux/slab.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/gdb/linux/slab.py b/scripts/gdb/linux/slab.py index 0e2d93867fe2..ddde25aeca8d 100644 --- a/scripts/gdb/linux/slab.py +++ b/scripts/gdb/linux/slab.py @@ -196,7 +196,7 @@ def slabtrace(alloc, cache_name): if target_cache['flags'] & SLAB_STORE_USER: for i in range(0, nr_node_ids): - cache_node = target_cache['node'][i] + cache_node = target_cache['per_node']['node'][i] if cache_node['nr_slabs']['counter'] == 0: continue process_slab(loc_track, cache_node['partial'], alloc, target_cache) @@ -300,7 +300,7 @@ def slabinfo(): nr_free = 0 nr_slabs = 0 for i in range(0, nr_node_ids): - cache_node = cache['node'][i] + cache_node = cache['per_node']['node'][i] try: nr_slabs += cache_node['nr_slabs']['counter'] nr_objs = int(cache_node['total_objects']['counter']) -- cgit v1.2.3 From 2c31897a17e55a6da529b4e797e98c6febc60fd2 Mon Sep 17 00:00:00 2001 From: Thomas Weißschuh Date: Wed, 18 Mar 2026 21:37:20 +0100 Subject: kbuild: pacman-pkg: package unstripped vDSO libraries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unstripped vDSO files are useful for debugging. They are provided in the upstream 'linux-headers' package. Also package them as part of 'make pacman-pkg'. Make them part of the '-debug' package, as they fit there best. This differs from the upstream package as that has no '-debug' variant. Signed-off-by: Thomas Weißschuh Reviewed-by: Nathan Chancellor Tested-by: Nathan Chancellor Link: https://patch.msgid.link/20260318-kbuild-pacman-vdso-install-v1-1-48ceb31c0e80@weissschuh.net Signed-off-by: Nathan Chancellor --- scripts/package/PKGBUILD | 3 +++ 1 file changed, 3 insertions(+) (limited to 'scripts') diff --git a/scripts/package/PKGBUILD b/scripts/package/PKGBUILD index 452374d63c24..b1d0c8a9f030 100644 --- a/scripts/package/PKGBUILD +++ b/scripts/package/PKGBUILD @@ -121,6 +121,9 @@ _package-debug(){ install -Dt "${debugdir}" -m644 vmlinux mkdir -p "${builddir}" ln -sr "${debugdir}/vmlinux" "${builddir}/vmlinux" + + echo "Installing unstripped vDSO(s)..." + ${MAKE} INSTALL_MOD_PATH="${pkgdir}/usr" vdso_install } for _p in "${pkgname[@]}"; do -- cgit v1.2.3 From 49f8fcde68898f5033082e8155cd344dd54ef232 Mon Sep 17 00:00:00 2001 From: Hasan Basbunar Date: Tue, 5 May 2026 18:11:02 +0200 Subject: modpost: prevent stack buffer overflow in do_input_entry() and do_dmi_entry() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several functions in scripts/mod/file2alias.c build the module alias string by repeatedly appending into a fixed-size on-stack buffer: char alias[256] = {}; ... sprintf(alias + strlen(alias), "%X,*", i); This pattern is unbounded and silently corrupts the stack when the formatted output exceeds the destination size. Two functions in this file are realistically reachable with input that overflows their buffer: 1. do_input_entry() appends across nine bitmap classes (evbit/keybit/relbit/absbit/mscbit/ledbit/sndbit/ffbit/swbit). The keybit case alone scans bits from INPUT_DEVICE_ID_KEY_MIN_INTERESTING (0x71) to INPUT_DEVICE_ID_KEY_MAX (0x2ff), 655 iterations; if a MODULE_DEVICE_TABLE(input, ...) populates keybit[] densely, the emission reaches ~3132 bytes — overflowing the 256-byte buffer by about 12x. include/linux/mod_devicetable.h declares storage for the full bit range ("keybit[INPUT_DEVICE_ID_KEY_MAX / BITS_PER_LONG + 1]"), so the worst case is reachable per the ABI. 2. do_dmi_entry() emits one ":**" segment per matched DMI field, up to 4 matches per dmi_system_id. Each substr is sized as char[79] in struct dmi_strmatch (mod_devicetable.h:584), and dmi_ascii_filter() copies it verbatim into the alias buffer without bounds. Worst case: 4 × (1 + 3 + 1 + 79 + 1) = 336 bytes into alias[256], an 80-byte overflow. No driver in the current tree triggers either case — every in-tree INPUT_DEVICE_ID_MATCH_KEYBIT user populates keybit[] very sparsely (1-3 bits), and no in-tree dmi_system_id has four maximally-long matches. The concern is defense-in-depth: both unbounded sprintf chains are silent stack-corruption primitives in a host build tool, and the buffer sizes have not been revisited since the corresponding code was first introduced. The other do_*_entry() handlers in this file (do_usb_entry, do_cpu_entry, do_typec_entry, ...) were audited and are bounded by their input field sizes (uint16 IDs, fixed-length keys); their alias buffers do not need this treatment. Reproduced under AddressSanitizer with a stand-alone harness mirroring do_input on a fully-populated keybit: ==18319==ERROR: AddressSanitizer: stack-buffer-overflow WRITE of size 2 at offset 288 in frame [32, 288) 'alias' #6 do_input poc.c:44 Stack-canary build: Abort trap: 6 (strlen(alias)=3134, cap was 256-1) Add a small alias_append() helper around vsnprintf with a remaining- space check and call fatal() on overflow, matching the modpost style for unrecoverable build conditions. do_input() takes the buffer size as a new parameter; do_input_entry() and do_dmi_entry() pass sizeof(alias) at every call site. dmi_ascii_filter() takes the remaining buffer size as well and aborts on truncation. This bounds every write into the on-stack buffers and turns the latent overflow into a clean build error if it is ever reached. Fixes: 1d8f430c15b3 ("[PATCH] Input: add modalias support") Reviewed-by: Randy Dunlap Tested-by: Randy Dunlap Signed-off-by: Hasan Basbunar Link: https://patch.msgid.link/20260505161102.44087-1-basbunarhasan@gmail.com Signed-off-by: Nicolas Schier --- scripts/mod/file2alias.c | 79 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 26 deletions(-) (limited to 'scripts') diff --git a/scripts/mod/file2alias.c b/scripts/mod/file2alias.c index 4e99393a35f1..2ad87a74bb03 100644 --- a/scripts/mod/file2alias.c +++ b/scripts/mod/file2alias.c @@ -651,7 +651,26 @@ static void do_vio_entry(struct module *mod, void *symval) module_alias_printf(mod, true, "%s", alias); } -static void do_input(char *alias, +static void __attribute__((format(printf, 3, 4))) +alias_append(char *alias, size_t size, const char *fmt, ...) +{ + size_t len = strlen(alias); + va_list args; + int n; + + if (len >= size) + fatal("alias buffer (%zu) overflow before append\n", size); + + va_start(args, fmt); + n = vsnprintf(alias + len, size - len, fmt, args); + va_end(args); + + if (n < 0 || (size_t)n >= size - len) + fatal("alias buffer (%zu) overflow on append (need %d, have %zu)\n", + size, n, size - len); +} + +static void do_input(char *alias, size_t size, kernel_ulong_t *arr, unsigned int min, unsigned int max) { unsigned int i; @@ -659,13 +678,14 @@ static void do_input(char *alias, for (i = min; i <= max; i++) if (get_unaligned_native(arr + i / BITS_PER_LONG) & (1ULL << (i % BITS_PER_LONG))) - sprintf(alias + strlen(alias), "%X,*", i); + alias_append(alias, size, "%X,*", i); } /* input:b0v0p0e0-eXkXrXaXmXlXsXfXwX where X is comma-separated %02X. */ static void do_input_entry(struct module *mod, void *symval) { char alias[256] = {}; + const size_t sizeof_alias = sizeof(alias); DEF_FIELD(symval, input_device_id, flags); DEF_FIELD(symval, input_device_id, bustype); @@ -687,35 +707,35 @@ static void do_input_entry(struct module *mod, void *symval) ADD(alias, "p", flags & INPUT_DEVICE_ID_MATCH_PRODUCT, product); ADD(alias, "e", flags & INPUT_DEVICE_ID_MATCH_VERSION, version); - sprintf(alias + strlen(alias), "-e*"); + alias_append(alias, sizeof_alias, "-e*"); if (flags & INPUT_DEVICE_ID_MATCH_EVBIT) - do_input(alias, *evbit, 0, INPUT_DEVICE_ID_EV_MAX); - sprintf(alias + strlen(alias), "k*"); + do_input(alias, sizeof_alias, *evbit, 0, INPUT_DEVICE_ID_EV_MAX); + alias_append(alias, sizeof_alias, "k*"); if (flags & INPUT_DEVICE_ID_MATCH_KEYBIT) - do_input(alias, *keybit, + do_input(alias, sizeof_alias, *keybit, INPUT_DEVICE_ID_KEY_MIN_INTERESTING, INPUT_DEVICE_ID_KEY_MAX); - sprintf(alias + strlen(alias), "r*"); + alias_append(alias, sizeof_alias, "r*"); if (flags & INPUT_DEVICE_ID_MATCH_RELBIT) - do_input(alias, *relbit, 0, INPUT_DEVICE_ID_REL_MAX); - sprintf(alias + strlen(alias), "a*"); + do_input(alias, sizeof_alias, *relbit, 0, INPUT_DEVICE_ID_REL_MAX); + alias_append(alias, sizeof_alias, "a*"); if (flags & INPUT_DEVICE_ID_MATCH_ABSBIT) - do_input(alias, *absbit, 0, INPUT_DEVICE_ID_ABS_MAX); - sprintf(alias + strlen(alias), "m*"); + do_input(alias, sizeof_alias, *absbit, 0, INPUT_DEVICE_ID_ABS_MAX); + alias_append(alias, sizeof_alias, "m*"); if (flags & INPUT_DEVICE_ID_MATCH_MSCIT) - do_input(alias, *mscbit, 0, INPUT_DEVICE_ID_MSC_MAX); - sprintf(alias + strlen(alias), "l*"); + do_input(alias, sizeof_alias, *mscbit, 0, INPUT_DEVICE_ID_MSC_MAX); + alias_append(alias, sizeof_alias, "l*"); if (flags & INPUT_DEVICE_ID_MATCH_LEDBIT) - do_input(alias, *ledbit, 0, INPUT_DEVICE_ID_LED_MAX); - sprintf(alias + strlen(alias), "s*"); + do_input(alias, sizeof_alias, *ledbit, 0, INPUT_DEVICE_ID_LED_MAX); + alias_append(alias, sizeof_alias, "s*"); if (flags & INPUT_DEVICE_ID_MATCH_SNDBIT) - do_input(alias, *sndbit, 0, INPUT_DEVICE_ID_SND_MAX); - sprintf(alias + strlen(alias), "f*"); + do_input(alias, sizeof_alias, *sndbit, 0, INPUT_DEVICE_ID_SND_MAX); + alias_append(alias, sizeof_alias, "f*"); if (flags & INPUT_DEVICE_ID_MATCH_FFBIT) - do_input(alias, *ffbit, 0, INPUT_DEVICE_ID_FF_MAX); - sprintf(alias + strlen(alias), "w*"); + do_input(alias, sizeof_alias, *ffbit, 0, INPUT_DEVICE_ID_FF_MAX); + alias_append(alias, sizeof_alias, "w*"); if (flags & INPUT_DEVICE_ID_MATCH_SWBIT) - do_input(alias, *swbit, 0, INPUT_DEVICE_ID_SW_MAX); + do_input(alias, sizeof_alias, *swbit, 0, INPUT_DEVICE_ID_SW_MAX); module_alias_printf(mod, false, "input:%s", alias); } @@ -895,12 +915,16 @@ static const struct dmifield { { NULL, DMI_NONE } }; -static void dmi_ascii_filter(char *d, const char *s) +static void dmi_ascii_filter(char *d, size_t avail, const char *s) { /* Filter out characters we don't want to see in the modalias string */ for (; *s; s++) - if (*s > ' ' && *s < 127 && *s != ':') + if (*s > ' ' && *s < 127 && *s != ':') { + if (avail <= 1) + fatal("%s: alias buffer overflow\n", __func__); *(d++) = *s; + avail--; + } *d = 0; } @@ -909,6 +933,8 @@ static void dmi_ascii_filter(char *d, const char *s) static void do_dmi_entry(struct module *mod, void *symval) { char alias[256] = {}; + const size_t sizeof_alias = sizeof(alias); + size_t len; int i, j; DEF_FIELD_ADDR(symval, dmi_system_id, matches); @@ -916,11 +942,12 @@ static void do_dmi_entry(struct module *mod, void *symval) for (j = 0; j < 4; j++) { if ((*matches)[j].slot && (*matches)[j].slot == dmi_fields[i].field) { - sprintf(alias + strlen(alias), ":%s*", - dmi_fields[i].prefix); - dmi_ascii_filter(alias + strlen(alias), + alias_append(alias, sizeof_alias, ":%s*", + dmi_fields[i].prefix); + len = strlen(alias); + dmi_ascii_filter(alias + len, sizeof_alias - len, (*matches)[j].substr); - strcat(alias, "*"); + alias_append(alias, sizeof_alias, "*"); } } } -- cgit v1.2.3 From 202550713128da20d9381d6d2dc0f6b73839f434 Mon Sep 17 00:00:00 2001 From: Viktor Jägersküpper Date: Fri, 15 May 2026 23:58:45 +0200 Subject: kbuild: pacman-pkg: make "rc" releases adhere to pacman versioning scheme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The package versioning scheme does not enable smooth upgrades from "rc" releases to the corresponding stable releases (e.g. 7.0.0-rc7 -> 7.0.0) because pacman considers that a downgrade due to the underscore in pkgver (e.g. 7.0.0_rc7), see e.g. vercmp(8) for an explanation of the package version comparison used by pacman. Package versions which are derived from said releases (e.g. built from git revisions) are similarly affected. Fix this by modifying pkgver in order to remove the hyphen from kernel versions containing "-rcN", where N is a non-negative integer. Acked-by: Thomas Weißschuh Signed-off-by: Viktor Jägersküpper Reviewed-by: Nathan Chancellor Tested-by: Nathan Chancellor Link: https://patch.msgid.link/20260515215913.92481-1-viktor_jaegerskuepper@freenet.de Fixes: c8578539deba ("kbuild: add script and target to generate pacman package") Signed-off-by: Nicolas Schier --- scripts/package/PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/package/PKGBUILD b/scripts/package/PKGBUILD index 452374d63c24..1213c8e04671 100644 --- a/scripts/package/PKGBUILD +++ b/scripts/package/PKGBUILD @@ -10,7 +10,7 @@ for pkg in $_extrapackages; do pkgname+=("${pkgbase}-${pkg}") done -pkgver="${KERNELRELEASE//-/_}" +pkgver="$(echo "${KERNELRELEASE}" | sed 's/-\(rc[0-9]\+\)/\1/;s/-/_/g')" # The PKGBUILD is evaluated multiple times. # Running scripts/build-version from here would introduce inconsistencies. pkgrel="${KBUILD_REVISION}" -- cgit v1.2.3 From e72b635ceaf7e8d5ad757169d5950c43adeb5261 Mon Sep 17 00:00:00 2001 From: Luis Augenstein Date: Mon, 18 May 2026 08:20:49 +0200 Subject: scripts/sbom: integrate script in make process integrate SBOM script into the kernel build process. Assisted-by: Cursor:claude-sonnet-4-5 Assisted-by: OpenCode:GLM-4-7 Co-developed-by: Maximilian Huber Signed-off-by: Maximilian Huber Signed-off-by: Luis Augenstein Acked-by: Nathan Chancellor Signed-off-by: Greg Kroah-Hartman --- .gitignore | 1 + MAINTAINERS | 6 ++++++ Makefile | 20 ++++++++++++++++++-- scripts/sbom/sbom.py | 16 ++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 scripts/sbom/sbom.py (limited to 'scripts') diff --git a/.gitignore b/.gitignore index 3044b9590f05..f0d35a9d591d 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ *.s *.so *.so.dbg +*.spdx.json *.su *.symtypes *.tab.[ch] diff --git a/MAINTAINERS b/MAINTAINERS index c2c6d79275c6..36dac854a21d 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23903,6 +23903,12 @@ R: Marc Murphy S: Supported F: arch/arm/boot/dts/ti/omap/am335x-sancloud* +SBOM +M: Luis Augenstein +M: Maximilian Huber +S: Maintained +F: scripts/sbom/ + SC1200 WDT DRIVER M: Zwane Mwaikambo S: Maintained diff --git a/Makefile b/Makefile index 9f59598d3a08..ec54f7d51cf4 100644 --- a/Makefile +++ b/Makefile @@ -787,7 +787,7 @@ endif # in addition to whatever we do anyway. # Just "make" or "make all" shall build modules as well -ifneq ($(filter all modules nsdeps compile_commands.json clang-%,$(MAKECMDGOALS)),) +ifneq ($(filter all modules nsdeps compile_commands.json clang-% sbom,$(MAKECMDGOALS)),) KBUILD_MODULES := y endif @@ -1692,7 +1692,7 @@ CLEAN_FILES += vmlinux.symvers modules-only.symvers \ modules.builtin.ranges vmlinux.o.map vmlinux.unstripped \ compile_commands.json rust/test \ rust-project.json .vmlinux.objs .vmlinux.export.c \ - .builtin-dtbs-list .builtin-dtbs.S + .builtin-dtbs-list .builtin-dtbs.S sbom-*.spdx.json # Directories & files removed with 'make mrproper' MRPROPER_FILES += include/config include/generated \ @@ -1811,6 +1811,7 @@ help: @echo '' @echo 'Tools:' @echo ' nsdeps - Generate missing symbol namespace dependencies' + @echo ' sbom - Generate Software Bill of Materials' @echo '' @echo 'Kernel selftest:' @echo ' kselftest - Build and run kernel selftest' @@ -2197,6 +2198,21 @@ nsdeps: export KBUILD_NSDEPS=1 nsdeps: modules $(Q)$(CONFIG_SHELL) $(srctree)/scripts/nsdeps +# Script to generate .spdx.json SBOM documents describing the build +# --------------------------------------------------------------------------- + +ifdef building_out_of_srctree +sbom_targets := sbom-source.spdx.json +endif +sbom_targets += sbom-build.spdx.json sbom-output.spdx.json +quiet_cmd_sbom = GEN $(sbom_targets) + cmd_sbom = printf "%s\n" "$(KBUILD_IMAGE)" >"$(tmp-target)"; \ + $(if $(CONFIG_MODULES),sed 's/\.o$$/.ko/' $(objtree)/modules.order >> "$(tmp-target)";) \ + $(PYTHON3) $(srctree)/scripts/sbom/sbom.py; +PHONY += sbom +sbom: $(notdir $(KBUILD_IMAGE)) include/generated/autoconf.h $(if $(CONFIG_MODULES),modules modules.order) + $(call cmd,sbom) + # Clang Tooling # --------------------------------------------------------------------------- diff --git a/scripts/sbom/sbom.py b/scripts/sbom/sbom.py new file mode 100644 index 000000000000..9c2e4c7f17ce --- /dev/null +++ b/scripts/sbom/sbom.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +""" +Compute software bill of materials in SPDX format describing a kernel build. +""" + + +def main(): + pass + + +# Call main method +if __name__ == "__main__": + main() -- cgit v1.2.3 From 3fd79200835f382261a7b53fed659625b22484af Mon Sep 17 00:00:00 2001 From: Luis Augenstein Date: Mon, 18 May 2026 08:20:50 +0200 Subject: scripts/sbom: setup sbom logging Add logging infrastructure for warnings and errors. Errors and warnings are accumulated and summarized in the end. Assisted-by: Cursor:claude-sonnet-4-5 Assisted-by: OpenCode:GLM-4-7 Co-developed-by: Maximilian Huber Signed-off-by: Maximilian Huber Signed-off-by: Luis Augenstein Signed-off-by: Greg Kroah-Hartman --- scripts/sbom/sbom.py | 26 ++++++++++- scripts/sbom/sbom/__init__.py | 0 scripts/sbom/sbom/config.py | 46 +++++++++++++++++++ scripts/sbom/sbom/sbom_logging.py | 94 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 scripts/sbom/sbom/__init__.py create mode 100644 scripts/sbom/sbom/config.py create mode 100644 scripts/sbom/sbom/sbom_logging.py (limited to 'scripts') diff --git a/scripts/sbom/sbom.py b/scripts/sbom/sbom.py index 9c2e4c7f17ce..3bd466720b0d 100644 --- a/scripts/sbom/sbom.py +++ b/scripts/sbom/sbom.py @@ -6,9 +6,33 @@ Compute software bill of materials in SPDX format describing a kernel build. """ +import logging +import sys +import sbom.sbom_logging as sbom_logging +from sbom.config import get_config + + +def _exit_with_summary(write_output_on_error: bool = False) -> None: + warning_summary = sbom_logging.summarize_warnings() + error_summary = sbom_logging.summarize_errors() + if warning_summary: + logging.warning(warning_summary) + if error_summary: + logging.error(error_summary) + sys.exit(1) + def main(): - pass + # Read config + config = get_config() + + # Configure logging + logging.basicConfig( + level=logging.DEBUG if config.debug else logging.INFO, + format="[%(levelname)s] %(message)s", + ) + + _exit_with_summary(config.write_output_on_error) # Call main method diff --git a/scripts/sbom/sbom/__init__.py b/scripts/sbom/sbom/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/scripts/sbom/sbom/config.py b/scripts/sbom/sbom/config.py new file mode 100644 index 000000000000..c1ac9ad5737f --- /dev/null +++ b/scripts/sbom/sbom/config.py @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +import argparse +from dataclasses import dataclass + + +@dataclass +class KernelSbomConfig: + debug: bool + """Whether to enable debug logging.""" + + +def _parse_cli_arguments(parser: argparse.ArgumentParser) -> dict[str, bool]: + """ + Parse command-line arguments using argparse. + + Returns: + Dictionary of parsed arguments. + """ + parser.add_argument( + "--debug", + action="store_true", + default=False, + help="Enable debug logs (default: False)", + ) + + args = vars(parser.parse_args()) + return args + + +def get_config() -> KernelSbomConfig: + """ + Parse command-line arguments and construct the configuration object. + + Returns: + KernelSbomConfig: Configuration object with all settings for SBOM generation. + """ + parser = argparse.ArgumentParser( + description="Generate SPDX SBOM documents for kernel builds", + ) + args = _parse_cli_arguments(parser) + + debug = args["debug"] + + return KernelSbomConfig(debug=debug) diff --git a/scripts/sbom/sbom/sbom_logging.py b/scripts/sbom/sbom/sbom_logging.py new file mode 100644 index 000000000000..fbc53cc77ef4 --- /dev/null +++ b/scripts/sbom/sbom/sbom_logging.py @@ -0,0 +1,94 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +import logging +import inspect +from typing import Literal + + +MessageTemplate = str + + +class MessageLogger: + """Logger that suppresses repeated messages and stores a summary of all logged messages.""" + + _messages: dict[MessageTemplate, list[str]] + _message_counts: dict[MessageTemplate, int] + _repeated_logs_limit: int + """Maximum number of repeated messages of the same type to log before suppressing further output.""" + + def __init__(self, level: Literal["error", "warning"], repeated_logs_limit: int = 3) -> None: + self._level = level + self._messages = {} + self._message_counts = {} + self._repeated_logs_limit = repeated_logs_limit + + def log(self, template: MessageTemplate, /, **kwargs: str) -> None: + """Log a message based on a template and optional variables. Example: `log("Missing {path}", path=str(p))`.""" + message = template + for key, value in kwargs.items(): + message = message.replace("{" + key + "}", value) + if template not in self._messages: + self._messages[template] = [] + self._message_counts[template] = 0 + self._message_counts[template] += 1 + if self._message_counts[template] <= self._repeated_logs_limit: + if self._level == "error": + logging.error(message) + elif self._level == "warning": + logging.warning(message) + self._messages[template].append(message) + + def get_summary(self) -> str: + if len(self._messages) == 0: + return "" + summary: list[str] = [f"Summarize {self._level}s:"] + for template, messages in self._messages.items(): + for message in messages: + summary.append(message) + n_suppressed_messages = self._message_counts[template] - self._repeated_logs_limit + if n_suppressed_messages > 0: + instances = "instance" if n_suppressed_messages == 1 else "instances" + summary.append(f"... (Found {n_suppressed_messages} more {instances} of this {self._level})") + return "\n".join(summary) + + def has_messages(self) -> bool: + return len(self._message_counts) > 0 + + +_warning_logger: MessageLogger +_error_logger: MessageLogger + + +def warning(msg_template: MessageTemplate, /, **kwargs: str) -> None: + _warning_logger.log(msg_template, **kwargs) + + +def error(msg_template: MessageTemplate, /, **kwargs: str) -> None: + frame = inspect.currentframe() + caller_frame = frame.f_back if frame else None + info = inspect.getframeinfo(caller_frame) if caller_frame else None + if info: + msg_template = f'File "{info.filename}", line {info.lineno}, in {info.function}\n{msg_template}' + _error_logger.log(msg_template, **kwargs) + + +def summarize_warnings() -> str: + return _warning_logger.get_summary() + + +def summarize_errors() -> str: + return _error_logger.get_summary() + + +def has_errors() -> bool: + return _error_logger.has_messages() + + +def init() -> None: + global _warning_logger, _error_logger + _warning_logger = MessageLogger("warning") + _error_logger = MessageLogger("error") + + +init() -- cgit v1.2.3 From a1a248adf1b0d79e9386d007cbcd4be85d643f03 Mon Sep 17 00:00:00 2001 From: Luis Augenstein Date: Mon, 18 May 2026 08:20:51 +0200 Subject: scripts/sbom: add command parsers Implement savedcmd_parser module for extracting input files from kernel build commands. Assisted-by: Cursor:claude-sonnet-4-5 Assisted-by: OpenCode:GLM-4-7 Co-developed-by: Maximilian Huber Signed-off-by: Maximilian Huber Signed-off-by: Luis Augenstein Signed-off-by: Greg Kroah-Hartman --- .../sbom/cmd_graph/savedcmd_parser/__init__.py | 6 + .../savedcmd_parser/command_parser_registry.py | 516 +++++++++++++++++++++ .../cmd_graph/savedcmd_parser/command_splitter.py | 128 +++++ .../cmd_graph/savedcmd_parser/savedcmd_parser.py | 67 +++ .../sbom/cmd_graph/savedcmd_parser/tokenizer.py | 92 ++++ scripts/sbom/sbom/environment.py | 192 ++++++++ 6 files changed, 1001 insertions(+) create mode 100644 scripts/sbom/sbom/cmd_graph/savedcmd_parser/__init__.py create mode 100644 scripts/sbom/sbom/cmd_graph/savedcmd_parser/command_parser_registry.py create mode 100644 scripts/sbom/sbom/cmd_graph/savedcmd_parser/command_splitter.py create mode 100644 scripts/sbom/sbom/cmd_graph/savedcmd_parser/savedcmd_parser.py create mode 100644 scripts/sbom/sbom/cmd_graph/savedcmd_parser/tokenizer.py create mode 100644 scripts/sbom/sbom/environment.py (limited to 'scripts') diff --git a/scripts/sbom/sbom/cmd_graph/savedcmd_parser/__init__.py b/scripts/sbom/sbom/cmd_graph/savedcmd_parser/__init__.py new file mode 100644 index 000000000000..d13876af4dfd --- /dev/null +++ b/scripts/sbom/sbom/cmd_graph/savedcmd_parser/__init__.py @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from sbom.cmd_graph.savedcmd_parser.savedcmd_parser import parse_inputs_from_commands + +__all__ = ["parse_inputs_from_commands"] diff --git a/scripts/sbom/sbom/cmd_graph/savedcmd_parser/command_parser_registry.py b/scripts/sbom/sbom/cmd_graph/savedcmd_parser/command_parser_registry.py new file mode 100644 index 000000000000..a48040b2c13c --- /dev/null +++ b/scripts/sbom/sbom/cmd_graph/savedcmd_parser/command_parser_registry.py @@ -0,0 +1,516 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +import re +import shlex +from typing import Callable, Iterator + +import sbom.sbom_logging as sbom_logging +from sbom.environment import Environment +from sbom.cmd_graph.savedcmd_parser.command_splitter import IfBlock, split_commands +from sbom.cmd_graph.savedcmd_parser.tokenizer import ( + CmdParsingError, + Option, + Positional, + tokenize_single_command, + tokenize_single_command_positionals_only, +) +from sbom.path_utils import PathStr + +CommandParser = Callable[[str], list[PathStr]] +CommandParserRegistryEntry = tuple[re.Pattern[str], CommandParser] + + +def _parse_dd_command(command: str) -> list[PathStr]: + match = re.match(r"dd.*?if=(\S+)", command) + if match: + return [match.group(1)] + return [] + + +def _parse_cat_command(command: str) -> list[PathStr]: + positionals = tokenize_single_command_positionals_only(command) + # expect positionals to be ["cat", input1, input2, ...] + return [p for p in positionals[1:]] + + +def _parse_compound_command(command: str) -> list[PathStr]: + compound_command_parsers: list[CommandParserRegistryEntry] = [ + (re.compile(r"dd\b"), _parse_dd_command), + (re.compile(r"cat.*?\|"), lambda c: _parse_cat_command(c.split("|")[0])), + (re.compile(r"cat\b[^|>]*$"), _parse_cat_command), + (re.compile(r"echo\b"), _parse_noop), + (re.compile(r"\S+="), _parse_noop), + (re.compile(r"printf\b"), _parse_noop), + (re.compile(r"sed\b"), _parse_sed_command), + ( + re.compile(r"(.*/)scripts/bin2c\s*<"), + lambda c: [input] if (input := c.split("<")[1].split(">")[0].strip()) != "/dev/null" else [], + ), + (re.compile(r"^:$"), _parse_noop), + ] + + match = re.match(r"\s*[\(\{](.*)[\)\}]\s*>", command, re.DOTALL) + if match is None: + raise CmdParsingError("No inner commands found for compound command") + input_files: list[PathStr] = [] + inner_commands = split_commands(match.group(1)) + for inner_command in inner_commands: + if isinstance(inner_command, IfBlock): + sbom_logging.error( + "Skip parsing inner command {inner_command} of compound command because IfBlock is not supported", + inner_command=inner_command, + ) + continue + + parser = next((parser for pattern, parser in compound_command_parsers if pattern.match(inner_command)), None) + if parser is None: + sbom_logging.error( + "Skip parsing inner command {inner_command} of compound command because no matching parser was found", + inner_command=inner_command, + ) + continue + try: + input_files += parser(inner_command) + except (CmdParsingError, IndexError) as e: + sbom_logging.error( + "Skip parsing inner command {inner_command} of compound command because of command parsing error: {error_message}", + inner_command=inner_command, + error_message=str(e), + ) + return input_files + + +def _parse_objcopy_command(command: str) -> list[PathStr]: + command_parts = tokenize_single_command(command, flag_options=["-S", "-w"]) + positionals = [part.value for part in command_parts if isinstance(part, Positional)] + # expect positionals to be ['objcopy', input_file] or ['objcopy', input_file, output_file] + return [positionals[1]] + + +def _parse_link_vmlinux_command(command: str) -> list[PathStr]: + """ + For simplicity we do not parse the `scripts/link-vmlinux.sh` script. + Instead the `vmlinux.a` dependency is just hardcoded for now. + """ + return ["vmlinux.a"] + + +def _parse_cp_command(command: str) -> list[PathStr]: + positionals = tokenize_single_command_positionals_only(command) + # expect positionals to be ["cp", input1, ..., destination] + return positionals[1:-1] + + +def _parse_noop(command: str) -> list[PathStr]: + """ + No-op parser for commands with no input files (e.g., 'rm', 'true'). + Returns an empty list. + """ + return [] + + +def _parse_ar_command(command: str) -> list[PathStr]: + positionals = tokenize_single_command_positionals_only(command) + # expect positionals to be ['ar', flags, output, input1, input2, ...] + flags = positionals[1] + if "r" not in flags: + # 'r' option indicates that new files are added to the archive. + # If this option is missing we won't find any relevant input files. + return [] + return positionals[3:] + + +def _parse_ar_piped_xargs_command(command: str) -> list[PathStr]: + printf_command, _ = command.split("|", 1) + positionals = tokenize_single_command_positionals_only(printf_command.strip()) + # expect positionals to be ['printf', '{prefix_path}%s ', input1, input2, ...] + prefix_path = positionals[1].removesuffix("%s ") + return [f"{prefix_path}{filename}" for filename in positionals[2:]] + + +def _parse_gcc_or_clang_command(command: str) -> list[PathStr]: + parts = shlex.split(command) + # compile mode: expect last positional argument ending in a source file extension to be the input file + for part in reversed(parts): + if not part.startswith("-") and any(part.endswith(suffix) for suffix in [".c", ".S", ".dts"]): + return [part] + + # linking mode: expect all .o files to be the inputs + return [p for p in parts if p.endswith(".o")] + + +def _parse_rustc_command(command: str) -> list[PathStr]: + parts = shlex.split(command) + # expect last positional argument ending in `.rs` to be the input file + for part in reversed(parts): + if not part.startswith("-") and part.endswith(".rs"): + return [part] + raise CmdParsingError("Could not find .rs input source file") + + +def _parse_rustdoc_command(command: str) -> list[PathStr]: + parts = shlex.split(command) + # expect last positional argument ending in `.rs` to be the input file + for part in reversed(parts): + if not part.startswith("-") and part.endswith(".rs"): + return [part] + raise CmdParsingError("Could not find .rs input source file") + + +def _parse_syscallhdr_command(command: str) -> list[PathStr]: + command_parts = tokenize_single_command(command.strip(), flag_options=["--emit-nr"]) + positionals = [p.value for p in command_parts if isinstance(p, Positional)] + # expect positionals to be ["sh", path/to/syscallhdr.sh, input, output] + return [positionals[2]] + + +def _parse_syscalltbl_command(command: str) -> list[PathStr]: + command_parts = tokenize_single_command(command.strip()) + positionals = [p.value for p in command_parts if isinstance(p, Positional)] + # expect positionals to be ["sh", path/to/syscalltbl.sh, input, output] + return [positionals[2]] + + +def _parse_mkcapflags_command(command: str) -> list[PathStr]: + positionals = tokenize_single_command_positionals_only(command) + # expect positionals to be ["sh", path/to/mkcapflags.sh, output, input1, input2] + return [positionals[3], positionals[4]] + + +def _parse_orc_hash_command(command: str) -> list[PathStr]: + positionals = tokenize_single_command_positionals_only(command) + # expect positionals to be ["sh", path/to/orc_hash.sh, '<', input, '>', output] + return [positionals[3]] + + +def _parse_xen_hypercalls_command(command: str) -> list[PathStr]: + positionals = tokenize_single_command_positionals_only(command) + # expect positionals to be ["sh", path/to/xen-hypercalls.sh, output, input1, input2, ...] + return positionals[3:] + + +def _parse_gen_initramfs_command(command: str) -> list[PathStr]: + command_parts = tokenize_single_command(command) + positionals = [p.value for p in command_parts if isinstance(p, Positional)] + # expect positionals to be ["sh", path/to/gen_initramfs.sh, input1, input2, ...] + return positionals[2:] + + +def _parse_vdso2c_command(command: str) -> list[PathStr]: + positionals = tokenize_single_command_positionals_only(command) + # expect positionals to be ['vdso2c', raw_input, stripped_input, output] + return [positionals[1], positionals[2]] + + +def _parse_vdsomunge_command(command: str) -> list[PathStr]: + positionals = tokenize_single_command_positionals_only(command) + # expect positionals to be ['vdsomunge', input, output] + return [positionals[1]] + + +def _parse_ld_command(command: str) -> list[PathStr]: + command_parts = tokenize_single_command( + command=command.strip(), + flag_options=[ + "-shared", + "--no-undefined", + "--eh-frame-hdr", + "-Bsymbolic", + "-r", + "--no-ld-generated-unwind-info", + "--no-dynamic-linker", + "-pie", + "--no-dynamic-linker--whole-archive", + "--whole-archive", + "--no-whole-archive", + "--start-group", + "--end-group", + ], + ) + positionals = [p.value for p in command_parts if isinstance(p, Positional)] + # expect positionals to be ["ld", input1, input2, ...] + return positionals[1:] + + +def _parse_sed_command(command: str) -> list[PathStr]: + command_parts = shlex.split(command) + # expect command parts to be ["sed", *, input] + input = command_parts[-1] + if input == "/dev/null": + return [] + return [input] + + +def _parse_awk(command: str) -> list[PathStr]: + command_parts = tokenize_single_command(command) + options = [p for p in command_parts if isinstance(p, Option)] + positionals = [p.value for p in command_parts if isinstance(p, Positional)] + has_script_file = any(p.name == "-f" for p in options) + # With -f option: expect ["awk", input1, input2, ...] + # Without -f option: expect ["awk", inline_program, input1, input2, ...] + return positionals[1:] if has_script_file else positionals[2:] + + +def _parse_nm_piped_command(command: str) -> list[PathStr]: + nm_command, _ = command.split("|", 1) + command_parts = tokenize_single_command( + command=nm_command.strip(), + flag_options=["-p", "--defined-only"], + ) + positionals = [p.value for p in command_parts if isinstance(p, Positional)] + # expect positionals to be ["nm", input1, input2, ...] + return [p for p in positionals[1:]] + + +def _parse_pnm_to_logo_command(command: str) -> list[PathStr]: + command_parts = shlex.split(command) + # expect command parts to be ["pnmtologo", , input] + return [command_parts[-1]] + + +def _parse_relacheck(command: str) -> list[PathStr]: + positionals = tokenize_single_command_positionals_only(command) + # expect positionals to be ["relacheck", input, log_reference] + return [positionals[1]] + + +def _parse_gen_hyprel_command(command: str) -> list[PathStr]: + gen_hyprel_command, _ = command.split(">", 1) + command_parts = shlex.split(gen_hyprel_command) + # expect command_parts to be ["gen-hyprel", input] + return [command_parts[1]] + + +def _parse_perl_command(command: str) -> list[PathStr]: + positionals = tokenize_single_command_positionals_only(command.strip()) + # expect positionals to be ["perl", input] + return [positionals[1]] + + +def _parse_strip_command(command: str) -> list[PathStr]: + command_parts = tokenize_single_command(command, flag_options=["--strip-debug"]) + positionals = [p.value for p in command_parts if isinstance(p, Positional)] + # expect positionals to be ["strip", input1, input2, ...] + return positionals[1:] + + +def _parse_mkpiggy_command(command: str) -> list[PathStr]: + mkpiggy_command, _ = command.split(">", 1) + positionals = tokenize_single_command_positionals_only(mkpiggy_command) + # expect positionals to be ["mkpiggy", input] + return [positionals[1]] + + +def _parse_relocs_command(command: str) -> list[PathStr]: + if ">" not in command: + # Only consider relocs commands that redirect output to a file. + # If there's no redirection, we assume it produces no output file and therefore has no input we care about. + return [] + relocs_command, _ = command.split(">", 1) + command_parts = shlex.split(relocs_command) + # expect command_parts to be ["relocs", options, input] + return [command_parts[-1]] + + +def _parse_mk_elfconfig_command(command: str) -> list[PathStr]: + positionals = tokenize_single_command_positionals_only(command) + # expect positionals to be ["mk_elfconfig", "<", input, ">", output] + return [positionals[2]] + + +def _parse_flex_command(command: str) -> list[PathStr]: + parts = shlex.split(command) + # expect last positional argument ending in `.l` to be the input file + for part in reversed(parts): + if not part.startswith("-") and part.endswith(".l"): + return [part] + raise CmdParsingError("Could not find .l input source file in command") + + +def _parse_bison_command(command: str) -> list[PathStr]: + parts = shlex.split(command) + # expect last positional argument ending in `.y` to be the input file + for part in reversed(parts): + if not part.startswith("-") and part.endswith(".y"): + return [part] + raise CmdParsingError("Could not find input .y input source file in command") + + +def _parse_tools_build_command(command: str) -> list[PathStr]: + positionals = tokenize_single_command_positionals_only(command) + # expect positionals to be ["tools/build", "input1", "input2", "input3", "output"] + return positionals[1:-1] + + +def _parse_extract_cert_command(command: str) -> list[PathStr]: + command_parts = shlex.split(command) + # expect command parts to be [path/to/extract-cert, input, output] + input = command_parts[1] + if not input: + return [] + return [input] + + +def _parse_dtc_command(command: str) -> list[PathStr]: + wno_flags = [command_part for command_part in shlex.split(command) if command_part.startswith("-Wno-")] + command_parts = tokenize_single_command(command, flag_options=wno_flags) + positionals = [p.value for p in command_parts if isinstance(p, Positional)] + # expect positionals to be [path/to/dtc, input] + return [positionals[1]] + + +def _parse_bindgen_command(command: str) -> list[PathStr]: + command_parts = shlex.split(command) + header_file_input_paths = [part for part in command_parts if part.endswith(".h")] + return header_file_input_paths + + +def _parse_gen_header(command: str) -> list[PathStr]: + command_parts = shlex.split(command) + # expect command parts to be ["python3", path/to/gen_headers.py, ..., "--xml", input] + i = next((i for i, token in enumerate(command_parts) if token == "--xml"), None) + if i is None: + raise CmdParsingError(f"Expected --xml input file in gen_headers command but got {command}") + return [command_parts[i + 1]] + +def _parse_mkuboot_command(command: str) -> list[PathStr]: + command_parts = tokenize_single_command(command) + # mkuboot.sh passes all args to mkimage; -d specifies the data/input image file + for part in command_parts: + if isinstance(part, Option) and part.name == "-d" and part.value is not None: + return [part.value] + raise CmdParsingError("Could not find -d (data file) option in mkuboot.sh command") + + +def _parse_syscallnr_command(command: str) -> list[PathStr]: + command_parts = tokenize_single_command(command.strip()) + positionals = [p.value for p in command_parts if isinstance(p, Positional)] + # expect positionals to be ["sh", path/to/syscallnr.sh, input, output] + return [positionals[2]] + + +def _parse_gen_kernel_hwcaps_command(command: str) -> list[PathStr]: + command_parts = tokenize_single_command(command.strip(), flag_options=["-e"]) + positionals = [p.value for p in command_parts if isinstance(p, Positional)] + # expect positionals to be ["sh", path/to/gen-kernel-hwcaps.sh, input] + return [positionals[2]] + + +class CommandParserRegistry: + """ + Registry mapping command patterns to their input-file parsers. + """ + + def __init__(self, entries: list[CommandParserRegistryEntry]) -> None: + self._entries = entries + + def __iter__(self) -> Iterator[CommandParserRegistryEntry]: + return iter(self._entries) + + @staticmethod + def create() -> "CommandParserRegistry": + def env_or_default_pattern(env_value: str | None, default_pattern: str) -> str: + if env_value is None or not env_value.strip(): + return default_pattern + return rf"(?:{re.escape(env_value.strip())}|{default_pattern})" + + cc_pattern = env_or_default_pattern(Environment.CC(), r"([^\s]+-)?(gcc|clang)") + ld_pattern = env_or_default_pattern(Environment.LD(), r"([^\s]+-)?ld") + ar_pattern = env_or_default_pattern(Environment.AR(), r"([^\s]+-)?ar") + nm_pattern = env_or_default_pattern(Environment.NM(), r"([^\s]+-)?nm") + objcopy_pattern = env_or_default_pattern(Environment.OBJCOPY(), r"([^\s]+-)?objcopy") + strip_pattern = env_or_default_pattern(Environment.STRIP(), r"([^\s]+-)?strip") + + entries: list[CommandParserRegistryEntry] = [ + # Compound commands + (re.compile(r"\(.*?\)\s*>", re.DOTALL), _parse_compound_command), + (re.compile(r"\{.*?\}\s*>", re.DOTALL), _parse_compound_command), + # Standard Unix utilities and system tools + (re.compile(r"^rm\b"), _parse_noop), + (re.compile(r"^mkdir\b"), _parse_noop), + (re.compile(r"^touch\b"), _parse_noop), + (re.compile(r"^cp\b"), _parse_cp_command), + (re.compile(r"^truncate\b"), _parse_noop), + (re.compile(r"^cat\b.*?[\|>]"), lambda c: _parse_cat_command(c.split("|")[0].split(">")[0])), + (re.compile(r"^echo[^|]*$"), _parse_noop), + (re.compile(r"^sed.*?>"), lambda c: _parse_sed_command(c.split(">")[0])), + (re.compile(r"^sed\b"), _parse_noop), + (re.compile(r"^awk.*?<.*?>"), lambda c: [c.split("<")[1].split(">")[0]]), + (re.compile(r"^awk.*?>"), lambda c: _parse_awk(c.split(">")[0])), + (re.compile(r"^(/bin/)?true\b"), _parse_noop), + (re.compile(r"^(/bin/)?false\b"), _parse_noop), + (re.compile(r"^openssl\s+req.*?-new.*?-keyout"), _parse_noop), + # Compilers and code generators + # (C/LLVM toolchain, Rust, Flex/Bison, Bindgen, Perl, etc.) + ( + re.compile(rf"^{cc_pattern}\b"), + lambda command: _parse_gcc_or_clang_command(re.sub(rf"^{cc_pattern}\b", "gcc", command, count=1)), + ), + ( + re.compile(rf"^{ld_pattern}\b"), + lambda command: _parse_ld_command(re.sub(rf"^{ld_pattern}\b", "ld", command, count=1)), + ), + ( + re.compile(rf"^printf\b.*\| xargs {ar_pattern}\b"), + lambda command: _parse_ar_piped_xargs_command( + re.sub(rf"xargs {ar_pattern}\b", "xargs ar", command, count=1) + ), + ), + ( + re.compile(rf"^{ar_pattern}\b"), + lambda command: _parse_ar_command(re.sub(rf"^{ar_pattern}\b", "ar", command, count=1)), + ), + ( + re.compile(rf"^{nm_pattern}\b.*?\|"), + lambda command: _parse_nm_piped_command(re.sub(rf"^{nm_pattern}\b", "nm", command, count=1)), + ), + ( + re.compile(rf"^{objcopy_pattern}\b"), + lambda command: _parse_objcopy_command(re.sub(rf"^{objcopy_pattern}\b", "objcopy", command, count=1)), + ), + ( + re.compile(rf"^{strip_pattern}\b"), + lambda command: _parse_strip_command(re.sub(rf"^{strip_pattern}\b", "strip", command, count=1)), + ), + (re.compile(r".*?rustc\b"), _parse_rustc_command), + (re.compile(r".*?rustdoc\b"), _parse_rustdoc_command), + (re.compile(r"^flex\b"), _parse_flex_command), + (re.compile(r"^bison\b"), _parse_bison_command), + (re.compile(r"^bindgen\b"), _parse_bindgen_command), + (re.compile(r"^perl\b"), _parse_perl_command), + # Kernel-specific build scripts and tools + (re.compile(r"^(.*/)?link-vmlinux\.sh\b"), _parse_link_vmlinux_command), + (re.compile(r"sh (.*/)?syscallhdr\.sh\b"), _parse_syscallhdr_command), + (re.compile(r"sh (.*/)?syscalltbl\.sh\b"), _parse_syscalltbl_command), + (re.compile(r"sh (.*/)?mkcapflags\.sh\b"), _parse_mkcapflags_command), + (re.compile(r"sh (.*/)?orc_hash\.sh\b"), _parse_orc_hash_command), + (re.compile(r"sh (.*/)?xen-hypercalls\.sh\b"), _parse_xen_hypercalls_command), + (re.compile(r"sh (.*/)?gen_initramfs\.sh\b"), _parse_gen_initramfs_command), + (re.compile(r"sh (.*/)?checkundef\.sh\b"), _parse_noop), + (re.compile(r"(bash|sh) (.*/)?mkuboot\.sh\b"), _parse_mkuboot_command), + (re.compile(r"sh (.*/)?syscallnr\.sh\b"), _parse_syscallnr_command), + (re.compile(r"(/bin/)?sh (.*/)?gen-kernel-hwcaps\.sh\b"), lambda c: _parse_gen_kernel_hwcaps_command(c.split(">")[0])), + (re.compile(r"(.*/)?vdso2c\b"), _parse_vdso2c_command), + (re.compile(r"(.*/)?vdsomunge\b"), _parse_vdsomunge_command), + (re.compile(r"^(.*/)?mkpiggy.*?>"), _parse_mkpiggy_command), + (re.compile(r"^(.*/)?relocs\b"), _parse_relocs_command), + (re.compile(r"^(.*/)?mk_elfconfig.*?<.*?>"), _parse_mk_elfconfig_command), + (re.compile(r"^(.*/)?tools/build\b"), _parse_tools_build_command), + (re.compile(r"^(.*/)?certs/extract-cert"), _parse_extract_cert_command), + (re.compile(r"^(.*/)?scripts/dtc/dtc\b"), _parse_dtc_command), + (re.compile(r"^(.*/)?pnmtologo\b"), _parse_pnm_to_logo_command), + (re.compile(r"^(.*/)?kernel/pi/relacheck"), _parse_relacheck), + (re.compile(r"^(.*/)?gen-hyprel\b"), _parse_gen_hyprel_command), + (re.compile(r"^drivers/gpu/drm/radeon/mkregtable"), lambda c: [c.split(" ")[1]]), + (re.compile(r"(.*/)?genheaders\b"), _parse_noop), + (re.compile(r"^(.*/)?mkcpustr\s+>"), _parse_noop), + (re.compile(r"^(.*/)polgen\b"), _parse_noop), + (re.compile(r"make -f .*/arch/x86/Makefile\.postlink"), _parse_noop), + (re.compile(r"^(.*/)?raid6/mktables\s+>"), _parse_noop), + (re.compile(r"^(.*/)?objtool\b"), _parse_noop), + (re.compile(r"^(.*/)?module/gen_test_kallsyms.sh"), _parse_noop), + (re.compile(r"^(.*/)?gen_header.py"), _parse_gen_header), + (re.compile(r"^(.*/)?scripts/rustdoc_test_gen"), _parse_noop), + ] + return CommandParserRegistry(entries) diff --git a/scripts/sbom/sbom/cmd_graph/savedcmd_parser/command_splitter.py b/scripts/sbom/sbom/cmd_graph/savedcmd_parser/command_splitter.py new file mode 100644 index 000000000000..4749f4bd669e --- /dev/null +++ b/scripts/sbom/sbom/cmd_graph/savedcmd_parser/command_splitter.py @@ -0,0 +1,128 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +import re +from dataclasses import dataclass + + +# If Block pattern to match a simple, single-level if-then-fi block. Nested If blocks are not supported. +IF_BLOCK_PATTERN = re.compile( + r""" + ^if(.*?);\s* # Match 'if ;' (non-greedy) + then(.*?);\s* # Match 'then ;' (non-greedy) + fi\b # Match 'fi' + """, + re.VERBOSE, +) + + +@dataclass +class IfBlock: + condition: str + then_statement: str + + +def _unwrap_outer_parentheses(s: str) -> str: + s = s.strip() + if not (s.startswith("(") and s.endswith(")")): + return s + + count = 0 + for i, char in enumerate(s): + if char == "(": + count += 1 + elif char == ")": + count -= 1 + # If count is 0 before the end, outer parentheses don't match + if count == 0 and i != len(s) - 1: + return s + + # outer parentheses do match, unwrap once + return _unwrap_outer_parentheses(s[1:-1]) + + +def _find_first_top_level_command_separator( + commands: str, separators: list[str] = [";", "&&"] +) -> tuple[int | None, int | None]: + def is_escaped(index: int) -> bool: + preceding = commands[:index] + return (len(preceding) - len(preceding.rstrip("\\"))) % 2 == 1 + + in_single_quote = False + in_double_quote = False + in_curly_braces = 0 + in_braces = 0 + for i, char in enumerate(commands): + if char == "'" and not in_double_quote and not is_escaped(i): + # Toggle single quote state (unless inside double quotes or escaped) + in_single_quote = not in_single_quote + elif char == '"' and not in_single_quote and not is_escaped(i): + # Toggle double quote state (unless inside single quotes or escaped) + in_double_quote = not in_double_quote + + if in_single_quote or in_double_quote: + continue + + # Toggle braces state + if char == "{": + in_curly_braces += 1 + if char == "}": + in_curly_braces -= 1 + + if char == "(": + in_braces += 1 + if char == ")": + in_braces -= 1 + + if in_curly_braces > 0 or in_braces > 0: + continue + + # return found separator position and separator length + for separator in separators: + if commands[i : i + len(separator)] == separator: + return i, len(separator) + + return None, None + + +def split_commands(commands: str) -> list[str | IfBlock]: + """ + Splits a string of command-line commands into individual parts. + + This function handles: + - Top-level command separators (e.g., `;` and `&&`) to split multiple commands. + - Conditional if-blocks, returning them as `IfBlock` instances. + - Preserves the order of commands and trims whitespace. + + Args: + commands (str): The raw command string. + + Returns: + list[str | IfBlock]: A list of single commands or `IfBlock` objects. + """ + single_commands: list[str | IfBlock] = [] + remaining_commands = _unwrap_outer_parentheses(commands) + while len(remaining_commands) > 0: + remaining_commands = remaining_commands.strip() + + # if block + matched_if = IF_BLOCK_PATTERN.match(remaining_commands) + if matched_if: + condition, then_statement = matched_if.groups() + single_commands.append(IfBlock(condition.strip(), then_statement.strip())) + full_matched = matched_if.group(0) + remaining_commands = remaining_commands.removeprefix(full_matched).lstrip("; \n") + continue + + # command until next separator + separator_position, separator_length = _find_first_top_level_command_separator(remaining_commands) + if separator_position is not None and separator_length is not None: + single_commands.append(remaining_commands[:separator_position].strip()) + remaining_commands = remaining_commands[separator_position + separator_length :].strip() + continue + + # single last command + single_commands.append(remaining_commands) + break + + return single_commands diff --git a/scripts/sbom/sbom/cmd_graph/savedcmd_parser/savedcmd_parser.py b/scripts/sbom/sbom/cmd_graph/savedcmd_parser/savedcmd_parser.py new file mode 100644 index 000000000000..6a7ea4787aa1 --- /dev/null +++ b/scripts/sbom/sbom/cmd_graph/savedcmd_parser/savedcmd_parser.py @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +import sbom.sbom_logging as sbom_logging +from sbom.cmd_graph.savedcmd_parser.command_splitter import IfBlock, split_commands +from sbom.cmd_graph.savedcmd_parser.command_parser_registry import CommandParserRegistry +from sbom.cmd_graph.savedcmd_parser.tokenizer import CmdParsingError +from sbom.path_utils import PathStr + +DEFAULT_COMMAND_PARSER_REGISTRY = CommandParserRegistry.create() + + +def parse_inputs_from_commands( + commands: str, + fail_on_unknown_build_command: bool, + registry: CommandParserRegistry | None = None, +) -> list[PathStr]: + """ + Extract input files referenced in a set of command-line commands. + + Args: + commands (str): Command line expression to parse. + fail_on_unknown_build_command (bool): Whether to fail if an unknown build command is encountered. If False, errors are logged as warnings. + registry (CommandParserRegistry | None): Registry of single command parsers. + + Returns: + list[PathStr]: List of input file paths required by the commands. + """ + + def log_error_or_warning(message: str, /, **kwargs: str) -> None: + if fail_on_unknown_build_command: + sbom_logging.error(message, **kwargs) + else: + sbom_logging.warning(message, **kwargs) + + if registry is None: + registry = DEFAULT_COMMAND_PARSER_REGISTRY + + input_files: list[PathStr] = [] + for single_command in split_commands(commands): + if isinstance(single_command, IfBlock): + inputs = parse_inputs_from_commands(single_command.then_statement, fail_on_unknown_build_command, registry) + if inputs: + log_error_or_warning( + "Skipped parsing command {then_statement} because input files in IfBlock 'then' statement are not supported", + then_statement=single_command.then_statement, + ) + continue + + matched_parser = next((parser for pattern, parser in registry if pattern.match(single_command)), None) + if matched_parser is None: + log_error_or_warning( + "Skipped parsing command {single_command} because no matching parser was found", + single_command=single_command, + ) + continue + try: + inputs = matched_parser(single_command) + input_files.extend(inputs) + except (CmdParsingError, IndexError) as e: + log_error_or_warning( + "Skipped parsing command {single_command} because of command parsing error: {error_message}", + single_command=single_command, + error_message=str(e), + ) + + return [input.strip().rstrip("/") for input in input_files] diff --git a/scripts/sbom/sbom/cmd_graph/savedcmd_parser/tokenizer.py b/scripts/sbom/sbom/cmd_graph/savedcmd_parser/tokenizer.py new file mode 100644 index 000000000000..1bf081f40be7 --- /dev/null +++ b/scripts/sbom/sbom/cmd_graph/savedcmd_parser/tokenizer.py @@ -0,0 +1,92 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +import re +import shlex +from dataclasses import dataclass +from typing import Union + + +class CmdParsingError(Exception): + pass + + +@dataclass +class Option: + name: str + value: str | None = None + + +@dataclass +class Positional: + value: str + + +_SUBCOMMAND_PATTERN = re.compile(r"\$\$\(([^()]*)\)") +"""Pattern to match $$(...) blocks""" + + +def tokenize_single_command(command: str, flag_options: list[str] | None = None) -> list[Union[Option, Positional]]: + """ + Parse a shell command into a list of Options and Positionals. + - Positional: the command and any positional arguments. + - Options: handles flags and options with values provided as space-separated, or equals-sign + (e.g., '--opt val', '--opt=val', '--flag'). + + Args: + command: Command line string. + flag_options: Options that are flags without values (e.g., '--verbose'). + + Returns: + List of `Option` and `Positional` objects in command order. + """ + + # Wrap all $$(...) blocks in double quotes to prevent shlex from splitting them. + command_with_protected_subcommands = _SUBCOMMAND_PATTERN.sub(lambda m: f'"$$({m.group(1)})"', command) + tokens = shlex.split(command_with_protected_subcommands) + + parsed: list[Option | Positional] = [] + i = 0 + while i < len(tokens): + token = tokens[i] + + # Positional + if not token.startswith("-"): + parsed.append(Positional(token)) + i += 1 + continue + + # Option without value (--flag) + if (token.startswith("-") and i + 1 < len(tokens) and tokens[i + 1].startswith("-")) or ( + flag_options and token in flag_options + ): + parsed.append(Option(name=token)) + i += 1 + continue + + # Option with equals sign (--opt=val) + if "=" in token: + name, value = token.split("=", 1) + parsed.append(Option(name=name, value=value)) + i += 1 + continue + + # Option with space-separated value (--opt val) + if i + 1 < len(tokens) and not tokens[i + 1].startswith("-"): + parsed.append(Option(name=token, value=tokens[i + 1])) + i += 2 + continue + + raise CmdParsingError(f"Unrecognized token: {token} in command {command}") + + return parsed + + +def tokenize_single_command_positionals_only(command: str) -> list[str]: + command_parts = tokenize_single_command(command) + positionals = [p.value for p in command_parts if isinstance(p, Positional)] + if len(positionals) != len(command_parts): + raise CmdParsingError( + f"Invalid command format: expected positional arguments only but got options in command {command}." + ) + return positionals diff --git a/scripts/sbom/sbom/environment.py b/scripts/sbom/sbom/environment.py new file mode 100644 index 000000000000..4304066fe974 --- /dev/null +++ b/scripts/sbom/sbom/environment.py @@ -0,0 +1,192 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +import os + +KERNEL_BUILD_VARIABLES_ALLOWLIST = [ + "AFLAGS_KERNEL", + "AFLAGS_MODULE", + "AR", + "ARCH", + "ARCH_CORE", + "ARCH_DRIVERS", + "ARCH_LIB", + "AWK", + "BASH", + "BINDGEN", + "BITS", + "CC", + "CC_FLAGS_FPU", + "CC_FLAGS_NO_FPU", + "CFLAGS_GCOV", + "CFLAGS_KERNEL", + "CFLAGS_MODULE", + "CHECK", + "CHECKFLAGS", + "CLIPPY_CONF_DIR", + "CONFIG_SHELL", + "CPP", + "CROSS_COMPILE", + "CURDIR", + "GNUMAKEFLAGS", + "HOSTCC", + "HOSTCXX", + "HOSTPKG_CONFIG", + "HOSTRUSTC", + "INSTALLKERNEL", + "INSTALL_DTBS_PATH", + "INSTALL_HDR_PATH", + "INSTALL_PATH", + "KBUILD_AFLAGS", + "KBUILD_AFLAGS_KERNEL", + "KBUILD_AFLAGS_MODULE", + "KBUILD_BUILTIN", + "KBUILD_CFLAGS", + "KBUILD_CFLAGS_KERNEL", + "KBUILD_CFLAGS_MODULE", + "KBUILD_CHECKSRC", + "KBUILD_CLIPPY", + "KBUILD_CPPFLAGS", + "KBUILD_EXTMOD", + "KBUILD_EXTRA_WARN", + "KBUILD_HOSTCFLAGS", + "KBUILD_HOSTCXXFLAGS", + "KBUILD_HOSTLDFLAGS", + "KBUILD_HOSTLDLIBS", + "KBUILD_HOSTRUSTFLAGS", + "KBUILD_IMAGE", + "KBUILD_LDFLAGS", + "KBUILD_LDFLAGS_MODULE", + "KBUILD_LDS", + "KBUILD_MODULES", + "KBUILD_PROCMACROLDFLAGS", + "KBUILD_RUSTFLAGS", + "KBUILD_RUSTFLAGS_KERNEL", + "KBUILD_RUSTFLAGS_MODULE", + "KBUILD_USERCFLAGS", + "KBUILD_USERLDFLAGS", + "KBUILD_VERBOSE", + "KBUILD_VMLINUX_LIBS", + "KBZIP2", + "KCONFIG_CONFIG", + "KERNELDOC", + "KERNELRELEASE", + "KERNELVERSION", + "KGZIP", + "KLZOP", + "LC_COLLATE", + "LC_NUMERIC", + "LD", + "LDFLAGS_MODULE", + "LEX", + "LINUXINCLUDE", + "LZ4", + "LZMA", + "MAKE", + "MAKEFILES", + "MAKEFILE_LIST", + "MAKEFLAGS", + "MAKELEVEL", + "MAKEOVERRIDES", + "MAKE_COMMAND", + "MAKE_HOST", + "MAKE_TERMERR", + "MAKE_TERMOUT", + "MAKE_VERSION", + "MFLAGS", + "MODLIB", + "NM", + "NOSTDINC_FLAGS", + "O", + "OBJCOPY", + "OBJCOPYFLAGS", + "OBJDUMP", + "PAHOLE", + "PATCHLEVEL", + "PERL", + "PYTHON3", + "Q", + "RCS_FIND_IGNORE", + "READELF", + "REALMODE_CFLAGS", + "RESOLVE_BTFIDS", + "RETHUNK_CFLAGS", + "RETHUNK_RUSTFLAGS", + "RETPOLINE_CFLAGS", + "RETPOLINE_RUSTFLAGS", + "RETPOLINE_VDSO_CFLAGS", + "RUSTC", + "RUSTC_BOOTSTRAP", + "RUSTC_OR_CLIPPY", + "RUSTC_OR_CLIPPY_QUIET", + "RUSTDOC", + "RUSTFLAGS_KERNEL", + "RUSTFLAGS_MODULE", + "RUSTFMT", + "SRCARCH", + "STRIP", + "SUBLEVEL", + "SUFFIXES", + "TAR", + "UTS_MACHINE", + "VERSION", + "VPATH", + "XZ", + "YACC", + "ZSTD", + "building_out_of_srctree", + "cross_compiling", + "objtree", + "quiet", + "rust_common_flags", + "srcroot", + "srctree", + "sub_make_done", + "subdir", +] + + +class Environment: + """ + Read-only accessor for kernel build environment variables. + """ + + @classmethod + def KERNEL_BUILD_VARIABLES(cls) -> dict[str, str]: + return { + name: value.strip() + for name in KERNEL_BUILD_VARIABLES_ALLOWLIST + if (value := os.getenv(name)) is not None and value.strip() + } + + @classmethod + def ARCH(cls) -> str | None: + return os.getenv("ARCH") + + @classmethod + def SRCARCH(cls) -> str | None: + return os.getenv("SRCARCH") + + @classmethod + def CC(cls) -> str | None: + return os.getenv("CC") + + @classmethod + def LD(cls) -> str | None: + return os.getenv("LD") + + @classmethod + def AR(cls) -> str | None: + return os.getenv("AR") + + @classmethod + def NM(cls) -> str | None: + return os.getenv("NM") + + @classmethod + def OBJCOPY(cls) -> str | None: + return os.getenv("OBJCOPY") + + @classmethod + def STRIP(cls) -> str | None: + return os.getenv("STRIP") -- cgit v1.2.3 From 9c16c1ea466d6c58b82c5d91353c3c6747c059bc Mon Sep 17 00:00:00 2001 From: Luis Augenstein Date: Mon, 18 May 2026 08:20:52 +0200 Subject: scripts/sbom: add cmd graph generation Implement command graph generation by parsing .cmd files to build a dependency graph. Add CmdGraph, CmdGraphNode, and .cmd file parsing. Supports generating a flat list of used source files via the --generate-used-files cli argument. Assisted-by: Cursor:claude-sonnet-4-5 Assisted-by: OpenCode:GLM-4-7 Co-developed-by: Maximilian Huber Signed-off-by: Maximilian Huber Signed-off-by: Luis Augenstein Signed-off-by: Greg Kroah-Hartman --- Makefile | 6 +- scripts/sbom/sbom.py | 39 +++++++ scripts/sbom/sbom/cmd_graph/__init__.py | 7 ++ scripts/sbom/sbom/cmd_graph/cmd_file.py | 162 ++++++++++++++++++++++++++ scripts/sbom/sbom/cmd_graph/cmd_graph.py | 46 ++++++++ scripts/sbom/sbom/cmd_graph/cmd_graph_node.py | 111 ++++++++++++++++++ scripts/sbom/sbom/cmd_graph/deps_parser.py | 52 +++++++++ scripts/sbom/sbom/config.py | 149 ++++++++++++++++++++++- scripts/sbom/sbom/path_utils.py | 22 ++++ 9 files changed, 591 insertions(+), 3 deletions(-) create mode 100644 scripts/sbom/sbom/cmd_graph/__init__.py create mode 100644 scripts/sbom/sbom/cmd_graph/cmd_file.py create mode 100644 scripts/sbom/sbom/cmd_graph/cmd_graph.py create mode 100644 scripts/sbom/sbom/cmd_graph/cmd_graph_node.py create mode 100644 scripts/sbom/sbom/cmd_graph/deps_parser.py create mode 100644 scripts/sbom/sbom/path_utils.py (limited to 'scripts') diff --git a/Makefile b/Makefile index ec54f7d51cf4..4c6133af5549 100644 --- a/Makefile +++ b/Makefile @@ -2208,7 +2208,11 @@ sbom_targets += sbom-build.spdx.json sbom-output.spdx.json quiet_cmd_sbom = GEN $(sbom_targets) cmd_sbom = printf "%s\n" "$(KBUILD_IMAGE)" >"$(tmp-target)"; \ $(if $(CONFIG_MODULES),sed 's/\.o$$/.ko/' $(objtree)/modules.order >> "$(tmp-target)";) \ - $(PYTHON3) $(srctree)/scripts/sbom/sbom.py; + $(PYTHON3) $(srctree)/scripts/sbom/sbom.py \ + --src-tree $(abspath $(srctree)) \ + --obj-tree $(abspath $(objtree)) \ + --roots-file "$(tmp-target)" \ + --output-directory $(abspath $(objtree)); PHONY += sbom sbom: $(notdir $(KBUILD_IMAGE)) include/generated/autoconf.h $(if $(CONFIG_MODULES),modules modules.order) $(call cmd,sbom) diff --git a/scripts/sbom/sbom.py b/scripts/sbom/sbom.py index 3bd466720b0d..d700e4f294f7 100644 --- a/scripts/sbom/sbom.py +++ b/scripts/sbom/sbom.py @@ -7,9 +7,13 @@ Compute software bill of materials in SPDX format describing a kernel build. """ import logging +import os import sys +import time import sbom.sbom_logging as sbom_logging from sbom.config import get_config +from sbom.path_utils import is_relative_to +from sbom.cmd_graph import CmdGraph def _exit_with_summary(write_output_on_error: bool = False) -> None: @@ -19,6 +23,11 @@ def _exit_with_summary(write_output_on_error: bool = False) -> None: logging.warning(warning_summary) if error_summary: logging.error(error_summary) + if not write_output_on_error: + logging.info( + "Use --write-output-on-error to generate output documents even when errors occur. " + "Note that in this case the generated documents may be incomplete." + ) sys.exit(1) @@ -32,6 +41,36 @@ def main(): format="[%(levelname)s] %(message)s", ) + # Build cmd graph + logging.debug("Start building cmd graph") + start_time = time.time() + cmd_graph = CmdGraph.create(config.root_paths, config) + logging.debug(f"Built cmd graph in {time.time() - start_time} seconds") + + # Save used files document + if config.generate_used_files: + if config.src_tree == config.obj_tree: + logging.info( + f"Extracting all files from the cmd graph to {config.used_files_file_name} " + "instead of only source files because source files cannot be " + "reliably classified when the source and object trees are identical.", + ) + used_files = [os.path.relpath(node.absolute_path, config.src_tree) for node in cmd_graph] + logging.debug(f"Found {len(used_files)} files in cmd graph.") + else: + used_files = [ + os.path.relpath(node.absolute_path, config.src_tree) + for node in cmd_graph + if is_relative_to(node.absolute_path, config.src_tree) + and not is_relative_to(node.absolute_path, config.obj_tree) + ] + logging.debug(f"Found {len(used_files)} source files in cmd graph") + if not sbom_logging.has_errors() or config.write_output_on_error: + used_files_path = os.path.join(config.output_directory, config.used_files_file_name) + with open(used_files_path, "w", encoding="utf-8") as f: + f.write("\n".join(str(file_path) for file_path in used_files)) + logging.debug(f"Successfully saved {used_files_path}") + _exit_with_summary(config.write_output_on_error) diff --git a/scripts/sbom/sbom/cmd_graph/__init__.py b/scripts/sbom/sbom/cmd_graph/__init__.py new file mode 100644 index 000000000000..9d661a5c3d93 --- /dev/null +++ b/scripts/sbom/sbom/cmd_graph/__init__.py @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from .cmd_graph import CmdGraph +from .cmd_graph_node import CmdGraphNode, CmdGraphNodeConfig + +__all__ = ["CmdGraph", "CmdGraphNode", "CmdGraphNodeConfig"] diff --git a/scripts/sbom/sbom/cmd_graph/cmd_file.py b/scripts/sbom/sbom/cmd_graph/cmd_file.py new file mode 100644 index 000000000000..dcd63e284a38 --- /dev/null +++ b/scripts/sbom/sbom/cmd_graph/cmd_file.py @@ -0,0 +1,162 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +import os +import re +from dataclasses import dataclass, field +from sbom.cmd_graph.deps_parser import parse_cmd_file_deps +from sbom.cmd_graph.savedcmd_parser import parse_inputs_from_commands +import sbom.sbom_logging as sbom_logging +from sbom.path_utils import PathStr + +SAVEDCMD_PATTERN = re.compile(r"^(saved)?cmd_.*?:=\s*(?P.+)$") +SOURCE_PATTERN = re.compile(r"^source.*?:=\s*(?P.+)$") + + +@dataclass +class CmdFile: + cmd_file_path: PathStr + savedcmd: str + source: PathStr | None = None + deps: list[str] = field(default_factory=list) + make_rules: list[str] = field(default_factory=list) + + @classmethod + def create(cls, cmd_file_path: PathStr) -> "CmdFile | None": + """ + Parses a .cmd file. + .cmd files are assumed to have one of the following structures: + 1. Full Cmd File + (saved)?cmd_ := + source_ := + deps_ := \ + + := $(deps_) + $(deps_): + + 2. Command Only Cmd File + (saved)?cmd_ := + + 3. Single Dependency Cmd File + (saved)?cmd_ := + : + + Args: + cmd_file_path (Path): absolute Path to a .cmd file + + Returns: + cmd_file (CmdFile): Parsed cmd file. + """ + with open(cmd_file_path, "rt", encoding="utf-8") as f: + lines = [line.strip() for line in f.readlines() if line.strip() != "" and not line.startswith("#")] + + # savedcmd + match = SAVEDCMD_PATTERN.match(lines[0] if lines else "") + if match is None: + sbom_logging.error( + "Skip parsing '{cmd_file_path}' because no 'savedcmd_' command was found.", cmd_file_path=cmd_file_path + ) + return None + savedcmd = match.group("full_command") + + # Command Only Cmd File + if len(lines) == 1: + return CmdFile(cmd_file_path, savedcmd) + + # Single Dependency Cmd File + if len(lines) == 2: + parts = lines[1].split(":", 1) + if len(parts) != 2: + sbom_logging.error( + "Skip parsing '{cmd_file_path}'. Expected dependency line ': ' but got {second_line}", cmd_file_path=cmd_file_path, second_line=lines[1] + ) + return None + dep = parts[1].strip() + return CmdFile(cmd_file_path, savedcmd, deps=[dep]) + + # Full Cmd File + # source + line1 = SOURCE_PATTERN.match(lines[1]) + if line1 is None: + sbom_logging.error( + "Skip parsing '{cmd_file_path}' because no 'source_' entry was found.", cmd_file_path=cmd_file_path + ) + return CmdFile(cmd_file_path, savedcmd) + source = line1.group("source_file") + + # deps + deps: list[str] = [] + i = 3 # lines[2] includes the variable assignment but no actual dependency, so we need to start at lines[3]. + while i < len(lines): + if not lines[i].endswith("\\"): + break + deps.append(lines[i][:-1].strip()) + i += 1 + + # make_rules + make_rules = lines[i:] + + return CmdFile(cmd_file_path, savedcmd, source, deps, make_rules) + + def get_dependencies( + self: "CmdFile", target_path: PathStr, obj_tree: PathStr, fail_on_unknown_build_command: bool + ) -> list[PathStr]: + """ + Parses all dependencies required to build a target file from its cmd file. + + Args: + target_path: path to the target file relative to `obj_tree`. + obj_tree: absolute path to the object tree. + fail_on_unknown_build_command: Whether to fail if an unknown build command is encountered. + + Returns: + list[PathStr]: dependency file paths relative to `obj_tree`. + """ + input_files: list[PathStr] = [ + str(p) for p in parse_inputs_from_commands(self.savedcmd, fail_on_unknown_build_command) + ] + if self.deps: + input_files += [str(p) for p in parse_cmd_file_deps(self.deps)] + input_files = _expand_resolve_files(input_files, obj_tree) + + cmd_file_dependencies: list[PathStr] = [] + for input_file in input_files: + # input files are either absolute or relative to the object tree + if os.path.isabs(input_file): + input_file = os.path.relpath(input_file, obj_tree) + if input_file == target_path: + # Skip target file to prevent cycles. This is necessary because some multi stage commands first create an output and then pass it as input to the next command, e.g., objcopy. + continue + cmd_file_dependencies.append(input_file) + unique_cmd_file_dependencies = list(dict.fromkeys(cmd_file_dependencies)) + return unique_cmd_file_dependencies + + +def _expand_resolve_files(input_files: list[PathStr], obj_tree: PathStr) -> list[PathStr]: + """ + Expands resolve files which may reference additional files via '@' notation. + + Args: + input_files (list[PathStr]): List of file paths relative to the object tree, where paths starting with '@' refer to files + containing further file paths, each on a separate line. + obj_tree: Absolute path to the root of the object tree. + + Returns: + list[PathStr]: Flattened list of all input file paths, with any nested '@' file references resolved recursively. + """ + expanded_input_files: list[PathStr] = [] + for input_file in input_files: + if not input_file.startswith("@"): + expanded_input_files.append(input_file) + continue + resolve_file_path = os.path.join(obj_tree, input_file.removeprefix("@")) + if not os.path.exists(resolve_file_path): + sbom_logging.error( + "Skip resolving '{resolve_file_path}' because the response file does not exist.", + resolve_file_path=resolve_file_path, + ) + continue + with open(resolve_file_path, "rt", encoding="utf-8") as f: + resolve_file_content = [line_stripped for line in f.readlines() if (line_stripped := line.strip())] + expanded_input_files += _expand_resolve_files(resolve_file_content, obj_tree) + return expanded_input_files diff --git a/scripts/sbom/sbom/cmd_graph/cmd_graph.py b/scripts/sbom/sbom/cmd_graph/cmd_graph.py new file mode 100644 index 000000000000..2f57965237f4 --- /dev/null +++ b/scripts/sbom/sbom/cmd_graph/cmd_graph.py @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from collections import deque +from dataclasses import dataclass, field +from typing import Iterator + +from sbom.cmd_graph.cmd_graph_node import CmdGraphNode, CmdGraphNodeConfig +from sbom.path_utils import PathStr + + +@dataclass +class CmdGraph: + """Directed acyclic graph of build dependencies primarily inferred from .cmd files produced during kernel builds""" + + roots: list[CmdGraphNode] = field(default_factory=list) + + @classmethod + def create(cls, root_paths: list[PathStr], config: CmdGraphNodeConfig) -> "CmdGraph": + """ + Recursively builds a dependency graph starting from `root_paths`. + Dependencies are mainly discovered by parsing the `.cmd` files. + + Args: + root_paths (list[PathStr]): List of paths to root outputs relative to obj_tree + config (CmdGraphNodeConfig): Configuration options + + Returns: + CmdGraph: A graph of all build dependencies for the given root files. + """ + node_cache: dict[PathStr, CmdGraphNode] = {} + root_nodes = [CmdGraphNode.create(root_path, config, node_cache) for root_path in root_paths] + return CmdGraph(root_nodes) + + def __iter__(self) -> Iterator[CmdGraphNode]: + """Traverse the graph in breadth-first order, yielding each unique node.""" + visited: set[PathStr] = set() + node_stack: deque[CmdGraphNode] = deque(self.roots) + while len(node_stack) > 0: + node = node_stack.popleft() + if node.absolute_path in visited: + continue + + visited.add(node.absolute_path) + node_stack.extend(node.children) + yield node diff --git a/scripts/sbom/sbom/cmd_graph/cmd_graph_node.py b/scripts/sbom/sbom/cmd_graph/cmd_graph_node.py new file mode 100644 index 000000000000..7dde1c28eef1 --- /dev/null +++ b/scripts/sbom/sbom/cmd_graph/cmd_graph_node.py @@ -0,0 +1,111 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from dataclasses import dataclass, field +import logging +import os +from typing import Iterator, Protocol + +from sbom import sbom_logging +from sbom.cmd_graph.cmd_file import CmdFile +from sbom.path_utils import PathStr, has_link, is_relative_to + + +class CmdGraphNodeConfig(Protocol): + obj_tree: PathStr + src_tree: PathStr + fail_on_unknown_build_command: bool + + +@dataclass +class CmdGraphNode: + """A node in the cmd graph representing a single file and its dependencies.""" + + absolute_path: PathStr + """Absolute path to the file this node represents.""" + + cmd_file: CmdFile | None = None + """Parsed .cmd file describing how the file at absolute_path was built, or None if not available.""" + + cmd_file_dependencies: list["CmdGraphNode"] = field(default_factory=list) + + @property + def children(self) -> Iterator["CmdGraphNode"]: + seen: set[PathStr] = set() + for node in self.cmd_file_dependencies: + if node.absolute_path not in seen: + seen.add(node.absolute_path) + yield node + + @classmethod + def create( + cls, + target_path: PathStr, + config: CmdGraphNodeConfig, + cache: dict[PathStr, "CmdGraphNode"] | None = None, + depth: int = 0, + ) -> "CmdGraphNode": + """ + Recursively builds a dependency graph starting from `target_path`. + Dependencies are mainly discovered by parsing the `..cmd` file. + + Args: + target_path: Path to the target file relative to obj_tree. + config: Config options + cache: Tracks processed nodes to prevent cycles. + depth: Internal parameter to track the current recursion depth. + + Returns: + CmdGraphNode: cmd graph node representing the target file + """ + if cache is None: + cache = {} + + target_path_absolute = ( + os.path.realpath(p) + if has_link(p:=os.path.join(config.obj_tree, target_path)) + else os.path.normpath(p) + ) + + if target_path_absolute in cache: + return cache[target_path_absolute] + + if depth == 0: + logging.debug(f"Build node: {target_path}") + + cmd_file_path = _to_cmd_path(target_path_absolute) + cmd_file = CmdFile.create(cmd_file_path) if os.path.exists(cmd_file_path) else None + node = CmdGraphNode(target_path_absolute, cmd_file) + cache[target_path_absolute] = node + + if not os.path.exists(target_path_absolute): + error_or_warning = ( + sbom_logging.error + if is_relative_to(target_path_absolute, config.obj_tree) + or is_relative_to(target_path_absolute, config.src_tree) + else sbom_logging.warning + ) + error_or_warning( + "Skip parsing '{target_path_absolute}' because file does not exist", + target_path_absolute=target_path_absolute, + ) + return node + + # Search for dependencies to add to the graph as child nodes. Child paths are always relative to the output tree. + def _build_child_node(child_path: PathStr) -> "CmdGraphNode": + return CmdGraphNode.create(child_path, config, cache, depth + 1) + + if cmd_file is not None: + node.cmd_file_dependencies = [ + _build_child_node(cmd_file_dependency_path) + for cmd_file_dependency_path in cmd_file.get_dependencies( + target_path, config.obj_tree, config.fail_on_unknown_build_command + ) + ] + + return node + + +def _to_cmd_path(path: PathStr) -> PathStr: + name = os.path.basename(path) + return path.removesuffix(name) + f".{name}.cmd" diff --git a/scripts/sbom/sbom/cmd_graph/deps_parser.py b/scripts/sbom/sbom/cmd_graph/deps_parser.py new file mode 100644 index 000000000000..6a2d92f0778c --- /dev/null +++ b/scripts/sbom/sbom/cmd_graph/deps_parser.py @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +import re +import sbom.sbom_logging as sbom_logging +from sbom.path_utils import PathStr + +# Match dependencies on config files +# Example match: "$(wildcard include/config/CONFIG_SOMETHING)" +CONFIG_PATTERN = re.compile(r"\$\(wildcard (include/config/[^)]+)\)") + +# Match dependencies on the objtool binary +# Example match: "$(wildcard ./tools/objtool/objtool)" +OBJTOOL_PATTERN = re.compile(r"\$\(wildcard \./tools/objtool/objtool\)") + +# Match any Makefile wildcard reference +# Example match: "$(wildcard path/to/file)" +WILDCARD_PATTERN = re.compile(r"\$\(wildcard (?P[^)]+)\)") + +# Match ordinary paths: +# - ^(\/)?: Optionally starts with a '/' +# - (([\w\-\.,+~=@ ]*)\/)*: Zero or more directory levels +# - [\w\-\.,+~=@ ]+$: Path component (file or directory) +# Example matches: "/foo/bar.c", "dir1/dir2/file.txt", "plainfile" +VALID_PATH_PATTERN = re.compile(r"^(\/)?(([\w\-\.,+~=@ ]*)\/)*[\w\-\.,+~=@ ]+$") + + +def parse_cmd_file_deps(deps: list[str]) -> list[PathStr]: + """ + Parse dependency strings of a .cmd file and return valid input file paths. + + Args: + deps: List of dependency strings as found in `.cmd` files. + + Returns: + input_files: List of input file paths + """ + input_files: list[PathStr] = [] + for dep in deps: + dep = dep.strip() + match dep: + case _ if CONFIG_PATTERN.match(dep) or OBJTOOL_PATTERN.match(dep): + # config paths like include/config/ should not be included in the graph + continue + case _ if match := WILDCARD_PATTERN.match(dep): + path = match.group("path") + input_files.append(path) + case _ if VALID_PATH_PATTERN.match(dep): + input_files.append(dep) + case _: + sbom_logging.error("Skip parsing dependency {dep} because of unrecognized format", dep=dep) + return input_files diff --git a/scripts/sbom/sbom/config.py b/scripts/sbom/sbom/config.py index c1ac9ad5737f..b8c1a2b404df 100644 --- a/scripts/sbom/sbom/config.py +++ b/scripts/sbom/sbom/config.py @@ -3,21 +3,88 @@ import argparse from dataclasses import dataclass +import os +from typing import Any +from sbom.path_utils import PathStr @dataclass class KernelSbomConfig: + src_tree: PathStr + """Absolute path to the Linux kernel source directory.""" + + obj_tree: PathStr + """Absolute path to the build output directory.""" + + root_paths: list[PathStr] + """List of paths to root outputs (relative to obj_tree) to base the SBOM on.""" + + generate_used_files: bool + """Whether to generate a flat list of all source files used in the build. + If False, no used-files document is created.""" + + used_files_file_name: str + """If `generate_used_files` is True, specifies the file name for the used-files document.""" + + output_directory: PathStr + """Path to the directory where the generated output documents will be saved.""" + debug: bool """Whether to enable debug logging.""" + fail_on_unknown_build_command: bool + """Whether to fail if an unknown build command is encountered in a .cmd file.""" + + write_output_on_error: bool + """Whether to write output documents even if errors occur.""" + -def _parse_cli_arguments(parser: argparse.ArgumentParser) -> dict[str, bool]: +def _parse_cli_arguments(parser: argparse.ArgumentParser) -> dict[str, Any]: """ Parse command-line arguments using argparse. Returns: Dictionary of parsed arguments. """ + parser.add_argument( + "--src-tree", + default="../linux", + help="Path to the kernel source tree (default: ../linux)", + ) + parser.add_argument( + "--obj-tree", + default="../linux/kernel_build", + help="Path to the build output directory (default: ../linux/kernel_build)", + ) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + "--roots", + nargs="+", + help="Space-separated list of paths relative to obj-tree for which the SBOM will be created.\n" + "Cannot be used together with --roots-file.", + ) + group.add_argument( + "--roots-file", + help="Path to a file containing the root paths (one per line). Cannot be used together with --roots.", + ) + parser.add_argument( + "--generate-used-files", + action="store_true", + default=False, + help=( + "Whether to create the sbom.used-files.txt file, a flat list of all " + "source files used for the kernel build.\n" + "If src-tree and obj-tree are equal it is not possible to reliably " + "classify source files.\n" + "In this case sbom.used-files.txt will contain all files used for the " + "kernel build including all build artifacts. (default: False)" + ), + ) + parser.add_argument( + "--output-directory", + default=".", + help="Path to the directory where the generated output documents will be stored (default: .)", + ) parser.add_argument( "--debug", action="store_true", @@ -25,6 +92,28 @@ def _parse_cli_arguments(parser: argparse.ArgumentParser) -> dict[str, bool]: help="Enable debug logs (default: False)", ) + # Error handling settings + parser.add_argument( + "--do-not-fail-on-unknown-build-command", + action="store_true", + default=False, + help=( + "Whether to fail if an unknown build command is encountered in a .cmd file.\n" + "If set to True, errors are logged as warnings instead. (default: False)" + ), + ) + parser.add_argument( + "--write-output-on-error", + action="store_true", + default=False, + help=( + "Write output documents even if errors occur. The resulting documents " + "may be incomplete.\n" + "A summary of warnings and errors can be found in the 'comment' property " + "of the CreationInfo element. (default: False)" + ), + ) + args = vars(parser.parse_args()) return args @@ -37,10 +126,66 @@ def get_config() -> KernelSbomConfig: KernelSbomConfig: Configuration object with all settings for SBOM generation. """ parser = argparse.ArgumentParser( + formatter_class=argparse.RawTextHelpFormatter, description="Generate SPDX SBOM documents for kernel builds", ) args = _parse_cli_arguments(parser) + # Extract and validate cli arguments + src_tree = os.path.realpath(args["src_tree"]) + obj_tree = os.path.realpath(args["obj_tree"]) + root_paths = [] + if args["roots_file"]: + with open(args["roots_file"], "rt", encoding="utf-8") as f: + root_paths = [root.strip() for root in f.readlines()] + if len(root_paths) == 0: + parser.error("--roots-file must contain at least one path") + else: + root_paths = args["roots"] + _validate_path_arguments(parser, src_tree, obj_tree, root_paths) + + generate_used_files = args["generate_used_files"] + output_directory = os.path.realpath(args["output_directory"]) debug = args["debug"] - return KernelSbomConfig(debug=debug) + fail_on_unknown_build_command = not args["do_not_fail_on_unknown_build_command"] + write_output_on_error = args["write_output_on_error"] + + # Hardcoded config + used_files_file_name = "sbom.used-files.txt" + + return KernelSbomConfig( + src_tree=src_tree, + obj_tree=obj_tree, + root_paths=root_paths, + generate_used_files=generate_used_files, + used_files_file_name=used_files_file_name, + output_directory=output_directory, + debug=debug, + fail_on_unknown_build_command=fail_on_unknown_build_command, + write_output_on_error=write_output_on_error, + ) + + +def _validate_path_arguments( + parser: argparse.ArgumentParser, + src_tree: PathStr, + obj_tree: PathStr, + root_paths: list[PathStr], +) -> None: + """ + Validate that the provided paths exist. + + Args: + parser: The argument parser, used to emit well-formatted error messages. + src_tree: Absolute path to the source tree. + obj_tree: Absolute path to the object tree. + root_paths: List of root paths relative to obj_tree. + """ + if not os.path.exists(src_tree): + parser.error(f"--src-tree {src_tree} does not exist") + if not os.path.exists(obj_tree): + parser.error(f"--obj-tree {obj_tree} does not exist") + for root_path in root_paths: + if not os.path.isfile(root_path_absolute := os.path.join(obj_tree, root_path)): + parser.error(f"path to root artifact {root_path_absolute} is not a file") diff --git a/scripts/sbom/sbom/path_utils.py b/scripts/sbom/sbom/path_utils.py new file mode 100644 index 000000000000..29820046dc88 --- /dev/null +++ b/scripts/sbom/sbom/path_utils.py @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +import os +from functools import lru_cache + +PathStr = str +"""Filesystem path represented as a plain string for better performance than pathlib.Path.""" + + +def is_relative_to(path: PathStr, base: PathStr) -> bool: + return os.path.commonpath([path, base]) == base + +@lru_cache(maxsize=None) +def has_link(path: PathStr) -> bool: + """Returns True if path or any of its ancestor directories is a symlink. Results are cached to avoid duplicate lstat syscalls.""" + if os.path.islink(path): + return True + parent = os.path.dirname(path) + if parent == path: + return False + return has_link(parent) -- cgit v1.2.3 From d764b54e2885d55a6d272507e8b8e1b2cbbc2530 Mon Sep 17 00:00:00 2001 From: Luis Augenstein Date: Mon, 18 May 2026 08:20:53 +0200 Subject: scripts/sbom: add additional dependency sources for cmd graph Add hardcoded dependencies and .incbin directive parsing to discover dependencies not tracked by .cmd files. Assisted-by: Cursor:claude-sonnet-4-5 Assisted-by: OpenCode:GLM-4-7 Co-developed-by: Maximilian Huber Signed-off-by: Maximilian Huber Signed-off-by: Luis Augenstein Signed-off-by: Greg Kroah-Hartman --- scripts/sbom/sbom/cmd_graph/cmd_graph_node.py | 33 +++++++- .../sbom/sbom/cmd_graph/hardcoded_dependencies.py | 87 ++++++++++++++++++++++ scripts/sbom/sbom/cmd_graph/incbin_parser.py | 42 +++++++++++ 3 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 scripts/sbom/sbom/cmd_graph/hardcoded_dependencies.py create mode 100644 scripts/sbom/sbom/cmd_graph/incbin_parser.py (limited to 'scripts') diff --git a/scripts/sbom/sbom/cmd_graph/cmd_graph_node.py b/scripts/sbom/sbom/cmd_graph/cmd_graph_node.py index 7dde1c28eef1..61f3a8140cea 100644 --- a/scripts/sbom/sbom/cmd_graph/cmd_graph_node.py +++ b/scripts/sbom/sbom/cmd_graph/cmd_graph_node.py @@ -2,15 +2,24 @@ # Copyright (C) 2025 TNG Technology Consulting GmbH from dataclasses import dataclass, field +from itertools import chain import logging import os from typing import Iterator, Protocol from sbom import sbom_logging from sbom.cmd_graph.cmd_file import CmdFile +from sbom.cmd_graph.hardcoded_dependencies import get_hardcoded_dependencies +from sbom.cmd_graph.incbin_parser import parse_incbin_statements from sbom.path_utils import PathStr, has_link, is_relative_to +@dataclass +class IncbinDependency: + node: "CmdGraphNode" + full_statement: str + + class CmdGraphNodeConfig(Protocol): obj_tree: PathStr src_tree: PathStr @@ -28,11 +37,17 @@ class CmdGraphNode: """Parsed .cmd file describing how the file at absolute_path was built, or None if not available.""" cmd_file_dependencies: list["CmdGraphNode"] = field(default_factory=list) + incbin_dependencies: list[IncbinDependency] = field(default_factory=list) + hardcoded_dependencies: list["CmdGraphNode"] = field(default_factory=list) @property def children(self) -> Iterator["CmdGraphNode"]: seen: set[PathStr] = set() - for node in self.cmd_file_dependencies: + for node in chain( + self.cmd_file_dependencies, + (dep.node for dep in self.incbin_dependencies), + self.hardcoded_dependencies, + ): if node.absolute_path not in seen: seen.add(node.absolute_path) yield node @@ -95,6 +110,13 @@ class CmdGraphNode: def _build_child_node(child_path: PathStr) -> "CmdGraphNode": return CmdGraphNode.create(child_path, config, cache, depth + 1) + node.hardcoded_dependencies = [ + _build_child_node(hardcoded_dependency_path) + for hardcoded_dependency_path in get_hardcoded_dependencies( + target_path_absolute, config.obj_tree, config.src_tree + ) + ] + if cmd_file is not None: node.cmd_file_dependencies = [ _build_child_node(cmd_file_dependency_path) @@ -103,6 +125,15 @@ class CmdGraphNode: ) ] + if node.absolute_path.endswith(".S"): + node.incbin_dependencies = [ + IncbinDependency( + node=_build_child_node(incbin_statement.path), + full_statement=incbin_statement.full_statement, + ) + for incbin_statement in parse_incbin_statements(node.absolute_path) + ] + return node diff --git a/scripts/sbom/sbom/cmd_graph/hardcoded_dependencies.py b/scripts/sbom/sbom/cmd_graph/hardcoded_dependencies.py new file mode 100644 index 000000000000..2eb04d30f4e6 --- /dev/null +++ b/scripts/sbom/sbom/cmd_graph/hardcoded_dependencies.py @@ -0,0 +1,87 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +import os +from typing import Callable +import sbom.sbom_logging as sbom_logging +from sbom.path_utils import PathStr, is_relative_to +from sbom.environment import Environment + +HARDCODED_DEPENDENCIES: dict[str, list[str]] = { + # defined in linux/Kbuild + "include/generated/rq-offsets.h": ["kernel/sched/rq-offsets.s"], + "kernel/sched/rq-offsets.s": ["include/generated/asm-offsets.h"], + "include/generated/bounds.h": ["kernel/bounds.s"], + "include/generated/asm-offsets.h": ["arch/{arch}/kernel/asm-offsets.s"], +} +""" +Maps file paths to the list of dependencies required to build them +which are not tracked by the .cmd dependency mechanism. +Paths are relative to either the source tree or the object tree. +""" + +def get_hardcoded_dependencies(path: PathStr, obj_tree: PathStr, src_tree: PathStr) -> list[PathStr]: + """ + Some files in the kernel build process are not tracked by the .cmd dependency mechanism. + Parsing these dependencies programmatically is too complex for the scope of this project. + Therefore, this function provides manually defined dependencies to be added to the build graph. + + Args: + path: absolute path to a file within the src tree or object tree. + obj_tree: absolute Path to the base directory of the object tree. + src_tree: absolute Path to the `linux` source directory. + + Returns: + list[PathStr]: A list of dependency file paths (relative to the object tree) required to build the file at the given path. + """ + if is_relative_to(path, obj_tree): + path = os.path.relpath(path, obj_tree) + elif is_relative_to(path, src_tree): + path = os.path.relpath(path, src_tree) + + if path not in HARDCODED_DEPENDENCIES: + return [] + + template_variables: dict[str, Callable[[], str | None]] = { + "arch": lambda: _get_arch(path), + } + + dependencies: list[PathStr] = [] + for dependency_template in HARDCODED_DEPENDENCIES[path]: + dependency = _evaluate_template(dependency_template, template_variables) + if dependency is None: + continue + if os.path.exists(os.path.join(obj_tree, dependency)): + dependencies.append(dependency) + elif os.path.exists(dependency_absolute := os.path.join(src_tree, dependency)): + dependencies.append(os.path.relpath(dependency_absolute, obj_tree)) + else: + sbom_logging.error( + "Skip hardcoded dependency '{dependency}' for '{path}' because the dependency lies neither in the src tree nor the object tree.", + dependency=dependency, + path=path, + ) + + return dependencies + + +def _evaluate_template(template: str, variables: dict[str, Callable[[], str | None]]) -> str | None: + for key, value_function in variables.items(): + template_key = "{" + key + "}" + if template_key in template: + value = value_function() + if value is None: + return None + template = template.replace(template_key, value) + return template + + +def _get_arch(path: PathStr): + srcarch = Environment.SRCARCH() + if srcarch is None: + sbom_logging.error( + "Skipped architecture specific hardcoded dependency for '{path}' because the SRCARCH environment variable was not set.", + path=path, + ) + return None + return srcarch diff --git a/scripts/sbom/sbom/cmd_graph/incbin_parser.py b/scripts/sbom/sbom/cmd_graph/incbin_parser.py new file mode 100644 index 000000000000..ca289c2b8888 --- /dev/null +++ b/scripts/sbom/sbom/cmd_graph/incbin_parser.py @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from dataclasses import dataclass +import re + +from sbom.path_utils import PathStr + +INCBIN_PATTERN = re.compile(r'\s*\.incbin\s+"(?P[^"]+)"') +"""Regex pattern for matching `.incbin ""` statements.""" + + +@dataclass +class IncbinStatement: + """A parsed `.incbin ""` directive.""" + + path: PathStr + """path to the file referenced by the `.incbin` directive.""" + + full_statement: str + """Full `.incbin ""` statement as it originally appeared in the file.""" + + +def parse_incbin_statements(absolute_path: PathStr) -> list[IncbinStatement]: + """ + Parses `.incbin` directives from an `.S` assembly file. + + Args: + absolute_path: Absolute path to the `.S` assembly file. + + Returns: + list[IncbinStatement]: Parsed `.incbin` statements. + """ + with open(absolute_path, "rt", encoding="utf-8") as f: + content = f.read() + return [ + IncbinStatement( + path=match.group("path"), + full_statement=match.group(0).strip(), + ) + for match in INCBIN_PATTERN.finditer(content) + ] -- cgit v1.2.3 From 06f4e57165caf3012876b211cb687ba802188aff Mon Sep 17 00:00:00 2001 From: Luis Augenstein Date: Mon, 18 May 2026 08:20:54 +0200 Subject: scripts/sbom: add SPDX classes Implement Python dataclasses to model the SPDX classes required within an SPDX document. The class and property names are consistent with the SPDX 3.0.1 specification. Assisted-by: Cursor:claude-sonnet-4-5 Assisted-by: OpenCode:GLM-4-7 Co-developed-by: Maximilian Huber Signed-off-by: Maximilian Huber Signed-off-by: Luis Augenstein Signed-off-by: Greg Kroah-Hartman --- scripts/sbom/sbom/spdx/__init__.py | 7 ++ scripts/sbom/sbom/spdx/build.py | 17 +++ scripts/sbom/sbom/spdx/core.py | 170 ++++++++++++++++++++++++++++++ scripts/sbom/sbom/spdx/serialization.py | 62 +++++++++++ scripts/sbom/sbom/spdx/simplelicensing.py | 20 ++++ scripts/sbom/sbom/spdx/software.py | 69 ++++++++++++ scripts/sbom/sbom/spdx/spdxId.py | 36 +++++++ 7 files changed, 381 insertions(+) create mode 100644 scripts/sbom/sbom/spdx/__init__.py create mode 100644 scripts/sbom/sbom/spdx/build.py create mode 100644 scripts/sbom/sbom/spdx/core.py create mode 100644 scripts/sbom/sbom/spdx/serialization.py create mode 100644 scripts/sbom/sbom/spdx/simplelicensing.py create mode 100644 scripts/sbom/sbom/spdx/software.py create mode 100644 scripts/sbom/sbom/spdx/spdxId.py (limited to 'scripts') diff --git a/scripts/sbom/sbom/spdx/__init__.py b/scripts/sbom/sbom/spdx/__init__.py new file mode 100644 index 000000000000..4097b59f8f17 --- /dev/null +++ b/scripts/sbom/sbom/spdx/__init__.py @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from .spdxId import SpdxId, SpdxIdGenerator +from .serialization import JsonLdSpdxDocument + +__all__ = ["JsonLdSpdxDocument", "SpdxId", "SpdxIdGenerator"] diff --git a/scripts/sbom/sbom/spdx/build.py b/scripts/sbom/sbom/spdx/build.py new file mode 100644 index 000000000000..a39ec9c09b16 --- /dev/null +++ b/scripts/sbom/sbom/spdx/build.py @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from dataclasses import dataclass, field +from sbom.spdx.core import DictionaryEntry, Element, Hash + + +@dataclass(kw_only=True) +class Build(Element): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Build/Classes/Build/""" + + type: str = field(init=False, default="build_Build") + build_buildType: str + build_buildId: str + build_environment: list[DictionaryEntry] = field(default_factory=list) + build_configSourceUri: list[str] = field(default_factory=list) + build_configSourceDigest: list[Hash] = field(default_factory=list) diff --git a/scripts/sbom/sbom/spdx/core.py b/scripts/sbom/sbom/spdx/core.py new file mode 100644 index 000000000000..7eb376a1cd88 --- /dev/null +++ b/scripts/sbom/sbom/spdx/core.py @@ -0,0 +1,170 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from dataclasses import dataclass, field + +from typing import Any, Literal +from sbom.spdx.spdxId import SpdxId + +SPDX_SPEC_VERSION = "3.0.1" + +ExternalIdentifierType = Literal["email", "gitoid", "urlScheme"] +HashAlgorithm = Literal["sha256", "sha512"] +ProfileIdentifierType = Literal["core", "software", "build", "lite", "simpleLicensing"] +RelationshipType = Literal[ + "contains", + "generates", + "hasDeclaredLicense", + "hasInput", + "hasOutput", + "ancestorOf", + "hasDistributionArtifact", + "dependsOn", +] +RelationshipCompleteness = Literal["complete", "incomplete", "noAssertion"] + + +@dataclass +class SpdxObject: + def to_dict(self) -> dict[str, Any]: + def _to_dict(v: Any): + return v.to_dict() if hasattr(v, "to_dict") else v + + d: dict[str, Any] = {} + for field_name in self.__dataclass_fields__: + value = getattr(self, field_name) + if value is None or value == [] or value == "": + continue + + if isinstance(value, Element): + d[field_name] = value.spdxId + elif isinstance(value, list) and len(value) > 0 and isinstance(value[0], Element): # type: ignore + value: list[Element] = value + d[field_name] = [v.spdxId for v in value] + else: + d[field_name] = [_to_dict(v) for v in value] if isinstance(value, list) else _to_dict(value) # type: ignore + return d + + +@dataclass(kw_only=True) +class IntegrityMethod(SpdxObject): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/IntegrityMethod/""" + + +@dataclass(kw_only=True) +class Hash(IntegrityMethod): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/Hash/""" + + type: str = field(init=False, default="Hash") + hashValue: str + algorithm: HashAlgorithm + + +@dataclass(kw_only=True) +class Element(SpdxObject): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/Element/""" + + type: str = field(init=False, default="Element") + spdxId: SpdxId + creationInfo: str = "_:creationinfo" + name: str | None = None + verifiedUsing: list[Hash] = field(default_factory=list) + comment: str | None = None + + +@dataclass(kw_only=True) +class ExternalMap(SpdxObject): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/ExternalMap/""" + + type: str = field(init=False, default="ExternalMap") + externalSpdxId: SpdxId + + +@dataclass(kw_only=True) +class NamespaceMap(SpdxObject): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/NamespaceMap/""" + + type: str = field(init=False, default="NamespaceMap") + prefix: str + namespace: str + + +@dataclass(kw_only=True) +class ElementCollection(Element): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/ElementCollection/""" + + type: str = field(init=False, default="ElementCollection") + element: list[Element] = field(default_factory=list) + rootElement: list[Element] = field(default_factory=list) + profileConformance: list[ProfileIdentifierType] = field(default_factory=list) + + +@dataclass(kw_only=True) +class SpdxDocument(ElementCollection): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/SpdxDocument/""" + + type: str = field(init=False, default="SpdxDocument") + import_: list[ExternalMap] = field(default_factory=list) + namespaceMap: list[NamespaceMap] = field(default_factory=list) + + def to_dict(self) -> dict[str, Any]: + return {("import" if k == "import_" else k): v for k, v in super().to_dict().items()} + + +@dataclass(kw_only=True) +class Agent(Element): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/Agent/""" + + type: str = field(init=False, default="Agent") + + +@dataclass(kw_only=True) +class SoftwareAgent(Agent): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/SoftwareAgent/""" + + type: str = field(init=False, default="SoftwareAgent") + + +@dataclass(kw_only=True) +class CreationInfo(SpdxObject): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/CreationInfo/""" + + type: str = field(init=False, default="CreationInfo") + id: SpdxId = "_:creationinfo" + specVersion: str = SPDX_SPEC_VERSION + createdBy: list[Agent] + created: str + comment: str | None = None + + def to_dict(self) -> dict[str, Any]: + return {("@id" if k == "id" else k): v for k, v in super().to_dict().items()} + + +@dataclass(kw_only=True) +class Relationship(Element): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/Relationship/""" + + type: str = field(init=False, default="Relationship") + relationshipType: RelationshipType + from_: Element # underscore because 'from' is a reserved keyword + to: list[Element] + completeness: RelationshipCompleteness | None = None + + def to_dict(self) -> dict[str, Any]: + return {("from" if k == "from_" else k): v for k, v in super().to_dict().items()} + + +@dataclass(kw_only=True) +class Artifact(Element): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/Artifact/""" + + type: str = field(init=False, default="Artifact") + + +@dataclass(kw_only=True) +class DictionaryEntry(SpdxObject): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/DictionaryEntry/""" + + type: str = field(init=False, default="DictionaryEntry") + key: str + value: str diff --git a/scripts/sbom/sbom/spdx/serialization.py b/scripts/sbom/sbom/spdx/serialization.py new file mode 100644 index 000000000000..b4df7d368d46 --- /dev/null +++ b/scripts/sbom/sbom/spdx/serialization.py @@ -0,0 +1,62 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +import json +from typing import Any +from sbom.path_utils import PathStr +from sbom.spdx.core import SPDX_SPEC_VERSION, SpdxDocument, SpdxObject + + +class JsonLdSpdxDocument: + """Represents an SPDX document in JSON-LD format for serialization.""" + + graph: list[SpdxObject] + + def __init__(self, graph: list[SpdxObject]) -> None: + """ + Initialize a JSON-LD SPDX document from a graph of SPDX objects. + The graph must contain a single SpdxDocument element. + + Args: + graph: List of SPDX objects representing the complete SPDX document. + """ + self.graph = graph + + @property + def context(self) -> list[str | dict[str, str]]: + spdx_document = next(element for element in self.graph if isinstance(element, SpdxDocument)) + return [ + f"https://spdx.org/rdf/{SPDX_SPEC_VERSION}/spdx-context.jsonld", + {ns.prefix: ns.namespace for ns in spdx_document.namespaceMap}, + ] + + def to_dict(self) -> dict[str, Any]: + """ + Convert the SPDX document to a dictionary representation suitable for JSON serialization. + + Returns: + Dictionary with @context and @graph keys following JSON-LD format. + """ + def _item_to_dict(item: SpdxObject) -> dict: + d = item.to_dict() + if isinstance(item, SpdxDocument): + d.pop("namespaceMap", None) + return d + return { + "@context": self.context, + "@graph": [_item_to_dict(item) for item in self.graph], + } + + def save(self, path: PathStr, prettify: bool) -> None: + """ + Save the SPDX document to a JSON file. + + Args: + path: File path where the document will be saved. + prettify: Whether to pretty-print the JSON with indentation. + """ + with open(path, "w", encoding="utf-8") as f: + if prettify: + json.dump(self.to_dict(), f, indent=2) + else: + json.dump(self.to_dict(), f, separators=(",", ":")) diff --git a/scripts/sbom/sbom/spdx/simplelicensing.py b/scripts/sbom/sbom/spdx/simplelicensing.py new file mode 100644 index 000000000000..750ddd24ad89 --- /dev/null +++ b/scripts/sbom/sbom/spdx/simplelicensing.py @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from dataclasses import dataclass, field +from sbom.spdx.core import Element + + +@dataclass(kw_only=True) +class AnyLicenseInfo(Element): + """https://spdx.github.io/spdx-spec/v3.0.1/model/SimpleLicensing/Classes/AnyLicenseInfo/""" + + type: str = field(init=False, default="simplelicensing_AnyLicenseInfo") + + +@dataclass(kw_only=True) +class LicenseExpression(AnyLicenseInfo): + """https://spdx.github.io/spdx-spec/v3.0.1/model/SimpleLicensing/Classes/LicenseExpression/""" + + type: str = field(init=False, default="simplelicensing_LicenseExpression") + simplelicensing_licenseExpression: str diff --git a/scripts/sbom/sbom/spdx/software.py b/scripts/sbom/sbom/spdx/software.py new file mode 100644 index 000000000000..2f46de7c3167 --- /dev/null +++ b/scripts/sbom/sbom/spdx/software.py @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from dataclasses import dataclass, field +from typing import Literal +from sbom.spdx.core import Artifact, ElementCollection, IntegrityMethod + + +SbomType = Literal["source", "build"] +FileKindType = Literal["file", "directory"] +SoftwarePurpose = Literal[ + "source", + "archive", + "library", + "file", + "data", + "configuration", + "executable", + "module", + "application", + "documentation", + "other", +] +ContentIdentifierType = Literal["gitoid", "swhid"] + + +@dataclass(kw_only=True) +class Sbom(ElementCollection): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Software/Classes/Sbom/""" + + type: str = field(init=False, default="software_Sbom") + software_sbomType: list[SbomType] = field(default_factory=list) + + +@dataclass(kw_only=True) +class ContentIdentifier(IntegrityMethod): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Software/Classes/ContentIdentifier/""" + + type: str = field(init=False, default="software_ContentIdentifier") + software_contentIdentifierType: ContentIdentifierType + software_contentIdentifierValue: str + + +@dataclass(kw_only=True) +class SoftwareArtifact(Artifact): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Software/Classes/SoftwareArtifact/""" + + type: str = field(init=False, default="software_Artifact") + software_primaryPurpose: SoftwarePurpose | None = None + software_copyrightText: str | None = None + software_contentIdentifier: list[ContentIdentifier] = field(default_factory=list) + + +@dataclass(kw_only=True) +class Package(SoftwareArtifact): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Software/Classes/Package/""" + + type: str = field(init=False, default="software_Package") + name: str # type: ignore + software_packageVersion: str | None = None + + +@dataclass(kw_only=True) +class File(SoftwareArtifact): + """https://spdx.github.io/spdx-spec/v3.0.1/model/Software/Classes/File/""" + + type: str = field(init=False, default="software_File") + name: str # type: ignore + software_fileKind: FileKindType | None = None diff --git a/scripts/sbom/sbom/spdx/spdxId.py b/scripts/sbom/sbom/spdx/spdxId.py new file mode 100644 index 000000000000..589e85c5f706 --- /dev/null +++ b/scripts/sbom/sbom/spdx/spdxId.py @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from itertools import count +from typing import Iterator + +SpdxId = str + + +class SpdxIdGenerator: + _namespace: str + _prefix: str | None = None + _counter: Iterator[int] + + def __init__(self, namespace: str, prefix: str | None = None) -> None: + """ + Initialize the SPDX ID generator with a namespace. + + Args: + namespace: The full namespace to use for generated IDs. + prefix: Optional. If provided, generated IDs will use this prefix instead of the full namespace. + """ + self._namespace = namespace + self._prefix = prefix + self._counter = count(0) + + def generate(self) -> SpdxId: + return f"{f'{self._prefix}:' if self._prefix else self._namespace}{next(self._counter)}" + + @property + def prefix(self) -> str | None: + return self._prefix + + @property + def namespace(self) -> str: + return self._namespace -- cgit v1.2.3 From a68a29a1cc3ae6c129acdf945964dea16c8a49dc Mon Sep 17 00:00:00 2001 From: Luis Augenstein Date: Mon, 18 May 2026 08:20:55 +0200 Subject: scripts/sbom: add JSON-LD serialization Add infrastructure to serialize an SPDX graph as a JSON-LD document. NamespaceMaps in the SPDX document are converted to custom prefixes in the @context field of the JSON-LD output. The SBOM tool uses NamespaceMaps solely to shorten SPDX IDs, avoiding repetition of full namespace URIs by using short prefixes. Assisted-by: Cursor:claude-sonnet-4-5 Assisted-by: OpenCode:GLM-4-7 Co-developed-by: Maximilian Huber Signed-off-by: Maximilian Huber Signed-off-by: Luis Augenstein Signed-off-by: Greg Kroah-Hartman --- Makefile | 3 +- scripts/sbom/sbom.py | 56 +++++++++++++++++++++++ scripts/sbom/sbom/config.py | 56 +++++++++++++++++++++++ scripts/sbom/sbom/spdx_graph/__init__.py | 7 +++ scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py | 36 +++++++++++++++ scripts/sbom/sbom/spdx_graph/spdx_graph_model.py | 36 +++++++++++++++ 6 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 scripts/sbom/sbom/spdx_graph/__init__.py create mode 100644 scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py create mode 100644 scripts/sbom/sbom/spdx_graph/spdx_graph_model.py (limited to 'scripts') diff --git a/Makefile b/Makefile index 4c6133af5549..2443d4c82454 100644 --- a/Makefile +++ b/Makefile @@ -2212,7 +2212,8 @@ quiet_cmd_sbom = GEN $(sbom_targets) --src-tree $(abspath $(srctree)) \ --obj-tree $(abspath $(objtree)) \ --roots-file "$(tmp-target)" \ - --output-directory $(abspath $(objtree)); + --output-directory $(abspath $(objtree)) \ + --generate-spdx; PHONY += sbom sbom: $(notdir $(KBUILD_IMAGE)) include/generated/autoconf.h $(if $(CONFIG_MODULES),modules modules.order) $(call cmd,sbom) diff --git a/scripts/sbom/sbom.py b/scripts/sbom/sbom.py index d700e4f294f7..764175b9c893 100644 --- a/scripts/sbom/sbom.py +++ b/scripts/sbom/sbom.py @@ -6,13 +6,18 @@ Compute software bill of materials in SPDX format describing a kernel build. """ +import json import logging import os import sys import time +import uuid import sbom.sbom_logging as sbom_logging from sbom.config import get_config from sbom.path_utils import is_relative_to +from sbom.spdx import JsonLdSpdxDocument, SpdxIdGenerator +from sbom.spdx.core import CreationInfo, SpdxDocument +from sbom.spdx_graph import SpdxIdGeneratorCollection, build_spdx_graphs from sbom.cmd_graph import CmdGraph @@ -71,6 +76,57 @@ def main(): f.write("\n".join(str(file_path) for file_path in used_files)) logging.debug(f"Successfully saved {used_files_path}") + if config.generate_spdx is False: + _exit_with_summary(config.write_output_on_error) + return + + # Build SPDX Documents + logging.debug("Start generating SPDX graph based on cmd graph") + start_time = time.time() + + # The real uuid will be generated based on the content of the SPDX graphs + # to ensure that the same SPDX document is always assigned the same uuid. + PLACEHOLDER_UUID = "00000000-0000-0000-0000-000000000000" + spdx_id_base_namespace = f"{config.spdxId_prefix}{PLACEHOLDER_UUID}/" + spdx_id_generators = SpdxIdGeneratorCollection( + base=SpdxIdGenerator(prefix="p", namespace=spdx_id_base_namespace), + source=SpdxIdGenerator(prefix="s", namespace=f"{spdx_id_base_namespace}source/"), + build=SpdxIdGenerator(prefix="b", namespace=f"{spdx_id_base_namespace}build/"), + output=SpdxIdGenerator(prefix="o", namespace=f"{spdx_id_base_namespace}output/"), + ) + + spdx_graphs = build_spdx_graphs( + cmd_graph, + spdx_id_generators, + config, + ) + spdx_id_uuid = uuid.uuid5( + uuid.NAMESPACE_URL, + "".join( + json.dumps(element.to_dict()) for spdx_graph in spdx_graphs.values() for element in spdx_graph.to_list() + ), + ) + logging.debug(f"Generated SPDX graph in {time.time() - start_time} seconds") + + if not sbom_logging.has_errors() or config.write_output_on_error: + for kernel_sbom_kind, spdx_graph in spdx_graphs.items(): + spdx_graph_objects = spdx_graph.to_list() + # Add warning and error summary to creation info comment + creation_info = next(element for element in spdx_graph_objects if isinstance(element, CreationInfo)) + creation_info.comment = "\n".join([ + sbom_logging.summarize_warnings(), + sbom_logging.summarize_errors(), + ]).strip() + # Replace Placeholder uuid with real uuid for spdxIds + spdx_document = next(element for element in spdx_graph_objects if isinstance(element, SpdxDocument)) + for namespaceMap in spdx_document.namespaceMap: + namespaceMap.namespace = namespaceMap.namespace.replace(PLACEHOLDER_UUID, str(spdx_id_uuid)) + # Serialize SPDX graph to JSON-LD + spdx_doc = JsonLdSpdxDocument(graph=spdx_graph_objects) + save_path = os.path.join(config.output_directory, config.spdx_file_names[kernel_sbom_kind]) + spdx_doc.save(save_path, config.prettify_json) + logging.debug(f"Successfully saved {save_path}") + _exit_with_summary(config.write_output_on_error) diff --git a/scripts/sbom/sbom/config.py b/scripts/sbom/sbom/config.py index b8c1a2b404df..98c7d939364d 100644 --- a/scripts/sbom/sbom/config.py +++ b/scripts/sbom/sbom/config.py @@ -3,11 +3,18 @@ import argparse from dataclasses import dataclass +from enum import Enum import os from typing import Any from sbom.path_utils import PathStr +class KernelSpdxDocumentKind(Enum): + SOURCE = "source" + BUILD = "build" + OUTPUT = "output" + + @dataclass class KernelSbomConfig: src_tree: PathStr @@ -19,6 +26,13 @@ class KernelSbomConfig: root_paths: list[PathStr] """List of paths to root outputs (relative to obj_tree) to base the SBOM on.""" + generate_spdx: bool + """Whether to generate SPDX SBOM documents. If False, no SPDX files are created.""" + + spdx_file_names: dict[KernelSpdxDocumentKind, str] + """If `generate_spdx` is True, defines the file names for each SPDX SBOM kind + (source, build, output) to store on disk.""" + generate_used_files: bool """Whether to generate a flat list of all source files used in the build. If False, no used-files document is created.""" @@ -38,6 +52,12 @@ class KernelSbomConfig: write_output_on_error: bool """Whether to write output documents even if errors occur.""" + spdxId_prefix: str + """Prefix to use for all SPDX element IDs.""" + + prettify_json: bool + """Whether to pretty-print generated SPDX JSON documents.""" + def _parse_cli_arguments(parser: argparse.ArgumentParser) -> dict[str, Any]: """ @@ -67,6 +87,15 @@ def _parse_cli_arguments(parser: argparse.ArgumentParser) -> dict[str, Any]: "--roots-file", help="Path to a file containing the root paths (one per line). Cannot be used together with --roots.", ) + parser.add_argument( + "--generate-spdx", + action="store_true", + default=False, + help=( + "Whether to create sbom-source.spdx.json, sbom-build.spdx.json and " + "sbom-output.spdx.json documents (default: False)" + ), + ) parser.add_argument( "--generate-used-files", action="store_true", @@ -114,6 +143,20 @@ def _parse_cli_arguments(parser: argparse.ArgumentParser) -> dict[str, Any]: ), ) + # SPDX specific options + spdx_group = parser.add_argument_group("SPDX options", "Options for customizing SPDX document generation") + spdx_group.add_argument( + "--spdxId-prefix", + default="urn:spdx.dev:", + help="The prefix to use for all spdxId properties. (default: urn:spdx.dev:)", + ) + spdx_group.add_argument( + "--prettify-json", + action="store_true", + default=False, + help="Whether to pretty print the generated spdx.json documents (default: False)", + ) + args = vars(parser.parse_args()) return args @@ -144,6 +187,7 @@ def get_config() -> KernelSbomConfig: root_paths = args["roots"] _validate_path_arguments(parser, src_tree, obj_tree, root_paths) + generate_spdx = args["generate_spdx"] generate_used_files = args["generate_used_files"] output_directory = os.path.realpath(args["output_directory"]) debug = args["debug"] @@ -151,19 +195,31 @@ def get_config() -> KernelSbomConfig: fail_on_unknown_build_command = not args["do_not_fail_on_unknown_build_command"] write_output_on_error = args["write_output_on_error"] + spdxId_prefix = args["spdxId_prefix"] + prettify_json = args["prettify_json"] + # Hardcoded config + spdx_file_names = { + KernelSpdxDocumentKind.SOURCE: "sbom-source.spdx.json", + KernelSpdxDocumentKind.BUILD: "sbom-build.spdx.json", + KernelSpdxDocumentKind.OUTPUT: "sbom-output.spdx.json", + } used_files_file_name = "sbom.used-files.txt" return KernelSbomConfig( src_tree=src_tree, obj_tree=obj_tree, root_paths=root_paths, + generate_spdx=generate_spdx, + spdx_file_names=spdx_file_names, generate_used_files=generate_used_files, used_files_file_name=used_files_file_name, output_directory=output_directory, debug=debug, fail_on_unknown_build_command=fail_on_unknown_build_command, write_output_on_error=write_output_on_error, + spdxId_prefix=spdxId_prefix, + prettify_json=prettify_json, ) diff --git a/scripts/sbom/sbom/spdx_graph/__init__.py b/scripts/sbom/sbom/spdx_graph/__init__.py new file mode 100644 index 000000000000..3557b1d51bf9 --- /dev/null +++ b/scripts/sbom/sbom/spdx_graph/__init__.py @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from .build_spdx_graphs import build_spdx_graphs +from .spdx_graph_model import SpdxIdGeneratorCollection + +__all__ = ["build_spdx_graphs", "SpdxIdGeneratorCollection"] diff --git a/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py b/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py new file mode 100644 index 000000000000..bb3db4e423da --- /dev/null +++ b/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + + +from typing import Protocol + +from sbom.config import KernelSpdxDocumentKind +from sbom.cmd_graph import CmdGraph +from sbom.path_utils import PathStr +from sbom.spdx_graph.spdx_graph_model import SpdxGraph, SpdxIdGeneratorCollection + + +class SpdxGraphConfig(Protocol): + obj_tree: PathStr + src_tree: PathStr + + +def build_spdx_graphs( + cmd_graph: CmdGraph, + spdx_id_generators: SpdxIdGeneratorCollection, + config: SpdxGraphConfig, +) -> dict[KernelSpdxDocumentKind, SpdxGraph]: + """ + Builds SPDX graphs (output, source, and build) based on a cmd dependency graph. + If the source and object trees are identical, no dedicated source graph can be created. + In that case the source files are added to the build graph instead. + + Args: + cmd_graph: The dependency graph of a kernel build. + spdx_id_generators: Collection of SPDX ID generators. + config: Configuration options. + + Returns: + Dictionary of SPDX graphs + """ + return {} diff --git a/scripts/sbom/sbom/spdx_graph/spdx_graph_model.py b/scripts/sbom/sbom/spdx_graph/spdx_graph_model.py new file mode 100644 index 000000000000..682194d4362a --- /dev/null +++ b/scripts/sbom/sbom/spdx_graph/spdx_graph_model.py @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from dataclasses import dataclass +from sbom.spdx.core import CreationInfo, SoftwareAgent, SpdxDocument, SpdxObject +from sbom.spdx.software import Sbom +from sbom.spdx.spdxId import SpdxIdGenerator + + +@dataclass +class SpdxGraph: + """Represents the complete graph of a single SPDX document.""" + + spdx_document: SpdxDocument + agent: SoftwareAgent + creation_info: CreationInfo + sbom: Sbom + + def to_list(self) -> list[SpdxObject]: + return [ + self.spdx_document, + self.agent, + self.creation_info, + self.sbom, + *self.sbom.element, + ] + + +@dataclass +class SpdxIdGeneratorCollection: + """Holds SPDX ID generators for different document types to ensure globally unique SPDX IDs.""" + + base: SpdxIdGenerator + source: SpdxIdGenerator + build: SpdxIdGenerator + output: SpdxIdGenerator -- cgit v1.2.3 From 11f9c14d0e498933b586f8de2e3d0f0c9b22dfee Mon Sep 17 00:00:00 2001 From: Luis Augenstein Date: Mon, 18 May 2026 08:20:56 +0200 Subject: scripts/sbom: add shared SPDX elements Implement shared SPDX elements used in all three documents. Assisted-by: Cursor:claude-sonnet-4-5 Assisted-by: OpenCode:GLM-4-7 Co-developed-by: Maximilian Huber Signed-off-by: Maximilian Huber Signed-off-by: Luis Augenstein Signed-off-by: Greg Kroah-Hartman --- scripts/sbom/sbom/config.py | 9 ++++++ scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py | 5 +++- .../sbom/sbom/spdx_graph/shared_spdx_elements.py | 32 ++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 scripts/sbom/sbom/spdx_graph/shared_spdx_elements.py (limited to 'scripts') diff --git a/scripts/sbom/sbom/config.py b/scripts/sbom/sbom/config.py index 98c7d939364d..b1dd30790f5b 100644 --- a/scripts/sbom/sbom/config.py +++ b/scripts/sbom/sbom/config.py @@ -3,6 +3,7 @@ import argparse from dataclasses import dataclass +from datetime import datetime, timezone from enum import Enum import os from typing import Any @@ -52,6 +53,9 @@ class KernelSbomConfig: write_output_on_error: bool """Whether to write output documents even if errors occur.""" + created: datetime + """Datetime to use for the SPDX created property of the CreationInfo element.""" + spdxId_prefix: str """Prefix to use for all SPDX element IDs.""" @@ -195,6 +199,10 @@ def get_config() -> KernelSbomConfig: fail_on_unknown_build_command = not args["do_not_fail_on_unknown_build_command"] write_output_on_error = args["write_output_on_error"] + created = datetime.fromtimestamp( + max([os.path.getmtime(os.path.join(obj_tree, root_path)) for root_path in root_paths]), + tz=timezone.utc, + ) spdxId_prefix = args["spdxId_prefix"] prettify_json = args["prettify_json"] @@ -218,6 +226,7 @@ def get_config() -> KernelSbomConfig: debug=debug, fail_on_unknown_build_command=fail_on_unknown_build_command, write_output_on_error=write_output_on_error, + created=created, spdxId_prefix=spdxId_prefix, prettify_json=prettify_json, ) diff --git a/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py b/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py index bb3db4e423da..9c47258a31c6 100644 --- a/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py +++ b/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py @@ -1,18 +1,20 @@ # SPDX-License-Identifier: GPL-2.0-only OR MIT # Copyright (C) 2025 TNG Technology Consulting GmbH - +from datetime import datetime from typing import Protocol from sbom.config import KernelSpdxDocumentKind from sbom.cmd_graph import CmdGraph from sbom.path_utils import PathStr from sbom.spdx_graph.spdx_graph_model import SpdxGraph, SpdxIdGeneratorCollection +from sbom.spdx_graph.shared_spdx_elements import SharedSpdxElements class SpdxGraphConfig(Protocol): obj_tree: PathStr src_tree: PathStr + created: datetime def build_spdx_graphs( @@ -33,4 +35,5 @@ def build_spdx_graphs( Returns: Dictionary of SPDX graphs """ + shared_elements = SharedSpdxElements.create(spdx_id_generators.base, config.created) return {} diff --git a/scripts/sbom/sbom/spdx_graph/shared_spdx_elements.py b/scripts/sbom/sbom/spdx_graph/shared_spdx_elements.py new file mode 100644 index 000000000000..115e8778a467 --- /dev/null +++ b/scripts/sbom/sbom/spdx_graph/shared_spdx_elements.py @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from dataclasses import dataclass +from datetime import datetime, timezone +from sbom.spdx.core import CreationInfo, SoftwareAgent +from sbom.spdx.spdxId import SpdxIdGenerator + + +@dataclass(frozen=True) +class SharedSpdxElements: + agent: SoftwareAgent + creation_info: CreationInfo + + @classmethod + def create(cls, spdx_id_generator: SpdxIdGenerator, created: datetime) -> "SharedSpdxElements": + """ + Creates shared SPDX elements used across multiple documents. + + Args: + spdx_id_generator: Generator for creating SPDX IDs. + created: SPDX 'created' property used for the creation info. + + Returns: + SharedSpdxElements with agent and creation info. + """ + agent = SoftwareAgent( + spdxId=spdx_id_generator.generate(), + name="KernelSbom", + ) + creation_info = CreationInfo(createdBy=[agent], created=created.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")) + return SharedSpdxElements(agent=agent, creation_info=creation_info) -- cgit v1.2.3 From ef0675c8712fea2db638594e619e7a557c2502d6 Mon Sep 17 00:00:00 2001 From: Luis Augenstein Date: Mon, 18 May 2026 08:20:57 +0200 Subject: scripts/sbom: collect file metadata Implement the kernel_file module that collects file metadata, including license identifier for source files, SHA-256 hash, Git blob object ID, an estimation of the file type, and whether files belong to the source, build, or output SBOM. Assisted-by: Cursor:claude-sonnet-4-5 Assisted-by: OpenCode:GLM-4-7 Co-developed-by: Maximilian Huber Signed-off-by: Maximilian Huber Signed-off-by: Luis Augenstein Signed-off-by: Greg Kroah-Hartman --- scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py | 2 + scripts/sbom/sbom/spdx_graph/kernel_file.py | 315 ++++++++++++++++++++++ 2 files changed, 317 insertions(+) create mode 100644 scripts/sbom/sbom/spdx_graph/kernel_file.py (limited to 'scripts') diff --git a/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py b/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py index 9c47258a31c6..0f95f99d560a 100644 --- a/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py +++ b/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py @@ -7,6 +7,7 @@ from typing import Protocol from sbom.config import KernelSpdxDocumentKind from sbom.cmd_graph import CmdGraph from sbom.path_utils import PathStr +from sbom.spdx_graph.kernel_file import KernelFileCollection from sbom.spdx_graph.spdx_graph_model import SpdxGraph, SpdxIdGeneratorCollection from sbom.spdx_graph.shared_spdx_elements import SharedSpdxElements @@ -36,4 +37,5 @@ def build_spdx_graphs( Dictionary of SPDX graphs """ shared_elements = SharedSpdxElements.create(spdx_id_generators.base, config.created) + kernel_files = KernelFileCollection.create(cmd_graph, config.obj_tree, config.src_tree, spdx_id_generators) return {} diff --git a/scripts/sbom/sbom/spdx_graph/kernel_file.py b/scripts/sbom/sbom/spdx_graph/kernel_file.py new file mode 100644 index 000000000000..505f25f66ebb --- /dev/null +++ b/scripts/sbom/sbom/spdx_graph/kernel_file.py @@ -0,0 +1,315 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from dataclasses import dataclass +from enum import Enum +import hashlib +import os +import re +from sbom.cmd_graph import CmdGraph +from sbom.path_utils import PathStr, is_relative_to +from sbom.spdx import SpdxId, SpdxIdGenerator +from sbom.spdx.core import Hash +from sbom.spdx.software import ContentIdentifier, File, SoftwarePurpose +import sbom.sbom_logging as sbom_logging +from sbom.spdx_graph.spdx_graph_model import SpdxIdGeneratorCollection + + +class KernelFileLocation(Enum): + """Represents the location of a file relative to the source/object trees.""" + + SOURCE_TREE = "source_tree" + """File is located in the source tree.""" + OBJ_TREE = "obj_tree" + """File is located in the object tree.""" + EXTERNAL = "external" + """File is located outside both source and object trees.""" + BOTH = "both" + """File is located in a folder that is both source and object tree.""" + + +@dataclass +class KernelFile: + """kernel-specific metadata used to generate an SPDX File element.""" + + absolute_path: PathStr + """Absolute path of the file.""" + file_location: KernelFileLocation + """Location of the file relative to the source/object trees.""" + name: str + """Name of the file element. Should be relative to the source tree if + file_location equals SOURCE_TREE and relative to the object tree if + file_location equals OBJ_TREE. If file_location equals EXTERNAL, the + absolute path is used.""" + license_identifier: str | None + """SPDX license ID if file_location equals SOURCE_TREE or BOTH; otherwise None.""" + spdx_id_generator: SpdxIdGenerator + """Generator for the SPDX ID of the file element.""" + + _spdx_file_element: File | None = None + + @classmethod + def create( + cls, + absolute_path: PathStr, + obj_tree: PathStr, + src_tree: PathStr, + spdx_id_generators: SpdxIdGeneratorCollection, + is_output: bool, + ) -> "KernelFile": + is_in_obj_tree = is_relative_to(absolute_path, obj_tree) + is_in_src_tree = is_relative_to(absolute_path, src_tree) + + # file element name should be relative to output or src tree if possible + if not is_in_src_tree and not is_in_obj_tree: + file_element_name = str(absolute_path) + file_location = KernelFileLocation.EXTERNAL + spdx_id_generator = spdx_id_generators.source if src_tree != obj_tree else spdx_id_generators.build + elif is_in_src_tree and src_tree == obj_tree: + file_element_name = os.path.relpath(absolute_path, obj_tree) + file_location = KernelFileLocation.BOTH + spdx_id_generator = spdx_id_generators.output if is_output else spdx_id_generators.build + elif is_in_obj_tree: + file_element_name = os.path.relpath(absolute_path, obj_tree) + file_location = KernelFileLocation.OBJ_TREE + spdx_id_generator = spdx_id_generators.output if is_output else spdx_id_generators.build + else: + file_element_name = os.path.relpath(absolute_path, src_tree) + file_location = KernelFileLocation.SOURCE_TREE + spdx_id_generator = spdx_id_generators.source + + # parse spdx license identifier + license_identifier = ( + _parse_spdx_license_identifier(absolute_path) + if file_location == KernelFileLocation.SOURCE_TREE or file_location == KernelFileLocation.BOTH + else None + ) + + return KernelFile( + absolute_path, + file_location, + file_element_name, + license_identifier, + spdx_id_generator, + ) + + @property + def spdx_file_element(self) -> File: + if self._spdx_file_element is None: + self._spdx_file_element = _build_file_element( + self.absolute_path, + self.name, + self.spdx_id_generator.generate(), + self.file_location, + ) + return self._spdx_file_element + + +@dataclass +class KernelFileCollection: + """Collection of kernel files.""" + + source: dict[PathStr, KernelFile] + build: dict[PathStr, KernelFile] + output: dict[PathStr, KernelFile] + external: dict[PathStr, KernelFile] + + @classmethod + def create( + cls, + cmd_graph: CmdGraph, + obj_tree: PathStr, + src_tree: PathStr, + spdx_id_generators: SpdxIdGeneratorCollection, + ) -> "KernelFileCollection": + source: dict[PathStr, KernelFile] = {} + build: dict[PathStr, KernelFile] = {} + output: dict[PathStr, KernelFile] = {} + external: dict[PathStr, KernelFile] = {} + root_node_paths = {node.absolute_path for node in cmd_graph.roots} + for node in cmd_graph: + is_root = node.absolute_path in root_node_paths + kernel_file = KernelFile.create( + node.absolute_path, + obj_tree, + src_tree, + spdx_id_generators, + is_root, + ) + if is_root: + output[kernel_file.absolute_path] = kernel_file + elif kernel_file.file_location == KernelFileLocation.SOURCE_TREE: + source[kernel_file.absolute_path] = kernel_file + elif kernel_file.file_location == KernelFileLocation.EXTERNAL: + external[kernel_file.absolute_path] = kernel_file + else: + build[kernel_file.absolute_path] = kernel_file + + return KernelFileCollection(source, build, output, external) + + def to_dict(self) -> dict[PathStr, KernelFile]: + return {**self.source, **self.build, **self.output, **self.external} + + +def _build_file_element(absolute_path: PathStr, name: str, spdx_id: SpdxId, file_location: KernelFileLocation) -> File: + verifiedUsing: list[Hash] = [] + content_identifier: list[ContentIdentifier] = [] + if os.path.isfile(absolute_path): + verifiedUsing = [Hash(algorithm="sha256", hashValue=_sha256(absolute_path))] + content_identifier = [ + ContentIdentifier( + software_contentIdentifierType="gitoid", + software_contentIdentifierValue=_git_blob_oid(absolute_path), + ) + ] + elif file_location == KernelFileLocation.EXTERNAL: + sbom_logging.warning( + "Cannot compute hash for {absolute_path} because file does not exist.", + absolute_path=absolute_path, + ) + else: + sbom_logging.error( + "Cannot compute hash for {absolute_path} because file does not exist.", + absolute_path=absolute_path, + ) + + # primary purpose + primary_purpose = _get_primary_purpose(absolute_path) + + return File( + spdxId=spdx_id, + name=name, + verifiedUsing=verifiedUsing, + software_primaryPurpose=primary_purpose, + software_contentIdentifier=content_identifier, + ) + + +def _sha256(file_path: PathStr, chunk_size: int = 1 << 20) -> str: + """Compute the SHA-256 hex digest of a file, reading it in chunks of chunk_size bytes.""" + h = hashlib.sha256() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + h.update(chunk) + return h.hexdigest() + + +def _git_blob_oid(file_path: str, chunk_size: int = 1 << 20) -> str: + """Compute the Git blob object ID (SHA-1 hex) for a file, like `git hash-object`, reading it in chunks of chunk_size bytes.""" + h = hashlib.sha1() + h.update(f"blob {os.path.getsize(file_path)}\0".encode()) + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + h.update(chunk) + return h.hexdigest() + + +# REUSE-IgnoreStart +SPDX_LICENSE_IDENTIFIER_PATTERN = re.compile( + r"SPDX-License-Identifier:" # literal tag + r"\s*" # optional whitespace after colon + r"(?P.*?)" # license expression (non-greedy, stops before terminator) + r"(?:\s*" # optional whitespace before terminator (not captured) + r"(-->|\*/|$))", # terminator: XML "-->", C-style "*/", or end of line + re.MULTILINE, # match end of each line, not just end of string +) +# REUSE-IgnoreEnd + + +def _parse_spdx_license_identifier(absolute_path: str, max_bytes: int = 512) -> str | None: + """ + Extracts the SPDX-License-Identifier from the beginning of a source file. + + Args: + absolute_path: Path to the source file. + max_bytes: Maximum number of bytes to scan for the license identifier. + + Returns: + The license identifier string (e.g., 'GPL-2.0-only') if found, otherwise None. + """ + try: + with open(absolute_path, "r", encoding="utf-8") as f: + match = SPDX_LICENSE_IDENTIFIER_PATTERN.search(f.read(max_bytes)) + if match: + return match.group("id") + except (UnicodeDecodeError, OSError): + return None + return None + + +def _get_primary_purpose(absolute_path: PathStr) -> SoftwarePurpose | None: + def ends_with(suffixes: list[str]) -> bool: + return any(absolute_path.endswith(suffix) for suffix in suffixes) + + def includes_path_segments(path_segments: list[str]) -> bool: + return any(segment in absolute_path for segment in path_segments) + + # Source code + if ends_with([".c", ".h", ".S", ".s", ".rs", ".pl", "gen_smb1_mapping", "gen_smb2_mapping"]): + return "source" + + # Libraries + if ends_with([".a", ".so", ".so.raw", ".rlib"]): + return "library" + + # Archives + if ends_with([".xz", ".cpio", ".gz", ".tar", ".zip", "piggy_data"]): + return "archive" + + # Applications + if ends_with(["bzImage", "Image", ".efi"]): + return "application" + + # Executables / machine code + if ends_with([".bin", ".elf", "vmlinux", "vmlinux.unstripped", "vmlinuz", "bpfilter_umh"]): + return "executable" + + # Kernel modules + if ends_with([".ko"]): + return "module" + + # Data files + if ends_with( + [ + ".tbl", + ".relocs", + ".rmeta", + ".in", + ".dbg", + ".x509", + ".pbm", + ".ppm", + ".dtb", + ".uc", + ".inc", + ".dts", + ".dtsi", + ".dtbo", + ".xml", + ".ro", + "initramfs_inc_data", + "default_cpio_list", + "x509_certificate_list", + "utf8data.c_shipped", + "blacklist_hash_list", + "x509_revocation_list", + "cpucaps", + "sysreg", + "mach-types", + ] + ) or includes_path_segments(["drivers/gpu/drm/radeon/reg_srcs/"]): + return "data" + + # Configuration files + if ends_with([".pem", ".key", ".conf", ".config", ".cfg", ".bconf"]): + return "configuration" + + # Documentation + if ends_with([".md"]): + return "documentation" + + # Other / miscellaneous + if ends_with([".o", ".tmp"]): + return "other" + + sbom_logging.warning("Could not infer primary purpose for {absolute_path}", absolute_path=absolute_path) -- cgit v1.2.3 From b01912114e2c1b378287fcdd013bb9a894d1879e Mon Sep 17 00:00:00 2001 From: Luis Augenstein Date: Mon, 18 May 2026 08:20:58 +0200 Subject: scripts/sbom: add SPDX output graph Implement the SPDX output graph which contains the distributable build outputs and high level metadata about the build. Assisted-by: Cursor:claude-sonnet-4-5 Assisted-by: OpenCode:GLM-4-7 Co-developed-by: Maximilian Huber Signed-off-by: Maximilian Huber Signed-off-by: Luis Augenstein Signed-off-by: Greg Kroah-Hartman --- Makefile | 5 +- scripts/sbom/sbom/config.py | 64 ++++++++ scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py | 18 ++- scripts/sbom/sbom/spdx_graph/spdx_output_graph.py | 187 ++++++++++++++++++++++ 4 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 scripts/sbom/sbom/spdx_graph/spdx_output_graph.py (limited to 'scripts') diff --git a/Makefile b/Makefile index 2443d4c82454..e02d2a614c53 100644 --- a/Makefile +++ b/Makefile @@ -2213,7 +2213,10 @@ quiet_cmd_sbom = GEN $(sbom_targets) --obj-tree $(abspath $(objtree)) \ --roots-file "$(tmp-target)" \ --output-directory $(abspath $(objtree)) \ - --generate-spdx; + --generate-spdx \ + --package-license "GPL-2.0 WITH Linux-syscall-note" \ + --package-version "$(KERNELVERSION)" \ + --write-output-on-error; PHONY += sbom sbom: $(notdir $(KBUILD_IMAGE)) include/generated/autoconf.h $(if $(CONFIG_MODULES),modules modules.order) $(call cmd,sbom) diff --git a/scripts/sbom/sbom/config.py b/scripts/sbom/sbom/config.py index b1dd30790f5b..6811f782943e 100644 --- a/scripts/sbom/sbom/config.py +++ b/scripts/sbom/sbom/config.py @@ -59,6 +59,21 @@ class KernelSbomConfig: spdxId_prefix: str """Prefix to use for all SPDX element IDs.""" + build_type: str + """SPDX buildType property to use for all Build elements.""" + + build_id: str | None + """SPDX buildId property to use for all Build elements.""" + + package_license: str + """License expression applied to all SPDX Packages.""" + + package_version: str | None + """Version string applied to all SPDX Packages.""" + + package_copyright_text: str | None + """Copyright text applied to all SPDX Packages.""" + prettify_json: bool """Whether to pretty-print generated SPDX JSON documents.""" @@ -154,6 +169,40 @@ def _parse_cli_arguments(parser: argparse.ArgumentParser) -> dict[str, Any]: default="urn:spdx.dev:", help="The prefix to use for all spdxId properties. (default: urn:spdx.dev:)", ) + spdx_group.add_argument( + "--build-type", + default="urn:spdx.dev:Kbuild", + help="The SPDX buildType property to use for all Build elements. (default: urn:spdx.dev:Kbuild)", + ) + spdx_group.add_argument( + "--build-id", + default=None, + help="The SPDX buildId property to use for all Build elements.\n" + "If not provided the spdxId of the high level Build element is used as the buildId. (default: None)", + ) + spdx_group.add_argument( + "--package-license", + default="NOASSERTION", + help=( + "The SPDX licenseExpression property to use for the LicenseExpression " + "linked to all SPDX Package elements. (default: NOASSERTION)" + ), + ) + spdx_group.add_argument( + "--package-version", + default=None, + help="The SPDX packageVersion property to use for all SPDX Package elements. (default: None)", + ) + spdx_group.add_argument( + "--package-copyright-text", + default=None, + help=( + "The SPDX copyrightText property to use for all SPDX Package elements.\n" + "If not specified, and if a COPYING file exists in the source tree,\n" + "the package-copyright-text is set to the content of this file. " + "(default: None)" + ), + ) spdx_group.add_argument( "--prettify-json", action="store_true", @@ -204,6 +253,16 @@ def get_config() -> KernelSbomConfig: tz=timezone.utc, ) spdxId_prefix = args["spdxId_prefix"] + build_type = args["build_type"] + build_id = args["build_id"] + package_license = args["package_license"] + package_version = args["package_version"] if args["package_version"] is not None else None + package_copyright_text: str | None = None + if args["package_copyright_text"] is not None: + package_copyright_text = args["package_copyright_text"] + elif os.path.isfile(copying_path := os.path.join(src_tree, "COPYING")): + with open(copying_path, "r", encoding="utf-8") as f: + package_copyright_text = f.read() prettify_json = args["prettify_json"] # Hardcoded config @@ -228,6 +287,11 @@ def get_config() -> KernelSbomConfig: write_output_on_error=write_output_on_error, created=created, spdxId_prefix=spdxId_prefix, + build_type=build_type, + build_id=build_id, + package_license=package_license, + package_version=package_version, + package_copyright_text=package_copyright_text, prettify_json=prettify_json, ) diff --git a/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py b/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py index 0f95f99d560a..2af0fbe6cdbe 100644 --- a/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py +++ b/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py @@ -10,12 +10,18 @@ from sbom.path_utils import PathStr from sbom.spdx_graph.kernel_file import KernelFileCollection from sbom.spdx_graph.spdx_graph_model import SpdxGraph, SpdxIdGeneratorCollection from sbom.spdx_graph.shared_spdx_elements import SharedSpdxElements +from sbom.spdx_graph.spdx_output_graph import SpdxOutputGraph class SpdxGraphConfig(Protocol): obj_tree: PathStr src_tree: PathStr created: datetime + build_type: str + build_id: str | None + package_license: str + package_version: str | None + package_copyright_text: str | None def build_spdx_graphs( @@ -38,4 +44,14 @@ def build_spdx_graphs( """ shared_elements = SharedSpdxElements.create(spdx_id_generators.base, config.created) kernel_files = KernelFileCollection.create(cmd_graph, config.obj_tree, config.src_tree, spdx_id_generators) - return {} + output_graph = SpdxOutputGraph.create( + root_files=list(kernel_files.output.values()), + shared_elements=shared_elements, + spdx_id_generators=spdx_id_generators, + config=config, + ) + spdx_graphs: dict[KernelSpdxDocumentKind, SpdxGraph] = { + KernelSpdxDocumentKind.OUTPUT: output_graph, + } + + return spdx_graphs diff --git a/scripts/sbom/sbom/spdx_graph/spdx_output_graph.py b/scripts/sbom/sbom/spdx_graph/spdx_output_graph.py new file mode 100644 index 000000000000..ff9b2c31fb04 --- /dev/null +++ b/scripts/sbom/sbom/spdx_graph/spdx_output_graph.py @@ -0,0 +1,187 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from dataclasses import dataclass +import os +from typing import Protocol +from sbom.environment import Environment +from sbom.path_utils import PathStr +from sbom.spdx.build import Build +from sbom.spdx.core import DictionaryEntry, NamespaceMap, Relationship, SpdxDocument +from sbom.spdx.simplelicensing import LicenseExpression +from sbom.spdx.software import File, Package, Sbom +from sbom.spdx.spdxId import SpdxIdGenerator +from sbom.spdx_graph.kernel_file import KernelFile +from sbom.spdx_graph.shared_spdx_elements import SharedSpdxElements +from sbom.spdx_graph.spdx_graph_model import SpdxGraph, SpdxIdGeneratorCollection + + +class SpdxOutputGraphConfig(Protocol): + obj_tree: PathStr + src_tree: PathStr + build_type: str + build_id: str | None + package_license: str + package_version: str | None + package_copyright_text: str | None + + +@dataclass +class SpdxOutputGraph(SpdxGraph): + """SPDX graph representing distributable output files""" + + high_level_build_element: Build + + @classmethod + def create( + cls, + root_files: list[KernelFile], + shared_elements: SharedSpdxElements, + spdx_id_generators: SpdxIdGeneratorCollection, + config: SpdxOutputGraphConfig, + ) -> "SpdxOutputGraph": + """ + Args: + root_files: List of distributable output files which act as roots + of the dependency graph. + shared_elements: Shared SPDX elements used across multiple documents. + spdx_id_generators: Collection of SPDX ID generators. + config: Configuration options. + + Returns: + SpdxOutputGraph: The SPDX output graph. + """ + # SpdxDocument + spdx_document = SpdxDocument( + spdxId=spdx_id_generators.output.generate(), + profileConformance=["core", "software", "build", "simpleLicensing"], + namespaceMap=[ + NamespaceMap(prefix=generator.prefix, namespace=generator.namespace) + for generator in [spdx_id_generators.output, spdx_id_generators.base] + if generator.prefix is not None + ], + ) + + # Sbom + sbom = Sbom( + spdxId=spdx_id_generators.output.generate(), + software_sbomType=["build"], + ) + + # High-level Build elements + config_source_element = KernelFile.create( + absolute_path=os.path.join(config.obj_tree, ".config"), + obj_tree=config.obj_tree, + src_tree=config.src_tree, + spdx_id_generators=spdx_id_generators, + is_output=True, + ).spdx_file_element + high_level_build_element, high_level_build_element_hasOutput_relationship = _high_level_build_elements( + config.build_type, + config.build_id, + config_source_element, + spdx_id_generators.output, + ) + + # Root file elements + root_file_elements: list[File] = [file.spdx_file_element for file in root_files] + + # Package elements + package_elements = [ + Package( + spdxId=spdx_id_generators.output.generate(), + name=_get_package_name(file.name), + software_packageVersion=config.package_version, + software_copyrightText=config.package_copyright_text, + comment=f"Architecture={arch}" if (arch := Environment.ARCH() or Environment.SRCARCH()) else None, + software_primaryPurpose=file.software_primaryPurpose, + ) + for file in root_file_elements + ] + package_hasDistributionArtifact_file_relationships = [ + Relationship( + spdxId=spdx_id_generators.output.generate(), + relationshipType="hasDistributionArtifact", + from_=package, + to=[file], + ) + for package, file in zip(package_elements, root_file_elements) + ] + package_license_expression = LicenseExpression( + spdxId=spdx_id_generators.output.generate(), + simplelicensing_licenseExpression=config.package_license, + ) + package_hasDeclaredLicense_relationships = [ + Relationship( + spdxId=spdx_id_generators.output.generate(), + relationshipType="hasDeclaredLicense", + from_=package, + to=[package_license_expression], + ) + for package in package_elements + ] + + # Update relationships + spdx_document.rootElement = [sbom] + + sbom.rootElement = [*package_elements] + sbom.element = [ + config_source_element, + high_level_build_element, + high_level_build_element_hasOutput_relationship, + *root_file_elements, + *package_elements, + *package_hasDistributionArtifact_file_relationships, + package_license_expression, + *package_hasDeclaredLicense_relationships, + ] + + high_level_build_element_hasOutput_relationship.to = [*root_file_elements] + + output_graph = SpdxOutputGraph( + spdx_document, + shared_elements.agent, + shared_elements.creation_info, + sbom, + high_level_build_element, + ) + return output_graph + + +def _get_package_name(filename: str) -> str: + """ + Generates a SPDX package name from a filename. + Kernel images (bzImage, Image) get a descriptive name, others use the basename of the file. + """ + KERNEL_FILENAMES = ["bzImage", "Image"] + basename = os.path.basename(filename) + return f"Linux Kernel ({basename})" if basename in KERNEL_FILENAMES else basename + + +def _high_level_build_elements( + build_type: str, + build_id: str | None, + config_source_element: File, + spdx_id_generator: SpdxIdGenerator, +) -> tuple[Build, Relationship]: + build_spdxId = spdx_id_generator.generate() + high_level_build_element = Build( + spdxId=build_spdxId, + build_buildType=build_type, + build_buildId=build_id if build_id is not None else build_spdxId, + build_environment=[ + DictionaryEntry(key=key, value=value) + for key, value in Environment.KERNEL_BUILD_VARIABLES().items() + if value + ], + build_configSourceUri=[config_source_element.spdxId], + build_configSourceDigest=config_source_element.verifiedUsing, + ) + + high_level_build_element_hasOutput_relationship = Relationship( + spdxId=spdx_id_generator.generate(), + relationshipType="hasOutput", + from_=high_level_build_element, + to=[], + ) + return high_level_build_element, high_level_build_element_hasOutput_relationship -- cgit v1.2.3 From e70c84a5649e6e233326a22732dc08f9c31d4f43 Mon Sep 17 00:00:00 2001 From: Luis Augenstein Date: Mon, 18 May 2026 08:20:59 +0200 Subject: scripts/sbom: add SPDX source graph Implement the SPDX source graph which contains all source files involved during the build, along with the licensing information for each file. Assisted-by: Cursor:claude-sonnet-4-5 Assisted-by: OpenCode:GLM-4-7 Co-developed-by: Maximilian Huber Signed-off-by: Maximilian Huber Signed-off-by: Luis Augenstein Signed-off-by: Greg Kroah-Hartman --- scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py | 9 ++ scripts/sbom/sbom/spdx_graph/spdx_source_graph.py | 130 ++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 scripts/sbom/sbom/spdx_graph/spdx_source_graph.py (limited to 'scripts') diff --git a/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py b/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py index 2af0fbe6cdbe..f2567d44960b 100644 --- a/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py +++ b/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py @@ -10,6 +10,7 @@ from sbom.path_utils import PathStr from sbom.spdx_graph.kernel_file import KernelFileCollection from sbom.spdx_graph.spdx_graph_model import SpdxGraph, SpdxIdGeneratorCollection from sbom.spdx_graph.shared_spdx_elements import SharedSpdxElements +from sbom.spdx_graph.spdx_source_graph import SpdxSourceGraph from sbom.spdx_graph.spdx_output_graph import SpdxOutputGraph @@ -54,4 +55,12 @@ def build_spdx_graphs( KernelSpdxDocumentKind.OUTPUT: output_graph, } + if len(kernel_files.source) > 0: + spdx_graphs[KernelSpdxDocumentKind.SOURCE] = SpdxSourceGraph.create( + source_files=list(kernel_files.source.values()), + external_files=list(kernel_files.external.values()), + shared_elements=shared_elements, + spdx_id_generators=spdx_id_generators, + ) + return spdx_graphs diff --git a/scripts/sbom/sbom/spdx_graph/spdx_source_graph.py b/scripts/sbom/sbom/spdx_graph/spdx_source_graph.py new file mode 100644 index 000000000000..90880212dedd --- /dev/null +++ b/scripts/sbom/sbom/spdx_graph/spdx_source_graph.py @@ -0,0 +1,130 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from dataclasses import dataclass +from sbom.spdx import SpdxIdGenerator +from sbom.spdx.core import Element, NamespaceMap, Relationship, SpdxDocument +from sbom.spdx.simplelicensing import LicenseExpression +from sbom.spdx.software import File, Sbom +from sbom.spdx_graph.kernel_file import KernelFile +from sbom.spdx_graph.shared_spdx_elements import SharedSpdxElements +from sbom.spdx_graph.spdx_graph_model import SpdxGraph, SpdxIdGeneratorCollection + + +@dataclass +class SpdxSourceGraph(SpdxGraph): + """SPDX graph representing source files""" + + @classmethod + def create( + cls, + source_files: list[KernelFile], + external_files: list[KernelFile], + shared_elements: SharedSpdxElements, + spdx_id_generators: SpdxIdGeneratorCollection, + ) -> "SpdxSourceGraph": + """ + Args: + source_files: List of files within the kernel source tree. + external_files: Files outside both source and object trees. + shared_elements: Shared SPDX elements used across multiple documents. + spdx_id_generators: Collection of SPDX ID generators. + + Returns: + SpdxSourceGraph: The SPDX source graph. + """ + # SpdxDocument + source_spdx_document = SpdxDocument( + spdxId=spdx_id_generators.source.generate(), + profileConformance=["core", "software", "simpleLicensing"], + namespaceMap=[ + NamespaceMap(prefix=generator.prefix, namespace=generator.namespace) + for generator in [spdx_id_generators.source, spdx_id_generators.base] + if generator.prefix is not None + ], + ) + + # Sbom + source_sbom = Sbom( + spdxId=spdx_id_generators.source.generate(), + software_sbomType=["source"], + ) + + # Src Tree Elements + src_tree_element = File( + spdxId=spdx_id_generators.source.generate(), + name="$(src_tree)", + software_fileKind="directory", + ) + src_tree_contains_relationship = Relationship( + spdxId=spdx_id_generators.source.generate(), + relationshipType="contains", + from_=src_tree_element, + to=[], + ) + + # Source file elements + source_file_elements: list[Element] = [file.spdx_file_element for file in source_files] + external_file_elements: list[Element] = [file.spdx_file_element for file in external_files] + + # Source file license elements + source_file_license_identifiers, source_file_license_relationships = source_file_license_elements( + source_files, spdx_id_generators.source + ) + + # Update relationships + source_spdx_document.rootElement = [source_sbom] + source_sbom.rootElement = [src_tree_element] + source_sbom.element = [ + src_tree_element, + src_tree_contains_relationship, + *source_file_elements, + *external_file_elements, + *source_file_license_identifiers, + *source_file_license_relationships, + ] + src_tree_contains_relationship.to = source_file_elements + + source_graph = SpdxSourceGraph( + source_spdx_document, + shared_elements.agent, + shared_elements.creation_info, + source_sbom, + ) + return source_graph + + +def source_file_license_elements( + source_files: list[KernelFile], spdx_id_generator: SpdxIdGenerator +) -> tuple[list[LicenseExpression], list[Relationship]]: + """ + Creates SPDX license expressions and links them to the given source files + via hasDeclaredLicense relationships. + + Args: + source_files: List of files within the kernel source tree. + spdx_id_generator: Generator for unique SPDX IDs. + + Returns: + Tuple of (license expressions, hasDeclaredLicense relationships). + """ + license_expressions: dict[str, LicenseExpression] = {} + for file in source_files: + if file.license_identifier is None or file.license_identifier in license_expressions: + continue + license_expressions[file.license_identifier] = LicenseExpression( + spdxId=spdx_id_generator.generate(), + simplelicensing_licenseExpression=file.license_identifier, + ) + + source_file_license_relationships = [ + Relationship( + spdxId=spdx_id_generator.generate(), + relationshipType="hasDeclaredLicense", + from_=file.spdx_file_element, + to=[license_expressions[file.license_identifier]], + ) + for file in source_files + if file.license_identifier is not None + ] + return ([*license_expressions.values()], source_file_license_relationships) -- cgit v1.2.3 From db8d07ef5e81457eb330240945d56d83a5a68b01 Mon Sep 17 00:00:00 2001 From: Luis Augenstein Date: Mon, 18 May 2026 08:21:00 +0200 Subject: scripts/sbom: add SPDX build graph Implement the SPDX build graph to describe the relationships between source files in the source SBOM and output files in the output SBOM. Assisted-by: Cursor:claude-sonnet-4-5 Assisted-by: OpenCode:GLM-4-7 Co-developed-by: Maximilian Huber Signed-off-by: Maximilian Huber Signed-off-by: Luis Augenstein Signed-off-by: Greg Kroah-Hartman --- scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py | 17 ++ scripts/sbom/sbom/spdx_graph/spdx_build_graph.py | 318 ++++++++++++++++++++++ 2 files changed, 335 insertions(+) create mode 100644 scripts/sbom/sbom/spdx_graph/spdx_build_graph.py (limited to 'scripts') diff --git a/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py b/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py index f2567d44960b..ee24e9eaf603 100644 --- a/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py +++ b/scripts/sbom/sbom/spdx_graph/build_spdx_graphs.py @@ -4,6 +4,7 @@ from datetime import datetime from typing import Protocol +import logging from sbom.config import KernelSpdxDocumentKind from sbom.cmd_graph import CmdGraph from sbom.path_utils import PathStr @@ -11,6 +12,7 @@ from sbom.spdx_graph.kernel_file import KernelFileCollection from sbom.spdx_graph.spdx_graph_model import SpdxGraph, SpdxIdGeneratorCollection from sbom.spdx_graph.shared_spdx_elements import SharedSpdxElements from sbom.spdx_graph.spdx_source_graph import SpdxSourceGraph +from sbom.spdx_graph.spdx_build_graph import SpdxBuildGraph from sbom.spdx_graph.spdx_output_graph import SpdxOutputGraph @@ -62,5 +64,20 @@ def build_spdx_graphs( shared_elements=shared_elements, spdx_id_generators=spdx_id_generators, ) + else: + logging.info( + "Skipped creating a dedicated source SBOM because source files cannot be " + "reliably classified when the source and object trees are identical. " + "Added source files to the build SBOM instead." + ) + + build_graph = SpdxBuildGraph.create( + cmd_graph, + kernel_files, + shared_elements, + output_graph.high_level_build_element, + spdx_id_generators, + ) + spdx_graphs[KernelSpdxDocumentKind.BUILD] = build_graph return spdx_graphs diff --git a/scripts/sbom/sbom/spdx_graph/spdx_build_graph.py b/scripts/sbom/sbom/spdx_graph/spdx_build_graph.py new file mode 100644 index 000000000000..4d738bc3b3e2 --- /dev/null +++ b/scripts/sbom/sbom/spdx_graph/spdx_build_graph.py @@ -0,0 +1,318 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +from dataclasses import dataclass +from typing import Mapping +from sbom.cmd_graph import CmdGraph +from sbom.path_utils import PathStr +from sbom.spdx import SpdxIdGenerator +from sbom.spdx.build import Build +from sbom.spdx.core import ExternalMap, NamespaceMap, Relationship, SpdxDocument +from sbom.spdx.software import File, Sbom +from sbom.spdx_graph.kernel_file import KernelFileCollection +from sbom.spdx_graph.shared_spdx_elements import SharedSpdxElements +from sbom.spdx_graph.spdx_graph_model import SpdxGraph, SpdxIdGeneratorCollection +from sbom.spdx_graph.spdx_source_graph import source_file_license_elements + + +@dataclass +class SpdxBuildGraph(SpdxGraph): + """SPDX graph representing build dependencies connecting source files and + distributable output files""" + + @classmethod + def create( + cls, + cmd_graph: CmdGraph, + kernel_files: KernelFileCollection, + shared_elements: SharedSpdxElements, + high_level_build_element: Build, + spdx_id_generators: SpdxIdGeneratorCollection, + ) -> "SpdxBuildGraph": + if len(kernel_files.source) > 0: + return _create_spdx_build_graph( + cmd_graph, + kernel_files, + shared_elements, + high_level_build_element, + spdx_id_generators, + ) + else: + return _create_spdx_build_graph_with_mixed_sources( + cmd_graph, + kernel_files, + shared_elements, + high_level_build_element, + spdx_id_generators, + ) + + +def _create_spdx_build_graph( + cmd_graph: CmdGraph, + kernel_files: KernelFileCollection, + shared_elements: SharedSpdxElements, + high_level_build_element: Build, + spdx_id_generators: SpdxIdGeneratorCollection, +) -> SpdxBuildGraph: + """ + Creates an SPDX build graph where source and output files are referenced + from external documents. + + Args: + cmd_graph: The dependency graph of a kernel build. + kernel_files: Collection of categorized kernel files involved in the build. + shared_elements: SPDX elements shared across multiple documents. + high_level_build_element: The high-level Build element referenced by the build graph. + spdx_id_generators: Collection of generators for SPDX element IDs. + + Returns: + SpdxBuildGraph: The SPDX build graph connecting source files and distributable output files. + """ + # SpdxDocument + build_spdx_document = SpdxDocument( + spdxId=spdx_id_generators.build.generate(), + profileConformance=["core", "software", "build"], + namespaceMap=[ + NamespaceMap(prefix=generator.prefix, namespace=generator.namespace) + for generator in [ + spdx_id_generators.build, + spdx_id_generators.source, + spdx_id_generators.output, + spdx_id_generators.base, + ] + if generator.prefix is not None + ], + ) + + # Sbom + build_sbom = Sbom( + spdxId=spdx_id_generators.build.generate(), + software_sbomType=["build"], + ) + + # Src and object tree elements + obj_tree_element = File( + spdxId=spdx_id_generators.build.generate(), + name="$(obj_tree)", + software_fileKind="directory", + ) + obj_tree_contains_relationship = Relationship( + spdxId=spdx_id_generators.build.generate(), + relationshipType="contains", + from_=obj_tree_element, + to=[], + ) + + # File elements + build_file_elements = [file.spdx_file_element for file in kernel_files.build.values()] + file_relationships = _file_relationships( + cmd_graph=cmd_graph, + file_elements={key: file.spdx_file_element for key, file in kernel_files.to_dict().items()}, + high_level_build_element=high_level_build_element, + spdx_id_generator=spdx_id_generators.build, + ) + + # Update relationships + build_spdx_document.rootElement = [build_sbom] + + build_spdx_document.import_ = [ + *( + ExternalMap(externalSpdxId=file.spdx_file_element.spdxId) + for file in (*kernel_files.source.values(), *kernel_files.external.values()) + ), + ExternalMap(externalSpdxId=high_level_build_element.spdxId), + *(ExternalMap(externalSpdxId=file.spdx_file_element.spdxId) for file in kernel_files.output.values()), + ] + + build_sbom.rootElement = [obj_tree_element] + build_sbom.element = [ + obj_tree_element, + obj_tree_contains_relationship, + *build_file_elements, + *file_relationships, + ] + + obj_tree_contains_relationship.to = [ + *build_file_elements, + *(file.spdx_file_element for file in kernel_files.output.values()), + ] + + # create Spdx graphs + build_graph = SpdxBuildGraph( + build_spdx_document, + shared_elements.agent, + shared_elements.creation_info, + build_sbom, + ) + return build_graph + + +def _create_spdx_build_graph_with_mixed_sources( + cmd_graph: CmdGraph, + kernel_files: KernelFileCollection, + shared_elements: SharedSpdxElements, + high_level_build_element: Build, + spdx_id_generators: SpdxIdGeneratorCollection, +) -> SpdxBuildGraph: + """ + Creates an SPDX build graph where only output files are referenced from + an external document. Source files are included directly in the build graph. + + Args: + cmd_graph: The dependency graph of a kernel build. + kernel_files: Collection of categorized kernel files involved in the build. + shared_elements: SPDX elements shared across multiple documents. + high_level_build_element: The high-level Build element referenced by the build graph. + spdx_id_generators: Collection of generators for SPDX element IDs. + + Returns: + SpdxBuildGraph: The SPDX build graph connecting source files and distributable output files. + """ + # SpdxDocument + build_spdx_document = SpdxDocument( + spdxId=spdx_id_generators.build.generate(), + profileConformance=["core", "software", "build"], + namespaceMap=[ + NamespaceMap(prefix=generator.prefix, namespace=generator.namespace) + for generator in [ + spdx_id_generators.build, + spdx_id_generators.output, + spdx_id_generators.base, + ] + if generator.prefix is not None + ], + ) + + # Sbom + build_sbom = Sbom( + spdxId=spdx_id_generators.build.generate(), + software_sbomType=["build"], + ) + + # File elements + build_file_elements = [file.spdx_file_element for file in kernel_files.build.values()] + external_file_elements = [file.spdx_file_element for file in kernel_files.external.values()] + file_relationships = _file_relationships( + cmd_graph=cmd_graph, + file_elements={key: file.spdx_file_element for key, file in kernel_files.to_dict().items()}, + high_level_build_element=high_level_build_element, + spdx_id_generator=spdx_id_generators.build, + ) + + # Source file license elements + source_file_license_identifiers, source_file_license_relationships = source_file_license_elements( + list(kernel_files.build.values()), spdx_id_generators.build + ) + + # Update relationships + build_spdx_document.rootElement = [build_sbom] + root_file_elements = [file.spdx_file_element for file in kernel_files.output.values()] + build_spdx_document.import_ = [ + ExternalMap(externalSpdxId=high_level_build_element.spdxId), + *(ExternalMap(externalSpdxId=file.spdxId) for file in root_file_elements), + ] + + build_sbom.rootElement = [*root_file_elements] + build_sbom.element = [ + *build_file_elements, + *external_file_elements, + *source_file_license_identifiers, + *source_file_license_relationships, + *file_relationships, + ] + + build_graph = SpdxBuildGraph( + build_spdx_document, + shared_elements.agent, + shared_elements.creation_info, + build_sbom, + ) + return build_graph + + +def _file_relationships( + cmd_graph: CmdGraph, + file_elements: Mapping[PathStr, File], + high_level_build_element: Build, + spdx_id_generator: SpdxIdGenerator, +) -> list[Build | Relationship]: + """ + Construct SPDX Build and Relationship elements representing dependency + relationships in the cmd graph. + + Args: + cmd_graph: The dependency graph of a kernel build. + file_elements: Mapping of filesystem paths (PathStr) to their + corresponding SPDX File elements. + high_level_build_element: The SPDX Build element representing the overall build process/root. + spdx_id_generator: Generator for unique SPDX IDs. + + Returns: + list[Build | Relationship]: List of SPDX Build and Relationship elements + """ + high_level_build_ancestorOf_relationship = Relationship( + spdxId=spdx_id_generator.generate(), + relationshipType="ancestorOf", + from_=high_level_build_element, + completeness="complete", + to=[], + ) + + # Create a relationship between each node (output file) + # and its children (input files) + build_and_relationship_elements: list[Build | Relationship] = [high_level_build_ancestorOf_relationship] + for node in cmd_graph: + # .cmd file dependencies + if node.cmd_file is not None: + build_element = Build( + spdxId=spdx_id_generator.generate(), + build_buildType=high_level_build_element.build_buildType, + build_buildId=high_level_build_element.build_buildId, + comment=node.cmd_file.savedcmd, + ) + build_and_relationship_elements.append(build_element) + + if node.cmd_file_dependencies: + hasInput_relationship = Relationship( + spdxId=spdx_id_generator.generate(), + relationshipType="hasInput", + from_=build_element, + to=[file_elements[dep.absolute_path] for dep in node.cmd_file_dependencies], + ) + build_and_relationship_elements.append(hasInput_relationship) + + hasOutput_relationship = Relationship( + spdxId=spdx_id_generator.generate(), + relationshipType="hasOutput", + from_=build_element, + to=[file_elements[node.absolute_path]], + ) + build_and_relationship_elements.append(hasOutput_relationship) + + high_level_build_ancestorOf_relationship.to.append(build_element) + + # incbin dependencies + if len(node.incbin_dependencies) > 0: + incbin_dependsOn_relationship = Relationship( + spdxId=spdx_id_generator.generate(), + relationshipType="dependsOn", + comment="\n".join([incbin_dependency.full_statement for incbin_dependency in node.incbin_dependencies]), + from_=file_elements[node.absolute_path], + to=[ + file_elements[incbin_dependency.node.absolute_path] + for incbin_dependency in node.incbin_dependencies + ], + ) + build_and_relationship_elements.append(incbin_dependsOn_relationship) + + # hardcoded dependencies + if len(node.hardcoded_dependencies) > 0: + hardcoded_dependency_relationship = Relationship( + spdxId=spdx_id_generator.generate(), + relationshipType="dependsOn", + from_=file_elements[node.absolute_path], + to=[file_elements[n.absolute_path] for n in node.hardcoded_dependencies], + ) + build_and_relationship_elements.append(hardcoded_dependency_relationship) + + return build_and_relationship_elements -- cgit v1.2.3 From 0de18f407c8169b51a525e208c0cd690df2d2b1a Mon Sep 17 00:00:00 2001 From: Luis Augenstein Date: Mon, 18 May 2026 08:21:01 +0200 Subject: scripts/sbom: add unit tests for command parsers Add unit tests to verify that command parsers correctly extract input files from build commands. Assisted-by: Cursor:claude-sonnet-4-5 Assisted-by: OpenCode:GLM-4-7 Co-developed-by: Maximilian Huber Signed-off-by: Maximilian Huber Signed-off-by: Luis Augenstein Signed-off-by: Greg Kroah-Hartman --- scripts/sbom/tests/__init__.py | 0 scripts/sbom/tests/cmd_graph/__init__.py | 0 .../sbom/tests/cmd_graph/test_savedcmd_parser.py | 443 +++++++++++++++++++++ 3 files changed, 443 insertions(+) create mode 100644 scripts/sbom/tests/__init__.py create mode 100644 scripts/sbom/tests/cmd_graph/__init__.py create mode 100644 scripts/sbom/tests/cmd_graph/test_savedcmd_parser.py (limited to 'scripts') diff --git a/scripts/sbom/tests/__init__.py b/scripts/sbom/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/scripts/sbom/tests/cmd_graph/__init__.py b/scripts/sbom/tests/cmd_graph/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/scripts/sbom/tests/cmd_graph/test_savedcmd_parser.py b/scripts/sbom/tests/cmd_graph/test_savedcmd_parser.py new file mode 100644 index 000000000000..a061a748e1bf --- /dev/null +++ b/scripts/sbom/tests/cmd_graph/test_savedcmd_parser.py @@ -0,0 +1,443 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +import os +import unittest +from unittest.mock import patch + +from sbom.cmd_graph.savedcmd_parser import parse_inputs_from_commands +from sbom.cmd_graph.savedcmd_parser.command_parser_registry import CommandParserRegistry +import sbom.sbom_logging as sbom_logging + + +class TestSavedCmdParser(unittest.TestCase): + def _assert_parsing(self, cmd: str, expected: str, registry: CommandParserRegistry | None = None) -> None: + sbom_logging.init() + parsed = parse_inputs_from_commands(cmd, fail_on_unknown_build_command=False, registry=registry) + target = [] if expected == "" else expected.split(" ") + self.assertEqual(parsed, target) + errors = sbom_logging._error_logger._message_counts # type: ignore + self.assertEqual(errors, {}) + + # Compound command tests + def test_dd_cat(self): + cmd = "(dd if=arch/x86/boot/setup.bin bs=4k conv=sync status=none; cat arch/x86/boot/vmlinux.bin) >arch/x86/boot/bzImage" + expected = "arch/x86/boot/setup.bin arch/x86/boot/vmlinux.bin" + self._assert_parsing(cmd, expected) + + def test_manual_file_creation(self): + cmd = """{ symbase=__dtbo_overlay_bad_unresolved; echo '$(pound)include '; echo '.section .rodata,"a"'; echo '.balign STRUCT_ALIGNMENT'; echo ".global $${symbase}_begin"; echo "$${symbase}_begin:"; echo '.incbin "drivers/of/unittest-data/overlay_bad_unresolved.dtbo" '; echo ".global $${symbase}_end"; echo "$${symbase}_end:"; echo '.balign STRUCT_ALIGNMENT'; } > drivers/of/unittest-data/overlay_bad_unresolved.dtbo.S""" + expected = "" + self._assert_parsing(cmd, expected) + + def test_cat_xz_wrap(self): + cmd = "{ cat arch/x86/boot/compressed/vmlinux.bin | sh ../scripts/xz_wrap.sh; printf \\130\\064\\024\\000; } > arch/x86/boot/compressed/vmlinux.bin.xz" + expected = "arch/x86/boot/compressed/vmlinux.bin" + self._assert_parsing(cmd, expected) + + def test_printf_sed(self): + cmd = r"""{ printf 'static char tomoyo_builtin_profile[] __initdata =\n'; sed -e 's/\\/\\\\/g' -e 's/\"/\\"/g' -e 's/\(.*\)/\t"\1\\n"/' -- /dev/null; printf '\t"";\n'; printf 'static char tomoyo_builtin_exception_policy[] __initdata =\n'; sed -e 's/\\/\\\\/g' -e 's/\"/\\"/g' -e 's/\(.*\)/\t"\1\\n"/' -- ../security/tomoyo/policy/exception_policy.conf.default; printf '\t"";\n'; printf 'static char tomoyo_builtin_domain_policy[] __initdata =\n'; sed -e 's/\\/\\\\/g' -e 's/\"/\\"/g' -e 's/\(.*\)/\t"\1\\n"/' -- /dev/null; printf '\t"";\n'; printf 'static char tomoyo_builtin_manager[] __initdata =\n'; sed -e 's/\\/\\\\/g' -e 's/\"/\\"/g' -e 's/\(.*\)/\t"\1\\n"/' -- /dev/null; printf '\t"";\n'; printf 'static char tomoyo_builtin_stat[] __initdata =\n'; sed -e 's/\\/\\\\/g' -e 's/\"/\\"/g' -e 's/\(.*\)/\t"\1\\n"/' -- /dev/null; printf '\t"";\n'; } > security/tomoyo/builtin-policy.h""" + expected = "../security/tomoyo/policy/exception_policy.conf.default" + self._assert_parsing(cmd, expected) + + def test_bin2c_echo(self): + cmd = """(echo "static char tomoyo_builtin_profile[] __initdata ="; ./scripts/bin2c security/tomoyo/builtin-policy.h""" + expected = "../security/tomoyo/policy/exception_policy.conf.default" + self._assert_parsing(cmd, expected) + + def test_cat_colon(self): + cmd = "{ cat init/modules.order; cat usr/modules.order; cat arch/x86/modules.order; cat arch/x86/boot/startup/modules.order; cat kernel/modules.order; cat certs/modules.order; cat mm/modules.order; cat fs/modules.order; cat ipc/modules.order; cat security/modules.order; cat crypto/modules.order; cat block/modules.order; cat io_uring/modules.order; cat lib/modules.order; cat arch/x86/lib/modules.order; cat drivers/modules.order; cat sound/modules.order; cat samples/modules.order; cat net/modules.order; cat virt/modules.order; cat arch/x86/pci/modules.order; cat arch/x86/power/modules.order; cat arch/x86/video/modules.order; :; } > modules.order" + expected = "init/modules.order usr/modules.order arch/x86/modules.order arch/x86/boot/startup/modules.order kernel/modules.order certs/modules.order mm/modules.order fs/modules.order ipc/modules.order security/modules.order crypto/modules.order block/modules.order io_uring/modules.order lib/modules.order arch/x86/lib/modules.order drivers/modules.order sound/modules.order samples/modules.order net/modules.order virt/modules.order arch/x86/pci/modules.order arch/x86/power/modules.order arch/x86/video/modules.order" + self._assert_parsing(cmd, expected) + + def test_cat_zstd(self): + cmd = "{ cat arch/x86/boot/compressed/vmlinux.bin arch/x86/boot/compressed/vmlinux.relocs | zstd -22 --ultra; printf \\340\\362\\066\\003; } > arch/x86/boot/compressed/vmlinux.bin.zst" + expected = "arch/x86/boot/compressed/vmlinux.bin arch/x86/boot/compressed/vmlinux.relocs" + self._assert_parsing(cmd, expected) + + # cat command tests + def test_cat_redirect(self): + cmd = "cat ../fs/unicode/utf8data.c_shipped > fs/unicode/utf8data.c" + expected = "../fs/unicode/utf8data.c_shipped" + self._assert_parsing(cmd, expected) + + def test_cat_piped(self): + cmd = "cat arch/x86/boot/compressed/vmlinux.bin arch/x86/boot/compressed/vmlinux.relocs | gzip -n -f -9 > arch/x86/boot/compressed/vmlinux.bin.gz" + expected = "arch/x86/boot/compressed/vmlinux.bin arch/x86/boot/compressed/vmlinux.relocs" + self._assert_parsing(cmd, expected) + + # sed command tests + def test_sed(self): + cmd = "sed -n 's/.*define *BLIST_\\([A-Z0-9_]*\\) *.*/BLIST_FLAG_NAME(\\1),/p' ../include/scsi/scsi_devinfo.h > drivers/scsi/scsi_devinfo_tbl.c" + expected = "../include/scsi/scsi_devinfo.h" + self._assert_parsing(cmd, expected) + + # awk command tests + def test_awk(self): + cmd = "awk -f ../arch/arm64/tools/gen-cpucaps.awk ../arch/arm64/tools/cpucaps > arch/arm64/include/generated/asm/cpucap-defs.h" + expected = "../arch/arm64/tools/cpucaps" + self._assert_parsing(cmd, expected) + + def test_awk_with_input_redirection(self): + cmd = "awk -v N=1 -f ../lib/raid6/unroll.awk < ../lib/raid6/int.uc > lib/raid6/int1.c" + expected = "../lib/raid6/int.uc" + self._assert_parsing(cmd, expected) + + # openssl command tests + def test_openssl(self): + cmd = "openssl req -new -nodes -utf8 -sha256 -days 36500 -batch -x509 -config certs/x509.genkey -outform PEM -out certs/signing_key.pem -keyout certs/signing_key.pem 2>&1" + expected = "" + self._assert_parsing(cmd, expected) + + # gcc/clang command tests + def test_gcc(self): + cmd = ( + "gcc -Wp,-MMD,arch/x86/pci/.i386.o.d -nostdinc -I../arch/x86/include -I./arch/x86/include/generated -I../include -I./include -I../arch/x86/include/uapi -I./arch/x86/include/generated/uapi -I../include/uapi -I./include/generated/uapi -include ../include/linux/compiler-version.h -include ../include/linux/kconfig.h -include ../include/linux/compiler_types.h -D__KERNEL__ -fmacro-prefix-map=../= -Werror -std=gnu11 -fshort-wchar -funsigned-char -fno-common -fno-PIE -fno-strict-aliasing -mno-sse -mno-mmx -mno-sse2 -mno-3dnow -mno-avx -fcf-protection=branch -fno-jump-tables -m64 -falign-jumps=1 -falign-loops=1 -mno-80387 -mno-fp-ret-in-387 -mpreferred-stack-boundary=3 -mskip-rax-setup -march=x86-64 -mtune=generic -mno-red-zone -mcmodel=kernel -mstack-protector-guard-reg=gs -mstack-protector-guard-symbol=__ref_stack_chk_guard -Wno-sign-compare -fno-asynchronous-unwind-tables -mindirect-branch=thunk-extern -mindirect-branch-register -mindirect-branch-cs-prefix -mfunction-return=thunk-extern -fno-jump-tables -fpatchable-function-entry=16,16 -fno-delete-null-pointer-checks -O2 -fno-allow-store-data-races -fstack-protector-strong -fomit-frame-pointer -fno-stack-clash-protection -falign-functions=16 -fno-strict-overflow -fno-stack-check -fconserve-stack -fno-builtin-wcslen -Wall -Wextra -Wundef -Werror=implicit-function-declaration -Werror=implicit-int -Werror=return-type -Werror=strict-prototypes -Wno-format-security -Wno-trigraphs -Wno-frame-address -Wno-address-of-packed-member -Wmissing-declarations -Wmissing-prototypes -Wframe-larger-than=2048 -Wno-main -Wvla-larger-than=1 -Wno-pointer-sign -Wcast-function-type -Wno-array-bounds -Wno-stringop-overflow -Wno-alloc-size-larger-than -Wimplicit-fallthrough=5 -Werror=date-time -Werror=incompatible-pointer-types -Werror=designated-init -Wenum-conversion -Wunused -Wno-unused-but-set-variable -Wno-unused-const-variable -Wno-packed-not-aligned -Wno-format-overflow -Wno-format-truncation -Wno-stringop-truncation -Wno-override-init -Wno-missing-field-initializers -Wno-type-limits -Wno-shift-negative-value -Wno-maybe-uninitialized -Wno-sign-compare -Wno-unused-parameter -I../arch/x86/pci -Iarch/x86/pci -DKBUILD_MODFILE=" + "arch/x86/pci/i386" + " -DKBUILD_BASENAME=" + "i386" + " -DKBUILD_MODNAME=" + "i386" + " -D__KBUILD_MODNAME=kmod_i386 -c -o arch/x86/pci/i386.o ../arch/x86/pci/i386.c " + ) + expected = "../arch/x86/pci/i386.c" + self._assert_parsing(cmd, expected) + + def test_gcc_linking(self): + cmd = "gcc -o arch/x86/tools/relocs arch/x86/tools/relocs_32.o arch/x86/tools/relocs_64.o arch/x86/tools/relocs_common.o" + expected = "arch/x86/tools/relocs_32.o arch/x86/tools/relocs_64.o arch/x86/tools/relocs_common.o" + self._assert_parsing(cmd, expected) + + def test_gcc_without_compile_flag(self): + cmd = "gcc -Wp,-MMD,arch/x86/boot/compressed/.mkpiggy.d -Wall -Wmissing-prototypes -Wstrict-prototypes -O2 -fomit-frame-pointer -std=gnu11 -I ../scripts/include -I../tools/include -I arch/x86/boot/compressed -o arch/x86/boot/compressed/mkpiggy ../arch/x86/boot/compressed/mkpiggy.c" + expected = "../arch/x86/boot/compressed/mkpiggy.c" + self._assert_parsing(cmd, expected) + + def test_gcc_with_env_override(self): + with patch.dict(os.environ, {"CC": "ccache gcc"}): + registry = CommandParserRegistry.create() + cmd = "gcc -o arch/x86/tools/relocs arch/x86/tools/relocs_32.o arch/x86/tools/relocs_64.o arch/x86/tools/relocs_common.o" + expected = "arch/x86/tools/relocs_32.o arch/x86/tools/relocs_64.o arch/x86/tools/relocs_common.o" + self._assert_parsing(cmd, expected, registry) + self._assert_parsing(f"ccache {cmd}", expected, registry) + + def test_gcc_dts_preprocessing(self): + cmd = "gcc -E -Wp,-MMD,drivers/of/.empty_root.dtb.d.pre.tmp -nostdinc -I ../scripts/dtc/include-prefixes -undef -D__DTS__ -x assembler-with-cpp -o drivers/of/.empty_root.dtb.dts.tmp ../drivers/of/empty_root.dts" + expected = "../drivers/of/empty_root.dts" + self._assert_parsing(cmd, expected) + + def test_clang(self): + cmd = """clang -Wp,-MMD,arch/x86/entry/.entry_64_compat.o.d -nostdinc -I../arch/x86/include -I./arch/x86/include/generated -I../include -I./include -I../arch/x86/include/uapi -I./arch/x86/include/generated/uapi -I../include/uapi -I./include/generated/uapi -include ../include/linux/compiler-version.h -include ../include/linux/kconfig.h -D__KERNEL__ --target=x86_64-linux-gnu -fintegrated-as -Werror=unknown-warning-option -Werror=ignored-optimization-argument -Werror=option-ignored -Werror=unused-command-line-argument -fmacro-prefix-map=../= -Werror -D__ASSEMBLY__ -fno-PIE -m64 -I../arch/x86/entry -Iarch/x86/entry -DKBUILD_MODFILE='"arch/x86/entry/entry_64_compat"' -DKBUILD_MODNAME='"entry_64_compat"' -D__KBUILD_MODNAME=kmod_entry_64_compat -c -o arch/x86/entry/entry_64_compat.o ../arch/x86/entry/entry_64_compat.S""" + expected = "../arch/x86/entry/entry_64_compat.S" + self._assert_parsing(cmd, expected) + + # ld command tests + def test_ld(self): + cmd = r'ld -o arch/x86/entry/vdso/vdso64.so.dbg -shared --hash-style=both --build-id=sha1 --no-undefined --eh-frame-hdr -Bsymbolic -z noexecstack -m elf_x86_64 -soname linux-vdso.so.1 -z max-page-size=4096 -T arch/x86/entry/vdso/vdso.lds arch/x86/entry/vdso/vdso-note.o arch/x86/entry/vdso/vclock_gettime.o arch/x86/entry/vdso/vgetcpu.o arch/x86/entry/vdso/vgetrandom.o arch/x86/entry/vdso/vgetrandom-chacha.o; if readelf -rW arch/x86/entry/vdso/vdso64.so.dbg | grep -v _NONE | grep -q " R_\w*_"; then (echo >&2 "arch/x86/entry/vdso/vdso64.so.dbg: dynamic relocations are not supported"; rm -f arch/x86/entry/vdso/vdso64.so.dbg; /bin/false); fi' + expected = "arch/x86/entry/vdso/vdso-note.o arch/x86/entry/vdso/vclock_gettime.o arch/x86/entry/vdso/vgetcpu.o arch/x86/entry/vdso/vgetrandom.o arch/x86/entry/vdso/vgetrandom-chacha.o" + self._assert_parsing(cmd, expected) + + def test_ld_with_env_override(self): + with patch.dict(os.environ, {"LD": "some-tool ld"}): + registry = CommandParserRegistry.create() + cmd = r'ld -o arch/x86/entry/vdso/vdso64.so.dbg -shared --hash-style=both --build-id=sha1 --no-undefined --eh-frame-hdr -Bsymbolic -z noexecstack -m elf_x86_64 -soname linux-vdso.so.1 -z max-page-size=4096 -T arch/x86/entry/vdso/vdso.lds arch/x86/entry/vdso/vdso-note.o arch/x86/entry/vdso/vclock_gettime.o arch/x86/entry/vdso/vgetcpu.o arch/x86/entry/vdso/vgetrandom.o arch/x86/entry/vdso/vgetrandom-chacha.o; if readelf -rW arch/x86/entry/vdso/vdso64.so.dbg | grep -v _NONE | grep -q " R_\w*_"; then (echo >&2 "arch/x86/entry/vdso/vdso64.so.dbg: dynamic relocations are not supported"; rm -f arch/x86/entry/vdso/vdso64.so.dbg; /bin/false); fi' + expected = "arch/x86/entry/vdso/vdso-note.o arch/x86/entry/vdso/vclock_gettime.o arch/x86/entry/vdso/vgetcpu.o arch/x86/entry/vdso/vgetrandom.o arch/x86/entry/vdso/vgetrandom-chacha.o" + self._assert_parsing(cmd, expected, registry) + self._assert_parsing(f"some-tool {cmd}", expected, registry) + + def test_ld_whole_archive(self): + cmd = "ld -m elf_x86_64 -z noexecstack -r -o vmlinux.o --whole-archive vmlinux.a --no-whole-archive --start-group --end-group" + expected = "vmlinux.a" + self._assert_parsing(cmd, expected) + + def test_ld_with_at_symbol(self): + cmd = "ld.lld -m elf_x86_64 -z noexecstack -r -o fs/efivarfs/efivarfs.o @fs/efivarfs/efivarfs.mod ; ./tools/objtool/objtool --hacks=jump_label --hacks=noinstr --hacks=skylake --ibt --orc --retpoline --rethunk --static-call --uaccess --prefix=16 --link --module fs/efivarfs/efivarfs.o" + expected = "@fs/efivarfs/efivarfs.mod" + self._assert_parsing(cmd, expected) + + def test_ld_if_objdump(self): + cmd = """ld -o arch/x86/entry/vdso/vdso64.so.dbg -shared --hash-style=both --build-id=sha1 --eh-frame-hdr -Bsymbolic -z noexecstack -m elf_x86_64 -soname linux-vdso.so.1 --no-undefined -z max-page-size=4096 -T arch/x86/entry/vdso/vdso.lds arch/x86/entry/vdso/vdso-note.o arch/x86/entry/vdso/vclock_gettime.o arch/x86/entry/vdso/vgetcpu.o arch/x86/entry/vdso/vsgx.o && sh ./arch/x86/entry/vdso/checkundef.sh 'nm' 'arch/x86/entry/vdso/vdso64.so.dbg'; if objdump -R arch/x86/entry/vdso/vdso64.so.dbg | grep -E -h "R_X86_64_JUMP_SLOT|R_X86_64_GLOB_DAT|R_X86_64_RELATIVE| R_386_GLOB_DAT|R_386_JMP_SLOT|R_386_RELATIVE"; then (echo >&2 "arch/x86/entry/vdso/vdso64.so.dbg: dynamic relocations are not supported"; rm -f arch/x86/entry/vdso/vdso64.so.dbg; /bin/false); fi""" + expected = "arch/x86/entry/vdso/vdso-note.o arch/x86/entry/vdso/vclock_gettime.o arch/x86/entry/vdso/vgetcpu.o arch/x86/entry/vdso/vsgx.o" + self._assert_parsing(cmd, expected) + + # printf | xargs ar command tests + def test_ar_printf(self): + cmd = 'rm -f built-in.a; printf "./%s " init/built-in.a usr/built-in.a arch/x86/built-in.a arch/x86/boot/startup/built-in.a kernel/built-in.a certs/built-in.a mm/built-in.a fs/built-in.a ipc/built-in.a security/built-in.a crypto/built-in.a block/built-in.a io_uring/built-in.a lib/built-in.a arch/x86/lib/built-in.a drivers/built-in.a sound/built-in.a net/built-in.a virt/built-in.a arch/x86/pci/built-in.a arch/x86/power/built-in.a arch/x86/video/built-in.a | xargs ar cDPrST built-in.a' + expected = "./init/built-in.a ./usr/built-in.a ./arch/x86/built-in.a ./arch/x86/boot/startup/built-in.a ./kernel/built-in.a ./certs/built-in.a ./mm/built-in.a ./fs/built-in.a ./ipc/built-in.a ./security/built-in.a ./crypto/built-in.a ./block/built-in.a ./io_uring/built-in.a ./lib/built-in.a ./arch/x86/lib/built-in.a ./drivers/built-in.a ./sound/built-in.a ./net/built-in.a ./virt/built-in.a ./arch/x86/pci/built-in.a ./arch/x86/power/built-in.a ./arch/x86/video/built-in.a" + self._assert_parsing(cmd, expected) + + def test_ar_printf_nested(self): + cmd = 'rm -f arch/x86/pci/built-in.a; printf "arch/x86/pci/%s " i386.o init.o mmconfig_64.o direct.o mmconfig-shared.o fixup.o acpi.o legacy.o irq.o common.o early.o bus_numa.o amd_bus.o | xargs ar cDPrST arch/x86/pci/built-in.a' + expected = "arch/x86/pci/i386.o arch/x86/pci/init.o arch/x86/pci/mmconfig_64.o arch/x86/pci/direct.o arch/x86/pci/mmconfig-shared.o arch/x86/pci/fixup.o arch/x86/pci/acpi.o arch/x86/pci/legacy.o arch/x86/pci/irq.o arch/x86/pci/common.o arch/x86/pci/early.o arch/x86/pci/bus_numa.o arch/x86/pci/amd_bus.o" + self._assert_parsing(cmd, expected) + + # ar command tests + def test_ar_reordering(self): + cmd = "rm -f vmlinux.a; ar cDPrST vmlinux.a built-in.a lib/lib.a arch/x86/lib/lib.a; ar mPiT $$(ar t vmlinux.a | sed -n 1p) vmlinux.a $$(ar t vmlinux.a | grep -F -f ../scripts/head-object-list.txt)" + expected = "built-in.a lib/lib.a arch/x86/lib/lib.a" + self._assert_parsing(cmd, expected) + + def test_ar_default(self): + cmd = "rm -f lib/lib.a; ar cDPrsT lib/lib.a lib/argv_split.o lib/bug.o lib/buildid.o lib/clz_tab.o lib/cmdline.o lib/cpumask.o lib/ctype.o lib/dec_and_lock.o lib/decompress.o lib/decompress_bunzip2.o lib/decompress_inflate.o lib/decompress_unlz4.o lib/decompress_unlzma.o lib/decompress_unlzo.o lib/decompress_unxz.o lib/decompress_unzstd.o lib/dump_stack.o lib/earlycpio.o lib/extable.o lib/flex_proportions.o lib/idr.o lib/iomem_copy.o lib/irq_regs.o lib/is_single_threaded.o lib/klist.o lib/kobject.o lib/kobject_uevent.o lib/logic_pio.o lib/maple_tree.o lib/memcat_p.o lib/nmi_backtrace.o lib/objpool.o lib/plist.o lib/radix-tree.o lib/ratelimit.o lib/rbtree.o lib/seq_buf.o lib/siphash.o lib/string.o lib/sys_info.o lib/timerqueue.o lib/union_find.o lib/vsprintf.o lib/win_minmax.o lib/xarray.o" + expected = "lib/argv_split.o lib/bug.o lib/buildid.o lib/clz_tab.o lib/cmdline.o lib/cpumask.o lib/ctype.o lib/dec_and_lock.o lib/decompress.o lib/decompress_bunzip2.o lib/decompress_inflate.o lib/decompress_unlz4.o lib/decompress_unlzma.o lib/decompress_unlzo.o lib/decompress_unxz.o lib/decompress_unzstd.o lib/dump_stack.o lib/earlycpio.o lib/extable.o lib/flex_proportions.o lib/idr.o lib/iomem_copy.o lib/irq_regs.o lib/is_single_threaded.o lib/klist.o lib/kobject.o lib/kobject_uevent.o lib/logic_pio.o lib/maple_tree.o lib/memcat_p.o lib/nmi_backtrace.o lib/objpool.o lib/plist.o lib/radix-tree.o lib/ratelimit.o lib/rbtree.o lib/seq_buf.o lib/siphash.o lib/string.o lib/sys_info.o lib/timerqueue.o lib/union_find.o lib/vsprintf.o lib/win_minmax.o lib/xarray.o" + self._assert_parsing(cmd, expected) + + def test_ar_llvm(self): + cmd = "llvm-ar mPiT $$(llvm-ar t vmlinux.a | sed -n 1p) vmlinux.a $$(llvm-ar t vmlinux.a | grep -F -f ../scripts/head-object-list.txt)" + expected = "" + self._assert_parsing(cmd, expected) + + # nm command tests + def test_nm(self): + cmd = """llvm-nm -p --defined-only rust/core.o | awk '$$2~/(T|R|D|B)/ && $$3!~/__(pfx|cfi|odr_asan)/ { printf "EXPORT_SYMBOL_RUST_GPL(%s);\n",$$3 }' > rust/exports_core_generated.h""" + expected = "rust/core.o" + self._assert_parsing(cmd, expected) + + def test_nm_vmlinux(self): + cmd = r"nm vmlinux | sed -n -e 's/^\([0-9a-fA-F]*\) [ABbCDGRSTtVW] \(_text\|__start_rodata\|__bss_start\|_end\)$/#define VO_\2 _AC(0x\1,UL)/p' > arch/x86/boot/voffset.h" + expected = "vmlinux" + self._assert_parsing(cmd, expected) + + # objcopy command tests + def test_objcopy(self): + cmd = "objcopy --remove-section='.rel*' --remove-section=!'.rel*.dyn' vmlinux.unstripped vmlinux" + expected = "vmlinux.unstripped" + self._assert_parsing(cmd, expected) + + def test_objcopy_llvm(self): + cmd = "llvm-objcopy --remove-section='.rel*' --remove-section=!'.rel*.dyn' vmlinux.unstripped vmlinux" + expected = "vmlinux.unstripped" + self._assert_parsing(cmd, expected) + + # strip command tests + def test_strip(self): + cmd = "strip --strip-debug -o drivers/firmware/efi/libstub/mem.stub.o drivers/firmware/efi/libstub/mem.o" + expected = "drivers/firmware/efi/libstub/mem.o" + self._assert_parsing(cmd, expected) + + # cp command tests + def test_cp_truncate(self): + cmd = "cp arch/arm64/boot/Image arch/arm64/boot/vmlinux.bin; truncate -s $$(hexdump -s16 -n4 -e '\"%u\"' arch/arm64/boot/Image) arch/arm64/boot/vmlinux.bin" + expected = "arch/arm64/boot/Image" + self._assert_parsing(cmd, expected) + + # rustc command tests + def test_rustc(self): + cmd = """OBJTREE=/workspace/linux/kernel_build rustc -Zbinary_dep_depinfo=y -Astable_features -Dnon_ascii_idents -Dunsafe_op_in_unsafe_fn -Wmissing_docs -Wrust_2018_idioms -Wclippy::all -Wclippy::as_ptr_cast_mut -Wclippy::as_underscore -Wclippy::cast_lossless -Wclippy::ignored_unit_patterns -Wclippy::mut_mut -Wclippy::needless_bitwise_bool -Aclippy::needless_lifetimes -Wclippy::no_mangle_with_rust_abi -Wclippy::ptr_as_ptr -Wclippy::ptr_cast_constness -Wclippy::ref_as_ptr -Wclippy::undocumented_unsafe_blocks -Wclippy::unnecessary_safety_comment -Wclippy::unnecessary_safety_doc -Wrustdoc::missing_crate_level_docs -Wrustdoc::unescaped_backticks -Cpanic=abort -Cembed-bitcode=n -Clto=n -Cforce-unwind-tables=n -Ccodegen-units=1 -Csymbol-mangling-version=v0 -Crelocation-model=static -Zfunction-sections=n -Wclippy::float_arithmetic --target=./scripts/target.json -Ctarget-feature=-sse,-sse2,-sse3,-ssse3,-sse4.1,-sse4.2,-avx,-avx2 -Zcf-protection=branch -Zno-jump-tables -Ctarget-cpu=x86-64 -Ztune-cpu=generic -Cno-redzone=y -Ccode-model=kernel -Zfunction-return=thunk-extern -Zpatchable-function-entry=16,16 -Copt-level=2 -Cdebug-assertions=n -Coverflow-checks=y -Dwarnings @./include/generated/rustc_cfg --edition=2021 --cfg no_fp_fmt_parse --emit=dep-info=rust/.core.o.d --emit=obj=rust/core.o --emit=metadata=rust/libcore.rmeta --crate-type rlib -L./rust --crate-name core /usr/lib/rust-1.84/lib/rustlib/src/rust/library/core/src/lib.rs --sysroot=/dev/null ;llvm-objcopy --redefine-sym __addsf3=__rust__addsf3 --redefine-sym __eqsf2=__rust__eqsf2 --redefine-sym __extendsfdf2=__rust__extendsfdf2 --redefine-sym __gesf2=__rust__gesf2 --redefine-sym __lesf2=__rust__lesf2 --redefine-sym __ltsf2=__rust__ltsf2 --redefine-sym __mulsf3=__rust__mulsf3 --redefine-sym __nesf2=__rust__nesf2 --redefine-sym __truncdfsf2=__rust__truncdfsf2 --redefine-sym __unordsf2=__rust__unordsf2 --redefine-sym __adddf3=__rust__adddf3 --redefine-sym __eqdf2=__rust__eqdf2 --redefine-sym __ledf2=__rust__ledf2 --redefine-sym __ltdf2=__rust__ltdf2 --redefine-sym __muldf3=__rust__muldf3 --redefine-sym __unorddf2=__rust__unorddf2 --redefine-sym __muloti4=__rust__muloti4 --redefine-sym __multi3=__rust__multi3 --redefine-sym __udivmodti4=__rust__udivmodti4 --redefine-sym __udivti3=__rust__udivti3 --redefine-sym __umodti3=__rust__umodti3 rust/core.o""" + expected = "/usr/lib/rust-1.84/lib/rustlib/src/rust/library/core/src/lib.rs rust/core.o" + self._assert_parsing(cmd, expected) + + # rustdoc command tests + def test_rustdoc(self): + cmd = """OBJTREE=/workspace/linux/kernel_build rustdoc --test --edition=2021 -Zbinary_dep_depinfo=y -Astable_features -Dnon_ascii_idents -Dunsafe_op_in_unsafe_fn -Wmissing_docs -Wrust_2018_idioms -Wunreachable_pub -Wclippy::all -Wclippy::as_ptr_cast_mut -Wclippy::as_underscore -Wclippy::cast_lossless -Wclippy::ignored_unit_patterns -Wclippy::mut_mut -Wclippy::needless_bitwise_bool -Aclippy::needless_lifetimes -Wclippy::no_mangle_with_rust_abi -Wclippy::ptr_as_ptr -Wclippy::ptr_cast_constness -Wclippy::ref_as_ptr -Wclippy::undocumented_unsafe_blocks -Wclippy::unnecessary_safety_comment -Wclippy::unnecessary_safety_doc -Wrustdoc::missing_crate_level_docs -Wrustdoc::unescaped_backticks -Cpanic=abort -Cembed-bitcode=n -Clto=n -Cforce-unwind-tables=n -Ccodegen-units=1 -Csymbol-mangling-version=v0 -Crelocation-model=static -Zfunction-sections=n -Wclippy::float_arithmetic --target=aarch64-unknown-none -Ctarget-feature="-neon" -Cforce-unwind-tables=n -Zbranch-protection=pac-ret -Copt-level=2 -Cdebug-assertions=y -Coverflow-checks=y -Dwarnings -Cforce-frame-pointers=y -Zsanitizer=kernel-address -Zsanitizer-recover=kernel-address -Cllvm-args=-asan-mapping-offset=0xdfff800000000000 -Cpasses=sancov-module -Cllvm-args=-sanitizer-coverage-level=3 -Cllvm-args=-sanitizer-coverage-trace-pc -Cllvm-args=-sanitizer-coverage-trace-compares @./include/generated/rustc_cfg -L./rust --extern ffi --extern pin_init --extern kernel --extern build_error --extern macros --extern bindings --extern uapi --no-run --crate-name kernel -Zunstable-options --sysroot=/dev/null --test-builder ./scripts/rustdoc_test_builder ../rust/kernel/lib.rs >/dev/null""" + expected = "../rust/kernel/lib.rs" + self._assert_parsing(cmd, expected) + + def test_rustdoc_test_gen(self): + cmd = "./scripts/rustdoc_test_gen" + expected = "" + self._assert_parsing(cmd, expected) + + # flex command tests + def test_flex(self): + cmd = "flex -oscripts/kconfig/lexer.lex.c -L ../scripts/kconfig/lexer.l" + expected = "../scripts/kconfig/lexer.l" + self._assert_parsing(cmd, expected) + + # bison command tests + def test_bison(self): + cmd = "bison -o scripts/kconfig/parser.tab.c --defines=scripts/kconfig/parser.tab.h -t -l ../scripts/kconfig/parser.y" + expected = "../scripts/kconfig/parser.y" + self._assert_parsing(cmd, expected) + + # bindgen command tests + def test_bindgen(self): + cmd = ( + "bindgen ../rust/bindings/bindings_helper.h " + "--blocklist-type __kernel_s?size_t --blocklist-type __kernel_ptrdiff_t " + "--opaque-type xregs_state --opaque-type desc_struct --no-doc-comments " + "--rust-target 1.68 --use-core --with-derive-default -o rust/bindings/bindings_generated.rs " + "-- -Wp,-MMD,rust/bindings/.bindings_generated.rs.d -nostdinc -I../arch/x86/include " + "-include ../include/linux/compiler-version.h -D__KERNEL__ -fintegrated-as -fno-builtin -DMODULE; " + "sed -Ei 's/pub const RUST_CONST_HELPER_([a-zA-Z0-9_]*)/pub const \\1/g' rust/bindings/bindings_generated.rs" + ) + expected = "../rust/bindings/bindings_helper.h ../include/linux/compiler-version.h" + self._assert_parsing(cmd, expected) + + # perl command tests + def test_perl(self): + cmd = "perl ../lib/crypto/x86/poly1305-x86_64-cryptogams.pl > lib/crypto/x86/poly1305-x86_64-cryptogams.S" + expected = "../lib/crypto/x86/poly1305-x86_64-cryptogams.pl" + self._assert_parsing(cmd, expected) + + # link-vmlinux.sh command tests + def test_link_vmlinux(self): + cmd = '../scripts/link-vmlinux.sh "ld" "-m elf_x86_64 -z noexecstack" "-z max-page-size=0x200000 --build-id=sha1 --orphan-handling=error --emit-relocs --discard-none" "vmlinux.unstripped"; true' + expected = "vmlinux.a" + self._assert_parsing(cmd, expected) + + def test_link_vmlinux_postlink(self): + cmd = '../scripts/link-vmlinux.sh "ld" "-m elf_x86_64 -z noexecstack --no-warn-rwx-segments" "--emit-relocs --discard-none -z max-page-size=0x200000 --build-id=sha1 -X --orphan-handling=error"; make -f ../arch/x86/Makefile.postlink vmlinux' + expected = "vmlinux.a" + self._assert_parsing(cmd, expected) + + # syscallhdr.sh command tests + def test_syscallhdr(self): + cmd = "sh ../scripts/syscallhdr.sh --abis common,64 --emit-nr ../arch/x86/entry/syscalls/syscall_64.tbl arch/x86/include/generated/uapi/asm/unistd_64.h" + expected = "../arch/x86/entry/syscalls/syscall_64.tbl" + self._assert_parsing(cmd, expected) + + # syscalltbl.sh command tests + def test_syscalltbl(self): + cmd = "sh ../scripts/syscalltbl.sh --abis common,64 ../arch/x86/entry/syscalls/syscall_64.tbl arch/x86/include/generated/asm/syscalls_64.h" + expected = "../arch/x86/entry/syscalls/syscall_64.tbl" + self._assert_parsing(cmd, expected) + + # mkcapflags.sh command tests + def test_mkcapflags(self): + cmd = "sh ../arch/x86/kernel/cpu/mkcapflags.sh arch/x86/kernel/cpu/capflags.c ../arch/x86/kernel/cpu/../../include/asm/cpufeatures.h ../arch/x86/kernel/cpu/../../include/asm/vmxfeatures.h ../arch/x86/kernel/cpu/mkcapflags.sh FORCE" + expected = "../arch/x86/kernel/cpu/../../include/asm/cpufeatures.h ../arch/x86/kernel/cpu/../../include/asm/vmxfeatures.h" + self._assert_parsing(cmd, expected) + + # orc_hash.sh command tests + def test_orc_hash(self): + cmd = "mkdir -p arch/x86/include/generated/asm/; sh ../scripts/orc_hash.sh < ../arch/x86/include/asm/orc_types.h > arch/x86/include/generated/asm/orc_hash.h" + expected = "../arch/x86/include/asm/orc_types.h" + self._assert_parsing(cmd, expected) + + # xen-hypercalls.sh command tests + def test_xen_hypercalls(self): + cmd = "sh '../scripts/xen-hypercalls.sh' arch/x86/include/generated/asm/xen-hypercalls.h ../include/xen/interface/xen-mca.h ../include/xen/interface/xen.h ../include/xen/interface/xenpmu.h" + expected = "../include/xen/interface/xen-mca.h ../include/xen/interface/xen.h ../include/xen/interface/xenpmu.h" + self._assert_parsing(cmd, expected) + + # gen_initramfs.sh command tests + def test_gen_initramfs(self): + cmd = "sh ../usr/gen_initramfs.sh -o usr/initramfs_data.cpio -l usr/.initramfs_data.cpio.d ../usr/default_cpio_list" + expected = "../usr/default_cpio_list" + self._assert_parsing(cmd, expected) + + # mkuboot.sh command tests + def test_mkuboot(self): + cmd = "bash ../scripts/mkuboot.sh -A arm -O linux -C none -T kernel -a 0x8000 -e 0x8000 -n 'Linux-6.15.0' -d arch/arm/boot/zImage arch/arm/boot/uImage" + expected = "arch/arm/boot/zImage" + self._assert_parsing(cmd, expected) + + # syscallnr.sh command tests + def test_syscallnr(self): + cmd = "sh ../arch/arm/tools/syscallnr.sh ../arch/arm/tools/syscall.tbl arch/arm/include/generated/asm/unistd-nr.h" + expected = "../arch/arm/tools/syscall.tbl" + self._assert_parsing(cmd, expected) + + # gen-kernel-hwcaps.sh command tests + def test_gen_kernel_hwcaps(self): + cmd = "/bin/sh -e ../arch/arm64/tools/gen-kernel-hwcaps.sh ../arch/arm64/include/uapi/asm/hwcap.h > arch/arm64/include/generated/asm/kernel-hwcap.h" + expected = "../arch/arm64/include/uapi/asm/hwcap.h" + self._assert_parsing(cmd, expected) + + # vdso2c command tests + def test_vdso2c(self): + cmd = "arch/x86/entry/vdso/vdso2c arch/x86/entry/vdso/vdso64.so.dbg arch/x86/entry/vdso/vdso64.so arch/x86/entry/vdso/vdso-image-64.c" + expected = "arch/x86/entry/vdso/vdso64.so.dbg arch/x86/entry/vdso/vdso64.so" + self._assert_parsing(cmd, expected) + + # vdsomunge command tests + def test_vdsomunge(self): + cmd = "arch/arm64/kernel/vdso32/../../../arm/vdso/vdsomunge arch/arm64/kernel/vdso32/vdso.so.raw arch/arm64/kernel/vdso32/vdso32.so.dbg" + expected = "arch/arm64/kernel/vdso32/vdso.so.raw" + self._assert_parsing(cmd, expected) + + # mkpiggy command tests + def test_mkpiggy(self): + cmd = "arch/x86/boot/compressed/mkpiggy arch/x86/boot/compressed/vmlinux.bin.gz > arch/x86/boot/compressed/piggy.S" + expected = "arch/x86/boot/compressed/vmlinux.bin.gz" + self._assert_parsing(cmd, expected) + + # relocs command tests + def test_relocs(self): + cmd = "arch/x86/tools/relocs vmlinux.unstripped > arch/x86/boot/compressed/vmlinux.relocs;arch/x86/tools/relocs --abs-relocs vmlinux.unstripped" + expected = "vmlinux.unstripped" + self._assert_parsing(cmd, expected) + + def test_relocs_with_realmode(self): + cmd = ( + "arch/x86/tools/relocs --realmode arch/x86/realmode/rm/realmode.elf > arch/x86/realmode/rm/realmode.relocs" + ) + expected = "arch/x86/realmode/rm/realmode.elf" + self._assert_parsing(cmd, expected) + + # mk_elfconfig command tests + def test_mk_elfconfig(self): + cmd = "scripts/mod/mk_elfconfig < scripts/mod/empty.o > scripts/mod/elfconfig.h" + expected = "scripts/mod/empty.o" + self._assert_parsing(cmd, expected) + + # tools/build command tests + def test_build(self): + cmd = "arch/x86/boot/tools/build arch/x86/boot/setup.bin arch/x86/boot/vmlinux.bin arch/x86/boot/zoffset.h arch/x86/boot/bzImage" + expected = "arch/x86/boot/setup.bin arch/x86/boot/vmlinux.bin arch/x86/boot/zoffset.h" + self._assert_parsing(cmd, expected) + + # extract-cert command tests + def test_extract_cert(self): + cmd = 'certs/extract-cert "" certs/signing_key.x509' + expected = "" + self._assert_parsing(cmd, expected) + + # dtc command tests + def test_dtc_cat(self): + cmd = "./scripts/dtc/dtc -o drivers/of/empty_root.dtb -b 0 -i../drivers/of/ -i../scripts/dtc/include-prefixes -Wno-unique_unit_address -Wno-unit_address_vs_reg -Wno-avoid_unnecessary_addr_size -Wno-alias_paths -Wno-graph_child_address -Wno-simple_bus_reg -d drivers/of/.empty_root.dtb.d.dtc.tmp drivers/of/.empty_root.dtb.dts.tmp ; cat drivers/of/.empty_root.dtb.d.pre.tmp drivers/of/.empty_root.dtb.d.dtc.tmp > drivers/of/.empty_root.dtb.d" + expected = "drivers/of/.empty_root.dtb.dts.tmp drivers/of/.empty_root.dtb.d.pre.tmp drivers/of/.empty_root.dtb.d.dtc.tmp" + self._assert_parsing(cmd, expected) + + # pnmtologo command tests + def test_pnmtologo(self): + cmd = "drivers/video/logo/pnmtologo -t clut224 -n logo_linux_clut224 -o drivers/video/logo/logo_linux_clut224.c ../drivers/video/logo/logo_linux_clut224.ppm" + expected = "../drivers/video/logo/logo_linux_clut224.ppm" + self._assert_parsing(cmd, expected) + + # relacheck command tests + def test_relacheck(self): + cmd = "arch/arm64/kernel/pi/relacheck arch/arm64/kernel/pi/idreg-override.pi.o arch/arm64/kernel/pi/idreg-override.o" + expected = "arch/arm64/kernel/pi/idreg-override.pi.o" + self._assert_parsing(cmd, expected) + + # gen-hyprel command tests + def test_gen_hyprel(self): + cmd = "arch/arm64/kvm/hyp/nvhe/gen-hyprel arch/arm64/kvm/hyp/nvhe/kvm_nvhe.tmp.o > arch/arm64/kvm/hyp/nvhe/hyp-reloc.S" + expected = "arch/arm64/kvm/hyp/nvhe/kvm_nvhe.tmp.o" + self._assert_parsing(cmd, expected) + + # mkregtable command tests + def test_mkregtable(self): + cmd = "drivers/gpu/drm/radeon/mkregtable ../drivers/gpu/drm/radeon/reg_srcs/r100 > drivers/gpu/drm/radeon/r100_reg_safe.h" + expected = "../drivers/gpu/drm/radeon/reg_srcs/r100" + self._assert_parsing(cmd, expected) + + # genheaders command tests + def test_genheaders(self): + cmd = "security/selinux/genheaders security/selinux/flask.h security/selinux/av_permissions.h" + expected = "" + self._assert_parsing(cmd, expected) + + # mkcpustr command tests + def test_mkcpustr(self): + cmd = "arch/x86/boot/mkcpustr > arch/x86/boot/cpustr.h" + expected = "" + self._assert_parsing(cmd, expected) + + # polgen command tests + def test_polgen(self): + cmd = "scripts/ipe/polgen/polgen security/ipe/boot_policy.c" + expected = "" + self._assert_parsing(cmd, expected) + + # gen_header.py command tests + def test_gen_header(self): + cmd = "mkdir -p drivers/gpu/drm/msm/generated && python3 ../drivers/gpu/drm/msm/registers/gen_header.py --no-validate --rnn ../drivers/gpu/drm/msm/registers --xml ../drivers/gpu/drm/msm/registers/adreno/a2xx.xml c-defines > drivers/gpu/drm/msm/generated/a2xx.xml.h" + expected = "../drivers/gpu/drm/msm/registers/adreno/a2xx.xml" + self._assert_parsing(cmd, expected) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From 880bae5f1269b4d81bb2a254963e84377cd37bc1 Mon Sep 17 00:00:00 2001 From: Luis Augenstein Date: Mon, 18 May 2026 08:21:02 +0200 Subject: scripts/sbom: add unit tests for SPDX-License-Identifier parsing Verify that SPDX-License-Identifier headers at the top of source files are parsed correctly. Assisted-by: Cursor:claude-sonnet-4-5 Assisted-by: OpenCode:GLM-4-7 Co-developed-by: Maximilian Huber Signed-off-by: Maximilian Huber Signed-off-by: Luis Augenstein Signed-off-by: Greg Kroah-Hartman --- scripts/sbom/tests/spdx_graph/__init__.py | 0 scripts/sbom/tests/spdx_graph/test_kernel_file.py | 35 +++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 scripts/sbom/tests/spdx_graph/__init__.py create mode 100644 scripts/sbom/tests/spdx_graph/test_kernel_file.py (limited to 'scripts') diff --git a/scripts/sbom/tests/spdx_graph/__init__.py b/scripts/sbom/tests/spdx_graph/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/scripts/sbom/tests/spdx_graph/test_kernel_file.py b/scripts/sbom/tests/spdx_graph/test_kernel_file.py new file mode 100644 index 000000000000..35a63a768ba2 --- /dev/null +++ b/scripts/sbom/tests/spdx_graph/test_kernel_file.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: GPL-2.0-only OR MIT +# Copyright (C) 2025 TNG Technology Consulting GmbH + +import unittest +from pathlib import Path +import tempfile +from sbom.spdx_graph.kernel_file import _parse_spdx_license_identifier # type: ignore + + +class TestKernelFile(unittest.TestCase): + def setUp(self): + self.tmpdir = tempfile.TemporaryDirectory() + self.src_tree = Path(self.tmpdir.name) + + def tearDown(self): + self.tmpdir.cleanup() + + def test_parse_spdx_license_identifier(self): + # REUSE-IgnoreStart + test_cases: list[tuple[str, str | None]] = [ + ("/* SPDX-License-Identifier: MIT*/", "MIT"), + ("// SPDX-License-Identifier: GPL-2.0-only", "GPL-2.0-only"), + ("# SPDX-License-Identifier: GPL-2.0-only", "GPL-2.0-only"), + ("#!/bin/bash\n# SPDX-License-Identifier: GPL-2.0-only", "GPL-2.0-only"), + ("/* SPDX-License-Identifier: GPL-2.0-or-later OR MIT */", "GPL-2.0-or-later OR MIT"), + ("/* SPDX-License-Identifier: Apache-2.0 */\n extra text", "Apache-2.0"), + ("", "GPL-2.0"), + ("int main() { return 0; }", None), + ] + # REUSE-IgnoreEnd + + for i, (file_content, expected_identifier) in enumerate(test_cases): + file_path = self.src_tree / f"file_{i}.c" + file_path.write_text(file_content) + self.assertEqual(_parse_spdx_license_identifier(str(file_path)), expected_identifier) -- cgit v1.2.3 From 012c889690edc14d724a5880b4d0fe01c1fbb488 Mon Sep 17 00:00:00 2001 From: "Paul E. McKenney" Date: Fri, 8 May 2026 10:43:50 -0700 Subject: checkpatch: Undeprecate rcu_read_lock_trace() and rcu_read_unlock_trace() It turns out that there are BPF use cases that rely on nesting RCU Tasks Trace readers. These use cases are well-served by the old rcu_read_lock_trace() and rcu_read_unlock_trace() functions that maintain a nesting counter in the task_struct structure. But these use cases incur a performance penalty when using the shiny new rcu_read_lock_tasks_trace() and rcu_read_unlock_tasks_trace() functions, which nest in the same way that SRCU does. This means that rcu_read_lock_trace() and rcu_read_unlock_trace() will be with us for some time. Therefore, remove the checkpatch.pl deprecation. Also, the rcu_read_lock_tasks_trace() and rcu_read_unlock_tasks_trace() functions are intended for use only by BPF. Therefore, add them to the list of functions that checkpatch complains about outside of BPF (and of course, RCU). Reported-by: Puranjay Mohan Signed-off-by: Paul E. McKenney Cc: Andy Whitcroft Cc: Joe Perches Cc: Dwaipayan Ray Cc: Lukas Bulwahn Signed-off-by: Uladzislau Rezki (Sony) --- scripts/checkpatch.pl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/checkpatch.pl b/scripts/checkpatch.pl index 0492d6afc9a1..cc5bbd70cb84 100755 --- a/scripts/checkpatch.pl +++ b/scripts/checkpatch.pl @@ -865,8 +865,6 @@ our %deprecated_apis = ( "DEFINE_IDR" => "DEFINE_XARRAY", "idr_init" => "xa_init", "idr_init_base" => "xa_init_flags", - "rcu_read_lock_trace" => "rcu_read_lock_tasks_trace", - "rcu_read_unlock_trace" => "rcu_read_unlock_tasks_trace", ); #Create a search pattern for all these strings to speed up a loop below @@ -7596,12 +7594,15 @@ sub process { # Complain about RCU Tasks Trace used outside of BPF (and of course, RCU). our $rcu_trace_funcs = qr{(?x: + rcu_read_lock_tasks_trace | rcu_read_lock_trace | rcu_read_lock_trace_held | rcu_read_unlock_trace | + rcu_read_unlock_tasks_trace | call_rcu_tasks_trace | synchronize_rcu_tasks_trace | rcu_barrier_tasks_trace | + rcu_tasks_trace_expedite_current | rcu_request_urgent_qs_task )}; our $rcu_trace_paths = qr{(?x: -- cgit v1.2.3 From 4a44b17406cb5a93f90af3df9392b3a45eb336fb Mon Sep 17 00:00:00 2001 From: Alice Ryhl Date: Thu, 7 May 2026 11:14:42 +0000 Subject: rust: kasan/kbuild: fix rustc-option when cross-compiling The Makefile version of rustc-option currently checks whether the option exists for the host target instead of the target actually being compiled for. It was done this way in commit 46e24a545cdb ("rust: kasan/kbuild: fix missing flags on first build") to avoid a circular dependency on target.json. However, because of this, rustc-option currently does not function when cross-compiling from x86_64 to aarch64 if CONFIG_SHADOW_CALL_STACK is enabled. This is because KBUILD_RUSTFLAGS contains -Zfixed-x18 under this configuration. Since that flag does not exist on the host target, rustc-option runs into a compilation failure every time, leading to all flags being rejected as unsupported. To fix this, update rustc-option to pass a --target parameter so that the host target is not used. For targets using target.json, use a built-in target that is as close as possible to the target created with target.json to avoid the circular dependency on target.json. One scenario where this causes a boot failure: * Cross-compiled from x86_64 to aarch64. * With CONFIG_SHADOW_CALL_STACK=y * With CONFIG_KASAN_SW_TAGS=y * With CONFIG_KASAN_INLINE=n Then the resulting kernel image will fail to boot when it first calls into Rust code with a crash along the lines of "Unable to handle kernel paging request at virtual address 0ffffffc08541796". This is because the call threshold is not specified, so rustc will inline kasan operations, but the kasan shadow offset is not specified, which leads to the inlined kasan instructions being incorrect. Note that the -Zsanitizer=kernel-hwaddress parameter itself does not lead to a rustc-option failure despite being aarch64-specific because RUSTFLAGS_KASAN has not yet been added to KBUILD_RUSTFLAGS when rustc-option is evaluated by the kasan Makefile. Cc: stable@vger.kernel.org Fixes: 46e24a545cdb ("rust: kasan/kbuild: fix missing flags on first build") Signed-off-by: Alice Ryhl Link: https://patch.msgid.link/20260507-rustc-option-cross-v2-1-2f650a49c2b5@google.com [ Edited slightly: - Reset variable to avoid using the environment. - Use a simply expanded variable flavor for simplicity. - Export variable so that behavior in sub-`make`s is consistent. This matches other variables. - Miguel ] Signed-off-by: Miguel Ojeda --- Makefile | 3 ++- arch/x86/Makefile | 4 ++++ arch/x86/Makefile.um | 8 ++++++++ scripts/Makefile.compiler | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/Makefile b/Makefile index b7b80e84e1eb..1f205a004437 100644 --- a/Makefile +++ b/Makefile @@ -607,6 +607,7 @@ KBUILD_RUSTFLAGS := $(rust_common_flags) \ -Crelocation-model=static \ -Zfunction-sections=n \ -Wclippy::float_arithmetic +KBUILD_RUSTFLAGS_OPTION_CHKS := KBUILD_AFLAGS_KERNEL := KBUILD_CFLAGS_KERNEL := @@ -643,7 +644,7 @@ export KBUILD_USERCFLAGS KBUILD_USERLDFLAGS export KBUILD_CPPFLAGS NOSTDINC_FLAGS LINUXINCLUDE OBJCOPYFLAGS KBUILD_LDFLAGS export KBUILD_CFLAGS CFLAGS_KERNEL CFLAGS_MODULE -export KBUILD_RUSTFLAGS RUSTFLAGS_KERNEL RUSTFLAGS_MODULE +export KBUILD_RUSTFLAGS RUSTFLAGS_KERNEL RUSTFLAGS_MODULE KBUILD_RUSTFLAGS_OPTION_CHKS export KBUILD_AFLAGS AFLAGS_KERNEL AFLAGS_MODULE export KBUILD_AFLAGS_MODULE KBUILD_CFLAGS_MODULE KBUILD_RUSTFLAGS_MODULE KBUILD_LDFLAGS_MODULE export KBUILD_AFLAGS_KERNEL KBUILD_CFLAGS_KERNEL KBUILD_RUSTFLAGS_KERNEL diff --git a/arch/x86/Makefile b/arch/x86/Makefile index 46fec0b08487..1d526a5d2a83 100644 --- a/arch/x86/Makefile +++ b/arch/x86/Makefile @@ -77,6 +77,10 @@ KBUILD_CFLAGS += -mno-sse -mno-mmx -mno-sse2 -mno-3dnow -mno-avx -mno-sse4a KBUILD_RUSTFLAGS += --target=$(objtree)/scripts/target.json KBUILD_RUSTFLAGS += -Ctarget-feature=-sse,-sse2,-sse3,-ssse3,-sse4.1,-sse4.2,-avx,-avx2 +# The target.json file is not available when invoking rustc-option, so use the +# built-in target when checking whether flags are supported instead. +KBUILD_RUSTFLAGS_OPTION_CHKS += --target=x86_64-unknown-none + # # CFLAGS for compiling floating point code inside the kernel. # diff --git a/arch/x86/Makefile.um b/arch/x86/Makefile.um index 19c13afa474e..9adecd65639f 100644 --- a/arch/x86/Makefile.um +++ b/arch/x86/Makefile.um @@ -14,6 +14,14 @@ endif KBUILD_RUSTFLAGS += --target=$(objtree)/scripts/target.json +# The target.json file is not available when invoking rustc-option, so use the +# built-in target when checking whether flags are supported instead. +ifeq ($(CONFIG_X86_32),y) +KBUILD_RUSTFLAGS_OPTION_CHKS += --target=i686-unknown-linux-gnu +else +KBUILD_RUSTFLAGS_OPTION_CHKS += --target=x86_64-unknown-linux-gnu +endif + ifeq ($(CONFIG_X86_32),y) START := 0x8048000 diff --git a/scripts/Makefile.compiler b/scripts/Makefile.compiler index ef91910de265..06bbe29c846c 100644 --- a/scripts/Makefile.compiler +++ b/scripts/Makefile.compiler @@ -80,7 +80,7 @@ ld-option = $(call try-run, $(LD) $(KBUILD_LDFLAGS) $(1) -v,$(1),$(2),$(3)) # TODO: remove RUSTC_BOOTSTRAP=1 when we raise the minimum GNU Make version to 4.4 __rustc-option = $(call try-run,\ echo '$(pound)![allow(missing_docs)]$(pound)![feature(no_core)]$(pound)![no_core]' | RUSTC_BOOTSTRAP=1\ - $(1) --sysroot=/dev/null $(filter-out --sysroot=/dev/null --target=%,$(2)) $(3)\ + $(1) --sysroot=/dev/null $(KBUILD_RUSTFLAGS_OPTION_CHKS) $(filter-out --sysroot=/dev/null --target=%target.json,$(2)) $(3)\ --crate-type=rlib --out-dir=$(TMPOUT) --emit=obj=- - >/dev/null,$(3),$(4)) # rustc-option -- cgit v1.2.3 From cca5e6fa791bdf84f716ae92604d8330a6b6411a Mon Sep 17 00:00:00 2001 From: Thomas Gleixner Date: Sun, 17 May 2026 22:02:04 +0200 Subject: scripts/gdb: Update x86 interrupts to the array based storage x86 changed the interrupt statistics from a struct with individual members to an counter array. It also provides a corresponding info array with the strings for prefix and description and an indicator to skip the entry. Update the already out of sync GDB script to use the counter and the info array, which keeps the GDB script in sync automatically. Signed-off-by: Thomas Gleixner Tested-by: Florian Fainelli Reviewed-by: Florian Fainelli Link: https://patch.msgid.link/20260517194931.442613033@kernel.org --- scripts/gdb/linux/interrupts.py | 73 +++++++++++------------------------------ 1 file changed, 19 insertions(+), 54 deletions(-) (limited to 'scripts') diff --git a/scripts/gdb/linux/interrupts.py b/scripts/gdb/linux/interrupts.py index f4f715a8f0e3..b794e013c360 100644 --- a/scripts/gdb/linux/interrupts.py +++ b/scripts/gdb/linux/interrupts.py @@ -48,7 +48,7 @@ def show_irq_desc(prec, irq): count = cpus.per_cpu(desc['kstat_irqs'], cpu)['cnt'] else: count = 0 - text += "%10u" % (count) + text += "%10u " % (count) name = "None" if desc['irq_data']['chip']: @@ -58,7 +58,7 @@ def show_irq_desc(prec, irq): else: name = "-" - text += " %8s" % (name) + text += " %-8s" % (name) if desc['irq_data']['domain']: text += " %*lu" % (prec, desc['irq_data']['hwirq']) @@ -97,64 +97,29 @@ def show_irq_err_count(prec): text += "%*s: %10u\n" % (prec, "ERR", cnt['counter']) return text -def x86_show_irqstat(prec, pfx, field, desc): - irq_stat = gdb.parse_and_eval("&irq_stat") +def x86_show_irqstat(prec, pfx, idx, desc): + irq_stat = gdb.parse_and_eval("&irq_stat.counts[%d]" %idx) text = "%*s: " % (prec, pfx) for cpu in cpus.each_online_cpu(): stat = cpus.per_cpu(irq_stat, cpu) - text += "%10u " % (stat[field]) - text += " %s\n" % (desc) - return text - -def x86_show_mce(prec, var, pfx, desc): - pvar = gdb.parse_and_eval(var) - text = "%*s: " % (prec, pfx) - for cpu in cpus.each_online_cpu(): - text += "%10u " % (cpus.per_cpu(pvar, cpu).dereference()) - text += " %s\n" % (desc) + text += "%10u " % (stat.dereference()) + text += desc return text def x86_show_interupts(prec): - text = x86_show_irqstat(prec, "NMI", '__nmi_count', 'Non-maskable interrupts') - - if constants.LX_CONFIG_X86_LOCAL_APIC: - text += x86_show_irqstat(prec, "LOC", 'apic_timer_irqs', "Local timer interrupts") - text += x86_show_irqstat(prec, "SPU", 'irq_spurious_count', "Spurious interrupts") - text += x86_show_irqstat(prec, "PMI", 'apic_perf_irqs', "Performance monitoring interrupts") - text += x86_show_irqstat(prec, "IWI", 'apic_irq_work_irqs', "IRQ work interrupts") - text += x86_show_irqstat(prec, "RTR", 'icr_read_retry_count', "APIC ICR read retries") - if utils.gdb_eval_or_none("x86_platform_ipi_callback") is not None: - text += x86_show_irqstat(prec, "PLT", 'x86_platform_ipis', "Platform interrupts") - - if constants.LX_CONFIG_SMP: - text += x86_show_irqstat(prec, "RES", 'irq_resched_count', "Rescheduling interrupts") - text += x86_show_irqstat(prec, "CAL", 'irq_call_count', "Function call interrupts") - text += x86_show_irqstat(prec, "TLB", 'irq_tlb_count', "TLB shootdowns") - - if constants.LX_CONFIG_X86_THERMAL_VECTOR: - text += x86_show_irqstat(prec, "TRM", 'irq_thermal_count', "Thermal events interrupts") - - if constants.LX_CONFIG_X86_MCE_THRESHOLD: - text += x86_show_irqstat(prec, "THR", 'irq_threshold_count', "Threshold APIC interrupts") - - if constants.LX_CONFIG_X86_MCE_AMD: - text += x86_show_irqstat(prec, "DFR", 'irq_deferred_error_count', "Deferred Error APIC interrupts") - - if constants.LX_CONFIG_X86_MCE: - text += x86_show_mce(prec, "&mce_exception_count", "MCE", "Machine check exceptions") - text += x86_show_mce(prec, "&mce_poll_count", "MCP", "Machine check polls") - - text += show_irq_err_count(prec) - - if constants.LX_CONFIG_X86_IO_APIC: - cnt = utils.gdb_eval_or_none("irq_mis_count") - if cnt is not None: - text += "%*s: %10u\n" % (prec, "MIS", cnt['counter']) - - if constants.LX_CONFIG_KVM: - text += x86_show_irqstat(prec, "PIN", 'kvm_posted_intr_ipis', 'Posted-interrupt notification event') - text += x86_show_irqstat(prec, "NPI", 'kvm_posted_intr_nested_ipis', 'Nested posted-interrupt event') - text += x86_show_irqstat(prec, "PIW", 'kvm_posted_intr_wakeup_ipis', 'Posted-interrupt wakeup event') + info_type = gdb.lookup_type('struct irq_stat_info') + info = gdb.parse_and_eval('irq_stat_info') + bitmap = gdb.parse_and_eval('irq_stat_count_show') + bitsperlong = 8 * int(bitmap.type.target().sizeof) + + text = "" + for idx in range(int(info.type.sizeof / info_type.sizeof)): + show = bitmap[int(idx / bitsperlong)] + if not show & 1 << int(idx % bitsperlong): + continue + pfx = info[idx]['symbol'].string() + desc = info[idx]['text'].string() + text += x86_show_irqstat(prec, pfx, idx, desc) return text -- cgit v1.2.3 From b99dc723b12ea587fc8b2e07bd8401433eec58d8 Mon Sep 17 00:00:00 2001 From: Thomas Gleixner Date: Sun, 17 May 2026 22:02:09 +0200 Subject: genirq: Expose nr_irqs in core code ... to avoid function calls in the core code to retrieve the maximum number of interrupts. Rename it to 'total_nr_irqs' as 'nr_irqs' is too generic and fix up the 'nr_irqs' reference in the related GDB script as well. Signed-off-by: Thomas Gleixner Tested-by: Michael Kelley Reviewed-by: Dmitry Ilvokhin Reviewed-by: Radu Rendec Reviewed-by: Shrikanth Hegde Link: https://patch.msgid.link/20260517194931.522168332@kernel.org --- kernel/irq/internals.h | 1 + kernel/irq/irqdesc.c | 28 ++++++++++++++-------------- kernel/irq/proc.c | 4 ++-- scripts/gdb/linux/interrupts.py | 2 +- 4 files changed, 18 insertions(+), 17 deletions(-) (limited to 'scripts') diff --git a/kernel/irq/internals.h b/kernel/irq/internals.h index 9412e57056f5..db359acdf27f 100644 --- a/kernel/irq/internals.h +++ b/kernel/irq/internals.h @@ -21,6 +21,7 @@ extern bool noirqdebug; extern int irq_poll_cpu; +extern unsigned int total_nr_irqs; extern struct irqaction chained_action; diff --git a/kernel/irq/irqdesc.c b/kernel/irq/irqdesc.c index 7173b8b634f2..a7ce1e8165a6 100644 --- a/kernel/irq/irqdesc.c +++ b/kernel/irq/irqdesc.c @@ -140,14 +140,14 @@ static void desc_set_defaults(unsigned int irq, struct irq_desc *desc, int node, desc_smp_init(desc, node, affinity); } -static unsigned int nr_irqs = NR_IRQS; +unsigned int total_nr_irqs __read_mostly = NR_IRQS; /** * irq_get_nr_irqs() - Number of interrupts supported by the system. */ unsigned int irq_get_nr_irqs(void) { - return nr_irqs; + return total_nr_irqs; } EXPORT_SYMBOL_GPL(irq_get_nr_irqs); @@ -159,7 +159,7 @@ EXPORT_SYMBOL_GPL(irq_get_nr_irqs); */ unsigned int irq_set_nr_irqs(unsigned int nr) { - nr_irqs = nr; + total_nr_irqs = nr; return nr; } @@ -187,9 +187,9 @@ static unsigned int irq_find_at_or_after(unsigned int offset) struct irq_desc *desc; guard(rcu)(); - desc = mt_find(&sparse_irqs, &index, nr_irqs); + desc = mt_find(&sparse_irqs, &index, total_nr_irqs); - return desc ? irq_desc_get_irq(desc) : nr_irqs; + return desc ? irq_desc_get_irq(desc) : total_nr_irqs; } static void irq_insert_desc(unsigned int irq, struct irq_desc *desc) @@ -543,7 +543,7 @@ static bool irq_expand_nr_irqs(unsigned int nr) { if (nr > MAX_SPARSE_IRQS) return false; - nr_irqs = nr; + total_nr_irqs = nr; return true; } @@ -557,16 +557,16 @@ int __init early_irq_init(void) /* Let arch update nr_irqs and return the nr of preallocated irqs */ initcnt = arch_probe_nr_irqs(); printk(KERN_INFO "NR_IRQS: %d, nr_irqs: %d, preallocated irqs: %d\n", - NR_IRQS, nr_irqs, initcnt); + NR_IRQS, total_nr_irqs, initcnt); - if (WARN_ON(nr_irqs > MAX_SPARSE_IRQS)) - nr_irqs = MAX_SPARSE_IRQS; + if (WARN_ON(total_nr_irqs > MAX_SPARSE_IRQS)) + total_nr_irqs = MAX_SPARSE_IRQS; if (WARN_ON(initcnt > MAX_SPARSE_IRQS)) initcnt = MAX_SPARSE_IRQS; - if (initcnt > nr_irqs) - nr_irqs = initcnt; + if (initcnt > total_nr_irqs) + total_nr_irqs = initcnt; for (i = 0; i < initcnt; i++) { desc = alloc_desc(i, node, 0, NULL, NULL); @@ -862,7 +862,7 @@ void irq_free_descs(unsigned int from, unsigned int cnt) { int i; - if (from >= nr_irqs || (from + cnt) > nr_irqs) + if (from >= total_nr_irqs || (from + cnt) > total_nr_irqs) return; guard(mutex)(&sparse_irq_lock); @@ -911,7 +911,7 @@ int __ref __irq_alloc_descs(int irq, unsigned int from, unsigned int cnt, int no if (irq >=0 && start != irq) return -EEXIST; - if (start + cnt > nr_irqs) { + if (start + cnt > total_nr_irqs) { if (!irq_expand_nr_irqs(start + cnt)) return -ENOMEM; } @@ -923,7 +923,7 @@ EXPORT_SYMBOL_GPL(__irq_alloc_descs); * irq_get_next_irq - get next allocated irq number * @offset: where to start the search * - * Returns next irq number after offset or nr_irqs if none is found. + * Returns next irq number after offset or total_nr_irqs if none is found. */ unsigned int irq_get_next_irq(unsigned int offset) { diff --git a/kernel/irq/proc.c b/kernel/irq/proc.c index 5a1805b0b8a9..d5b812680f5c 100644 --- a/kernel/irq/proc.c +++ b/kernel/irq/proc.c @@ -448,7 +448,7 @@ int __weak arch_show_interrupts(struct seq_file *p, int prec) } #ifndef ACTUAL_NR_IRQS -# define ACTUAL_NR_IRQS irq_get_nr_irqs() +# define ACTUAL_NR_IRQS total_nr_irqs #endif /* Same as seq_put_decimal_ull_width(p, " ", cnt, 10) */ @@ -490,7 +490,7 @@ void irq_proc_emit_counts(struct seq_file *p, unsigned int __percpu *cnts) int show_interrupts(struct seq_file *p, void *v) { - const unsigned int nr_irqs = irq_get_nr_irqs(); + const unsigned int nr_irqs = total_nr_irqs; static int prec; int i = *(loff_t *) v, j; diff --git a/scripts/gdb/linux/interrupts.py b/scripts/gdb/linux/interrupts.py index b794e013c360..a4e25d2bf123 100644 --- a/scripts/gdb/linux/interrupts.py +++ b/scripts/gdb/linux/interrupts.py @@ -174,7 +174,7 @@ class LxInterruptList(gdb.Command): super(LxInterruptList, self).__init__("lx-interruptlist", gdb.COMMAND_DATA) def invoke(self, arg, from_tty): - nr_irqs = gdb.parse_and_eval("nr_irqs") + nr_irqs = gdb.parse_and_eval("total_nr_irqs") prec = 3 j = 1000 while prec < 10 and j <= nr_irqs: -- cgit v1.2.3 From 34594da7650d3ea67f96c0f4034ff6b2453f67c3 Mon Sep 17 00:00:00 2001 From: Thomas Gleixner Date: Sun, 17 May 2026 22:02:29 +0200 Subject: genirq/proc: Increase default interrupt number precision to four Quite some architectures have four character wide acronyms for architecture specific interrupts like IPI, NMI, etc. The default precision of printing the Linux device interrupt numbers is three, which causes quite some code to play games with adding or omitting space after the acronym and the colon in order to keep the per CPU numbers properly aligned. Increase the default number precision to four in the core code and get rid of the space games all over the place. At the same time align all architecture specific descriptor texts left so that they show up in the same column as the interrupt chip names, which makes the output more uniform accross architectures. Fix up the GDB script to this new scheme as well. Signed-off-by: Thomas Gleixner Link: https://patch.msgid.link/20260517194931.839482411@kernel.org --- arch/alpha/kernel/irq.c | 8 ++++---- arch/arm/kernel/smp.c | 3 +-- arch/arm64/kernel/smp.c | 5 ++--- arch/loongarch/kernel/smp.c | 2 +- arch/riscv/kernel/smp.c | 3 +-- arch/sh/kernel/irq.c | 2 +- arch/sparc/kernel/irq_32.c | 12 ++++++------ arch/sparc/kernel/irq_64.c | 4 ++-- arch/um/kernel/irq.c | 4 ++-- arch/xtensa/kernel/irq.c | 2 +- kernel/irq/proc.c | 4 ++-- scripts/gdb/linux/interrupts.py | 16 ++++++---------- 12 files changed, 29 insertions(+), 36 deletions(-) (limited to 'scripts') diff --git a/arch/alpha/kernel/irq.c b/arch/alpha/kernel/irq.c index c67047c5d830..4a6a8b1d5a8b 100644 --- a/arch/alpha/kernel/irq.c +++ b/arch/alpha/kernel/irq.c @@ -72,16 +72,16 @@ int arch_show_interrupts(struct seq_file *p, int prec) int j; #ifdef CONFIG_SMP - seq_puts(p, "IPI: "); + seq_puts(p, " IPI: "); for_each_online_cpu(j) seq_printf(p, "%10lu ", cpu_data[j].ipi_count); seq_putc(p, '\n'); #endif - seq_puts(p, "PMI: "); + seq_puts(p, " PMI: "); for_each_online_cpu(j) seq_printf(p, "%10lu ", per_cpu(irq_pmi_count, j)); - seq_puts(p, " Performance Monitoring\n"); - seq_printf(p, "ERR: %10lu\n", irq_err_count); + seq_puts(p, " Performance Monitoring\n"); + seq_printf(p, " ERR: %10lu\n", irq_err_count); return 0; } diff --git a/arch/arm/kernel/smp.c b/arch/arm/kernel/smp.c index 4e8e89a26ca3..b5fb4697bc3f 100644 --- a/arch/arm/kernel/smp.c +++ b/arch/arm/kernel/smp.c @@ -551,8 +551,7 @@ void show_ipi_list(struct seq_file *p, int prec) if (!ipi_desc[i]) continue; - seq_printf(p, "%*s%u:%s", prec - 1, "IPI", i, - prec >= 4 ? " " : ""); + seq_printf(p, "%*s%u:", prec - 1, "IPI", i); for_each_online_cpu(cpu) seq_printf(p, "%10u ", irq_desc_kstat_cpu(ipi_desc[i], cpu)); diff --git a/arch/arm64/kernel/smp.c b/arch/arm64/kernel/smp.c index 1aa324104afb..1d0e0e6a5b92 100644 --- a/arch/arm64/kernel/smp.c +++ b/arch/arm64/kernel/smp.c @@ -833,11 +833,10 @@ int arch_show_interrupts(struct seq_file *p, int prec) unsigned int cpu, i; for (i = 0; i < MAX_IPI; i++) { - seq_printf(p, "%*s%u:%s", prec - 1, "IPI", i, - prec >= 4 ? " " : ""); + seq_printf(p, "%*s%u: ", prec - 1, "IPI", i); for_each_online_cpu(cpu) seq_printf(p, "%10u ", irq_desc_kstat_cpu(get_ipi_desc(cpu, i), cpu)); - seq_printf(p, " %s\n", ipi_types[i]); + seq_printf(p, " %s\n", ipi_types[i]); } seq_printf(p, "%*s: %10lu\n", prec, "Err", irq_err_count); diff --git a/arch/loongarch/kernel/smp.c b/arch/loongarch/kernel/smp.c index 64a048f1b880..50922610758b 100644 --- a/arch/loongarch/kernel/smp.c +++ b/arch/loongarch/kernel/smp.c @@ -88,7 +88,7 @@ void show_ipi_list(struct seq_file *p, int prec) unsigned int cpu, i; for (i = 0; i < NR_IPI; i++) { - seq_printf(p, "%*s%u:%s", prec - 1, "IPI", i, prec >= 4 ? " " : ""); + seq_printf(p, "%*s%u:", prec - 1, "IPI", i); for_each_online_cpu(cpu) seq_put_decimal_ull_width(p, " ", per_cpu(irq_stat, cpu).ipi_irqs[i], 10); seq_printf(p, " LoongArch %d %s\n", i + 1, ipi_types[i]); diff --git a/arch/riscv/kernel/smp.c b/arch/riscv/kernel/smp.c index 5ed5095320e6..fa66f9c97d74 100644 --- a/arch/riscv/kernel/smp.c +++ b/arch/riscv/kernel/smp.c @@ -226,8 +226,7 @@ void show_ipi_stats(struct seq_file *p, int prec) unsigned int cpu, i; for (i = 0; i < IPI_MAX; i++) { - seq_printf(p, "%*s%u:%s", prec - 1, "IPI", i, - prec >= 4 ? " " : ""); + seq_printf(p, "%*s%u:", prec - 1, "IPI", i); for_each_online_cpu(cpu) seq_printf(p, "%10u ", irq_desc_kstat_cpu(ipi_desc[i], cpu)); seq_printf(p, " %s\n", ipi_names[i]); diff --git a/arch/sh/kernel/irq.c b/arch/sh/kernel/irq.c index 9022d8af9d68..03c39b5da50f 100644 --- a/arch/sh/kernel/irq.c +++ b/arch/sh/kernel/irq.c @@ -46,7 +46,7 @@ int arch_show_interrupts(struct seq_file *p, int prec) seq_printf(p, "%*s:", prec, "NMI"); for_each_online_cpu(j) seq_put_decimal_ull_width(p, " ", per_cpu(irq_stat.__nmi_count, j), 10); - seq_printf(p, " Non-maskable interrupts\n"); + seq_printf(p, " Non-maskable interrupts\n"); seq_printf(p, "%*s: %10u\n", prec, "ERR", atomic_read(&irq_err_count)); diff --git a/arch/sparc/kernel/irq_32.c b/arch/sparc/kernel/irq_32.c index 5210991429d5..22db727652ba 100644 --- a/arch/sparc/kernel/irq_32.c +++ b/arch/sparc/kernel/irq_32.c @@ -199,19 +199,19 @@ int arch_show_interrupts(struct seq_file *p, int prec) int j; #ifdef CONFIG_SMP - seq_printf(p, "RES:"); + seq_printf(p, "%*s:", prec, "RES"); for_each_online_cpu(j) seq_put_decimal_ull_width(p, " ", cpu_data(j).irq_resched_count, 10); - seq_printf(p, " IPI rescheduling interrupts\n"); - seq_printf(p, "CAL:"); + seq_printf(p, " IPI rescheduling interrupts\n"); + seq_printf(p, "%*s:", prec, "CAL"); for_each_online_cpu(j) seq_put_decimal_ull_width(p, " ", cpu_data(j).irq_call_count, 10); - seq_printf(p, " IPI function call interrupts\n"); + seq_printf(p, " IPI function call interrupts\n"); #endif - seq_printf(p, "NMI:"); + seq_printf(p, "%*s:", prec, "NMI"); for_each_online_cpu(j) seq_put_decimal_ull_width(p, " ", cpu_data(j).counter, 10); - seq_printf(p, " Non-maskable interrupts\n"); + seq_printf(p, " Non-maskable interrupts\n"); return 0; } diff --git a/arch/sparc/kernel/irq_64.c b/arch/sparc/kernel/irq_64.c index c5466a9fd560..3f55c69d5f3b 100644 --- a/arch/sparc/kernel/irq_64.c +++ b/arch/sparc/kernel/irq_64.c @@ -303,10 +303,10 @@ int arch_show_interrupts(struct seq_file *p, int prec) { int j; - seq_printf(p, "NMI:"); + seq_printf(p, "%*s:", prec, "NMI"); for_each_online_cpu(j) seq_put_decimal_ull_width(p, " ", cpu_data(j).__nmi_count, 10); - seq_printf(p, " Non-maskable interrupts\n"); + seq_printf(p, " Non-maskable interrupts\n"); return 0; } diff --git a/arch/um/kernel/irq.c b/arch/um/kernel/irq.c index 5929d498b65f..ddfd6e9bd8c7 100644 --- a/arch/um/kernel/irq.c +++ b/arch/um/kernel/irq.c @@ -716,12 +716,12 @@ int arch_show_interrupts(struct seq_file *p, int prec) seq_printf(p, "%*s: ", prec, "RES"); for_each_online_cpu(cpu) seq_printf(p, "%10u ", irq_stats(cpu)->irq_resched_count); - seq_puts(p, " Rescheduling interrupts\n"); + seq_puts(p, " Rescheduling interrupts\n"); seq_printf(p, "%*s: ", prec, "CAL"); for_each_online_cpu(cpu) seq_printf(p, "%10u ", irq_stats(cpu)->irq_call_count); - seq_puts(p, " Function call interrupts\n"); + seq_puts(p, " Function call interrupts\n"); #endif return 0; diff --git a/arch/xtensa/kernel/irq.c b/arch/xtensa/kernel/irq.c index b1e410f6b5ab..6f01f530868b 100644 --- a/arch/xtensa/kernel/irq.c +++ b/arch/xtensa/kernel/irq.c @@ -59,7 +59,7 @@ int arch_show_interrupts(struct seq_file *p, int prec) seq_printf(p, "%*s:", prec, "NMI"); for_each_online_cpu(cpu) seq_printf(p, " %10lu", per_cpu(nmi_count, cpu)); - seq_puts(p, " Non-maskable interrupts\n"); + seq_puts(p, " Non-maskable interrupts\n"); #endif return 0; } diff --git a/kernel/irq/proc.c b/kernel/irq/proc.c index c635fb62c783..1cb47a8eab49 100644 --- a/kernel/irq/proc.c +++ b/kernel/irq/proc.c @@ -460,7 +460,7 @@ int __weak arch_show_interrupts(struct seq_file *p, int prec) static struct irq_proc_constraints { unsigned int num_prec; } irq_proc_constraints __read_mostly = { - .num_prec = 3, + .num_prec = 4, }; #ifndef ACTUAL_NR_IRQS @@ -471,7 +471,7 @@ void irq_proc_calc_prec(void) { unsigned int prec, n; - for (prec = 3, n = 1000; prec < 10 && n <= total_nr_irqs; ++prec) + for (prec = 4, n = 10000; prec < 10 && n <= total_nr_irqs; ++prec) n *= 10; WRITE_ONCE(irq_proc_constraints.num_prec, prec); } diff --git a/scripts/gdb/linux/interrupts.py b/scripts/gdb/linux/interrupts.py index a4e25d2bf123..e96734348f86 100644 --- a/scripts/gdb/linux/interrupts.py +++ b/scripts/gdb/linux/interrupts.py @@ -131,23 +131,19 @@ def arm_common_show_interrupts(prec): if nr_ipi is None or ipi_desc is None or ipi_types is None: return text - if prec >= 4: - sep = " " - else: - sep = "" - for ipi in range(nr_ipi): - text += "%*s%u:%s" % (prec - 1, "IPI", ipi, sep) + text += "%*s%u: " % (prec - 1, "IPI", ipi) desc = ipi_desc[ipi].cast(irq_desc_type.get_type().pointer()) if desc == 0: continue for cpu in cpus.each_online_cpu(): - text += "%10u" % (cpus.per_cpu(desc['kstat_irqs'], cpu)['cnt']) - text += " %s" % (ipi_types[ipi].string()) + text += "%10u " % (cpus.per_cpu(desc['kstat_irqs'], cpu)['cnt']) + text += "%s" % (ipi_types[ipi].string()) text += "\n" return text def aarch64_show_interrupts(prec): + # Does not work for ARM64 as "ipi_desc" is not available there text = arm_common_show_interrupts(prec) text += "%*s: %10lu\n" % (prec, "ERR", gdb.parse_and_eval("irq_err_count")) return text @@ -175,8 +171,8 @@ class LxInterruptList(gdb.Command): def invoke(self, arg, from_tty): nr_irqs = gdb.parse_and_eval("total_nr_irqs") - prec = 3 - j = 1000 + prec = 4 + j = 10000 while prec < 10 and j <= nr_irqs: prec += 1 j *= 10 -- cgit v1.2.3 From 61b51a167c524b65a59b1342e70c2008d514a796 Mon Sep 17 00:00:00 2001 From: Thomas Gleixner Date: Sun, 17 May 2026 22:02:44 +0200 Subject: genirq/proc: Runtime size the chip name The chip name column in the /proc/interrupt output is 8 characters and right aligned, which causes visual clutter due to the fixed length and the alignment. Many interrupt chips, e.g. PCI/MSI[X] have way longer names. Update the length when a chip is assigned to an interrupt and utilize this information for the output. Align it left so all chip names start at the begin of the column. Update the GDB script as well and disentangle the header maze so it actually works with all .config combinations. Signed-off-by: Thomas Gleixner Tested-by: Michael Kelley Reviewed-by: Dmitry Ilvokhin Link: https://patch.msgid.link/20260517194932.085786035@kernel.org --- kernel/irq/chip.c | 6 ++++-- kernel/irq/debugfs.h | 44 +++++++++++++++++++++++++++++++++++++ kernel/irq/internals.h | 48 +++-------------------------------------- kernel/irq/irqdomain.c | 5 ++++- kernel/irq/proc.c | 33 +++++++++++++++++++++++----- kernel/irq/proc.h | 13 +++++++++++ scripts/gdb/linux/interrupts.py | 23 +++++++++++++------- 7 files changed, 111 insertions(+), 61 deletions(-) create mode 100644 kernel/irq/debugfs.h create mode 100644 kernel/irq/proc.h (limited to 'scripts') diff --git a/kernel/irq/chip.c b/kernel/irq/chip.c index 2d283d1390dc..934777925f5c 100644 --- a/kernel/irq/chip.c +++ b/kernel/irq/chip.c @@ -47,9 +47,11 @@ int irq_set_chip(unsigned int irq, const struct irq_chip *chip) scoped_irqdesc->irq_data.chip = (struct irq_chip *)(chip ?: &no_irq_chip); ret = 0; } - /* For !CONFIG_SPARSE_IRQ make the irq show up in allocated_irqs. */ - if (!ret) + if (!ret) { + /* For !CONFIG_SPARSE_IRQ make the irq show up in allocated_irqs. */ irq_mark_irq(irq); + irq_proc_update_chip(chip); + } return ret; } EXPORT_SYMBOL(irq_set_chip); diff --git a/kernel/irq/debugfs.h b/kernel/irq/debugfs.h new file mode 100644 index 000000000000..8a9360d5fefb --- /dev/null +++ b/kernel/irq/debugfs.h @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _KERNEL_IRQ_DEBUGFS_H +#define _KERNEL_IRQ_DEBUGFS_H + +#ifdef CONFIG_GENERIC_IRQ_DEBUGFS +#include + +struct irq_bit_descr { + unsigned int mask; + char *name; +}; + +#define BIT_MASK_DESCR(m) { .mask = m, .name = #m } + +void irq_debug_show_bits(struct seq_file *m, int ind, unsigned int state, + const struct irq_bit_descr *sd, int size); + +void irq_add_debugfs_entry(unsigned int irq, struct irq_desc *desc); +static inline void irq_remove_debugfs_entry(struct irq_desc *desc) +{ + debugfs_remove(desc->debugfs_file); + kfree(desc->dev_name); +} +void irq_debugfs_copy_devname(int irq, struct device *dev); +# ifdef CONFIG_IRQ_DOMAIN +void irq_domain_debugfs_init(struct dentry *root); +# else +static inline void irq_domain_debugfs_init(struct dentry *root) +{ +} +# endif +#else /* CONFIG_GENERIC_IRQ_DEBUGFS */ +static inline void irq_add_debugfs_entry(unsigned int irq, struct irq_desc *d) +{ +} +static inline void irq_remove_debugfs_entry(struct irq_desc *d) +{ +} +static inline void irq_debugfs_copy_devname(int irq, struct device *dev) +{ +} +#endif /* CONFIG_GENERIC_IRQ_DEBUGFS */ + +#endif diff --git a/kernel/irq/internals.h b/kernel/irq/internals.h index 37eec0337867..f9c099d45a64 100644 --- a/kernel/irq/internals.h +++ b/kernel/irq/internals.h @@ -12,6 +12,9 @@ #include #include +#include "debugfs.h" +#include "proc.h" + #ifdef CONFIG_SPARSE_IRQ # define MAX_SPARSE_IRQS INT_MAX #else @@ -149,12 +152,6 @@ static inline void unregister_handler_proc(unsigned int irq, static inline void irq_proc_update_valid(struct irq_desc *desc) { } #endif -#if defined(CONFIG_PROC_FS) && defined(CONFIG_GENERIC_IRQ_SHOW) -void irq_proc_calc_prec(void); -#else -static inline void irq_proc_calc_prec(void) { } -#endif - struct irq_desc *irq_find_desc_at_or_after(unsigned int offset); extern bool irq_can_set_affinity_usr(unsigned int irq); @@ -398,42 +395,3 @@ static inline struct irq_data *irqd_get_parent_data(struct irq_data *irqd) return NULL; #endif } - -#ifdef CONFIG_GENERIC_IRQ_DEBUGFS -#include - -struct irq_bit_descr { - unsigned int mask; - char *name; -}; - -#define BIT_MASK_DESCR(m) { .mask = m, .name = #m } - -void irq_debug_show_bits(struct seq_file *m, int ind, unsigned int state, - const struct irq_bit_descr *sd, int size); - -void irq_add_debugfs_entry(unsigned int irq, struct irq_desc *desc); -static inline void irq_remove_debugfs_entry(struct irq_desc *desc) -{ - debugfs_remove(desc->debugfs_file); - kfree(desc->dev_name); -} -void irq_debugfs_copy_devname(int irq, struct device *dev); -# ifdef CONFIG_IRQ_DOMAIN -void irq_domain_debugfs_init(struct dentry *root); -# else -static inline void irq_domain_debugfs_init(struct dentry *root) -{ -} -# endif -#else /* CONFIG_GENERIC_IRQ_DEBUGFS */ -static inline void irq_add_debugfs_entry(unsigned int irq, struct irq_desc *d) -{ -} -static inline void irq_remove_debugfs_entry(struct irq_desc *d) -{ -} -static inline void irq_debugfs_copy_devname(int irq, struct device *dev) -{ -} -#endif /* CONFIG_GENERIC_IRQ_DEBUGFS */ diff --git a/kernel/irq/irqdomain.c b/kernel/irq/irqdomain.c index cc93abf009e8..f15c9f1223bb 100644 --- a/kernel/irq/irqdomain.c +++ b/kernel/irq/irqdomain.c @@ -20,6 +20,8 @@ #include #include +#include "proc.h" + static LIST_HEAD(irq_domain_list); static DEFINE_MUTEX(irq_domain_mutex); @@ -1532,6 +1534,7 @@ int irq_domain_set_hwirq_and_chip(struct irq_domain *domain, unsigned int virq, irq_data->chip = (struct irq_chip *)(chip ? chip : &no_irq_chip); irq_data->chip_data = chip_data; + irq_proc_update_chip(chip); return 0; } EXPORT_SYMBOL_GPL(irq_domain_set_hwirq_and_chip); @@ -2081,7 +2084,7 @@ static void irq_domain_free_one_irq(struct irq_domain *domain, unsigned int virq #endif /* CONFIG_IRQ_DOMAIN_HIERARCHY */ #ifdef CONFIG_GENERIC_IRQ_DEBUGFS -#include "internals.h" +#include "debugfs.h" static struct dentry *domain_dir; diff --git a/kernel/irq/proc.c b/kernel/irq/proc.c index 1cb47a8eab49..9a968007eb2d 100644 --- a/kernel/irq/proc.c +++ b/kernel/irq/proc.c @@ -457,10 +457,14 @@ int __weak arch_show_interrupts(struct seq_file *p, int prec) return 0; } +static DEFINE_RAW_SPINLOCK(irq_proc_constraints_lock); + static struct irq_proc_constraints { unsigned int num_prec; + unsigned int chip_width; } irq_proc_constraints __read_mostly = { .num_prec = 4, + .chip_width = 8, }; #ifndef ACTUAL_NR_IRQS @@ -473,7 +477,23 @@ void irq_proc_calc_prec(void) for (prec = 4, n = 10000; prec < 10 && n <= total_nr_irqs; ++prec) n *= 10; - WRITE_ONCE(irq_proc_constraints.num_prec, prec); + + guard(raw_spinlock_irqsave)(&irq_proc_constraints_lock); + if (prec > irq_proc_constraints.num_prec) + WRITE_ONCE(irq_proc_constraints.num_prec, prec); +} + +void irq_proc_update_chip(const struct irq_chip *chip) +{ + unsigned int len = chip && chip->name ? strlen(chip->name) : 0; + + if (!len || len <= READ_ONCE(irq_proc_constraints.chip_width)) + return; + + /* Can be invoked from interrupt disabled contexts */ + guard(raw_spinlock_irqsave)(&irq_proc_constraints_lock); + if (len > irq_proc_constraints.chip_width) + WRITE_ONCE(irq_proc_constraints.chip_width, len); } /* Same as seq_put_decimal_ull_width(p, " ", cnt, 10) */ @@ -515,6 +535,7 @@ void irq_proc_emit_counts(struct seq_file *p, unsigned int __percpu *cnts) int show_interrupts(struct seq_file *p, void *v) { + unsigned int chip_width = READ_ONCE(irq_proc_constraints.chip_width); unsigned int prec = READ_ONCE(irq_proc_constraints.num_prec); int i = *(loff_t *) v, j; struct irqaction *action; @@ -550,18 +571,20 @@ int show_interrupts(struct seq_file *p, void *v) irq_proc_emit_counts(p, &desc->kstat_irqs->cnt); else irq_proc_emit_zero_counts(p, num_online_cpus()); - seq_putc(p, ' '); + + /* Enforce a visual gap */ + seq_write(p, " ", 2); guard(raw_spinlock_irq)(&desc->lock); if (desc->irq_data.chip) { if (desc->irq_data.chip->irq_print_chip) desc->irq_data.chip->irq_print_chip(&desc->irq_data, p); else if (desc->irq_data.chip->name) - seq_printf(p, "%8s", desc->irq_data.chip->name); + seq_printf(p, "%-*s", chip_width, desc->irq_data.chip->name); else - seq_printf(p, "%8s", "-"); + seq_printf(p, "%-*s", chip_width, "-"); } else { - seq_printf(p, "%8s", "None"); + seq_printf(p, "%-*s", chip_width, "None"); } seq_putc(p, ' '); diff --git a/kernel/irq/proc.h b/kernel/irq/proc.h new file mode 100644 index 000000000000..0631d57fbfb7 --- /dev/null +++ b/kernel/irq/proc.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _KERNEL_IRQ_PROC_H +#define _KERNEL_IRQ_PROC_H + +#if defined(CONFIG_PROC_FS) && defined(CONFIG_GENERIC_IRQ_SHOW) +void irq_proc_calc_prec(void); +void irq_proc_update_chip(const struct irq_chip *chip); +#else +static inline void irq_proc_calc_prec(void) { } +static inline void irq_proc_update_chip(const struct irq_chip *chip) { } +#endif + +#endif diff --git a/scripts/gdb/linux/interrupts.py b/scripts/gdb/linux/interrupts.py index e96734348f86..a68ae91b4531 100644 --- a/scripts/gdb/linux/interrupts.py +++ b/scripts/gdb/linux/interrupts.py @@ -20,7 +20,7 @@ def irq_desc_is_chained(desc): def irqd_is_level(desc): return desc['irq_data']['common']['state_use_accessors'] & constants.LX_IRQD_LEVEL -def show_irq_desc(prec, irq): +def show_irq_desc(prec, chip_width, irq): text = "" desc = mapletree.mtree_load(gdb.parse_and_eval("&sparse_irqs"), irq) @@ -58,7 +58,7 @@ def show_irq_desc(prec, irq): else: name = "-" - text += " %-8s" % (name) + text += " %-*s" % (chip_width, name) if desc['irq_data']['domain']: text += " %*lu" % (prec, desc['irq_data']['hwirq']) @@ -171,11 +171,18 @@ class LxInterruptList(gdb.Command): def invoke(self, arg, from_tty): nr_irqs = gdb.parse_and_eval("total_nr_irqs") - prec = 4 - j = 10000 - while prec < 10 and j <= nr_irqs: - prec += 1 - j *= 10 + constr = utils.gdb_eval_or_none('irq_proc_constraints') + + if constr: + prec = int(constr['num_prec']) + chip_width = int(constr['chip_width']) + else: + prec = 4 + j = 10000 + while prec < 10 and j <= nr_irqs: + prec += 1 + j *= 10 + chip_width = 8 gdb.write("%*s" % (prec + 8, "")) for cpu in cpus.each_online_cpu(): @@ -186,7 +193,7 @@ class LxInterruptList(gdb.Command): raise gdb.GdbError("Unable to find the sparse IRQ tree, is CONFIG_SPARSE_IRQ enabled?") for irq in range(nr_irqs): - gdb.write(show_irq_desc(prec, irq)) + gdb.write(show_irq_desc(prec, chip_width, irq)) gdb.write(arch_show_interrupts(prec)) -- cgit v1.2.3 From a960c2cdb19e237f284a3b96eb8a0359abcf0e63 Mon Sep 17 00:00:00 2001 From: Alice Ryhl Date: Tue, 31 Mar 2026 10:57:49 +0000 Subject: kbuild: rust: add AutoFDO support This patch enables AutoFDO build support for Rust code within the Linux kernel. This allows Rust code to be profiled and optimized based on the profile. The RUSTFLAGS variable was suffixed with *_AUTOFDO_CLANG to match the naming of the config option, which is called CONFIG_AUTOFDO_CLANG. This implementation has been verified in Android, first by inspecting the object files and confirming that they look correct. After that, it was verified as below: 1. Running the binderAddInts benchmark [1] with Rust Binder built as rust_binder.ko module, using a Pixel 9 Pro. 2. Collecting a profile on a Pixel 10 Pro XL using the app-launch benchmark, which starts different apps many times, on a device with Rust Binder as a built-in kernel module. (C Binder was not present on the device.) 3. Using the collected profile, run the binderAddInts benchmark again with Rust Binder built both as a rust_binder.ko module, and as a built-in kernel module. 4. In both cases, Rust Binder without AutoFDO was approximately 13% slower than the AutoFDO optimized version. Built-in vs .ko did not make a measurable performance difference. All of the above was verified in conjunction with my helpers inlining series [2], which confirmed that this worked correctly for helpers too once [3] was fixed in the helpers inlining series. Link: https://android.googlesource.com/platform/system/extras/+/920f089/tests/binder/benchmarks/binderAddInts.cpp [1] Link: https://lore.kernel.org/r/20260203-inline-helpers-v2-0-beb8547a03c9@google.com [2] Link: https://lore.kernel.org/r/aasPsbMEsX6iGUl8@google.com [3] Reviewed-by: Rong Xu Reviewed-by: Gary Guo Tested-by: Alice Ryhl Signed-off-by: Alice Ryhl Acked-by: Nicolas Schier Acked-by: Nathan Chancellor Link: https://patch.msgid.link/20260331-autofdo-v2-1-eb5c5964820d@google.com [ Reworded for typos. - Miguel ] Signed-off-by: Miguel Ojeda --- scripts/Makefile.autofdo | 6 +++++- scripts/Makefile.lib | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/Makefile.autofdo b/scripts/Makefile.autofdo index 1caf2457e585..3f08acab4549 100644 --- a/scripts/Makefile.autofdo +++ b/scripts/Makefile.autofdo @@ -3,14 +3,18 @@ # Enable available and selected Clang AutoFDO features. CFLAGS_AUTOFDO_CLANG := -fdebug-info-for-profiling -mllvm -enable-fs-discriminator=true -mllvm -improved-fs-discriminator=true +RUSTFLAGS_AUTOFDO_CLANG := -Zdebug-info-for-profiling -Cllvm-args=-enable-fs-discriminator=true -Cllvm-args=-improved-fs-discriminator=true ifndef CONFIG_DEBUG_INFO CFLAGS_AUTOFDO_CLANG += -gmlt + RUSTFLAGS_AUTOFDO_CLANG += -Cdebuginfo=line-tables-only endif ifdef CLANG_AUTOFDO_PROFILE CFLAGS_AUTOFDO_CLANG += -fprofile-sample-use=$(CLANG_AUTOFDO_PROFILE) -ffunction-sections CFLAGS_AUTOFDO_CLANG += -fsplit-machine-functions + RUSTFLAGS_AUTOFDO_CLANG += -Zprofile-sample-use=$(CLANG_AUTOFDO_PROFILE) -Zfunction-sections=y + RUSTFLAGS_AUTOFDO_CLANG += -Cllvm-args=-split-machine-functions endif ifdef CONFIG_LTO_CLANG_THIN @@ -21,4 +25,4 @@ ifdef CONFIG_LTO_CLANG_THIN KBUILD_LDFLAGS += -plugin-opt=-split-machine-functions endif -export CFLAGS_AUTOFDO_CLANG +export CFLAGS_AUTOFDO_CLANG RUSTFLAGS_AUTOFDO_CLANG diff --git a/scripts/Makefile.lib b/scripts/Makefile.lib index 0718e39cedda..eaddf6637669 100644 --- a/scripts/Makefile.lib +++ b/scripts/Makefile.lib @@ -123,6 +123,9 @@ ifeq ($(CONFIG_AUTOFDO_CLANG),y) _c_flags += $(if $(patsubst n%,, \ $(AUTOFDO_PROFILE_$(target-stem).o)$(AUTOFDO_PROFILE)$(is-kernel-object)), \ $(CFLAGS_AUTOFDO_CLANG)) +_rust_flags += $(if $(patsubst n%,, \ + $(AUTOFDO_PROFILE_$(target-stem).o)$(AUTOFDO_PROFILE)$(is-kernel-object)), \ + $(RUSTFLAGS_AUTOFDO_CLANG)) endif # -- cgit v1.2.3 From 72d33b8bfeacbfdccf2cda4650e8e002def036f8 Mon Sep 17 00:00:00 2001 From: Alice Ryhl Date: Wed, 8 Apr 2026 08:32:17 +0000 Subject: rust: kasan: add support for Software Tag-Based KASAN This adds support for Software Tag-Based KASAN (KASAN_SW_TAGS) when CONFIG_RUST is enabled. This requires that rustc includes support for the kernel-hwaddress sanitizer, which is available since 1.96.0 [1]. Unlike with clang, we need to pass -Zsanitizer-recover in addition to -Zsanitizer because the option is not implied automatically. The kasan makefile uses different names for the flags depending on whether CC is clang or gcc, but as we require that CC is clang when using KASAN, we do not need to try to handle mixed gcc/llvm builds when Rust is enabled. Link: https://github.com/rust-lang/rust/pull/153049 [1] Reviewed-by: Danilo Krummrich Signed-off-by: Alice Ryhl Reviewed-by: Gary Guo Link: https://patch.msgid.link/20260408-kasan-rust-sw-tags-v3-2-e07964d14363@google.com Signed-off-by: Miguel Ojeda --- init/Kconfig | 2 +- scripts/Makefile.kasan | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) (limited to 'scripts') diff --git a/init/Kconfig b/init/Kconfig index 826a7d768ca3..7ef3fa222ca3 100644 --- a/init/Kconfig +++ b/init/Kconfig @@ -2199,7 +2199,7 @@ config RUST depends on !CFI || HAVE_CFI_ICALL_NORMALIZE_INTEGERS_RUSTC select CFI_ICALL_NORMALIZE_INTEGERS if CFI depends on !KASAN || CC_IS_CLANG - depends on !KASAN_SW_TAGS + depends on !KASAN_SW_TAGS || RUSTC_VERSION >= 109600 help Enables Rust support in the kernel. diff --git a/scripts/Makefile.kasan b/scripts/Makefile.kasan index 0ba2aac3b8dc..91504e81247a 100644 --- a/scripts/Makefile.kasan +++ b/scripts/Makefile.kasan @@ -71,8 +71,6 @@ ifdef CONFIG_KASAN_SW_TAGS CFLAGS_KASAN := -fsanitize=kernel-hwaddress -# This sets flags that will enable SW_TAGS KASAN once enabled in Rust. These -# will not work today, and is guarded against in dependencies for CONFIG_RUST. RUSTFLAGS_KASAN := -Zsanitizer=kernel-hwaddress \ -Zsanitizer-recover=kernel-hwaddress -- cgit v1.2.3 From ce3267a39a92be732429fa1a1f6d95a807a31fa7 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Sun, 17 May 2026 13:05:04 -1000 Subject: kbuild: Bump minimum version of LLVM for building the kernel to 17.0.1 The current minimum version of LLVM for building the kernel is 15.0.0. However, there are two deficiencies compared to GCC that were fixed in LLVM 17 that are starting to become more noticeable. The first was a bug in LLVM's scope checker [1], where all labels in a function were validated as potential targets of an asm goto statement, even if they were not listed in the asm goto statement as targets. This becomes particularly problematic when the cleanup attribute is used, as asm goto(... : label_a); ... label_a: ... int var __free(foo); asm goto(... : label_b); ... label_b: ... will trigger an error since the scope checker will complain that the cleanup variable would be skipped when jumping from the first asm goto to label_b (which obviously cannot happen). This issue was the catalyst for commit e2ffa15b9baa ("kbuild: Disable CC_HAS_ASM_GOTO_OUTPUT on clang < 17"). Unfortunately, this issue is reproducible with regular asm goto in addition to asm goto with outputs, so that change was not entirely sufficient to avoid the issue altogether. As asm goto has effectively been required since commit a0a12c3ed057 ("asm goto: eradicate CC_HAS_ASM_GOTO") and the usage of the cleanup attribute continues to grow across the tree, raising the minimum to a version that avoids this issue altogether is a better long term solution than attempting to workaround it at every spot where it happens. The second issue is an incompatibility with GCC 8.1+ around variables marked with const being valid constant expressions for _Static_assert and other macros [2]. With GCC 8.1 being the minimum supported version since commit 118c40b7b503 ("kbuild: require gcc-8 and binutils-2.30"), this incompatibility becomes more of a maintenance burden since only clang-15 and clang-16 are affected by it. Looking at the clang version of various major distributions through Docker images, no one should be left behind as a result of this bump, as the old ones cannot clear the current minimum of 15.0.0. archlinux:latest clang version 22.1.3 debian:oldoldstable-slim Debian clang version 11.0.1-2 debian:oldstable-slim Debian clang version 14.0.6 debian:stable-slim Debian clang version 19.1.7 (3+b1) debian:testing-slim Debian clang version 21.1.8 (3+b1) debian:unstable-slim Debian clang version 21.1.8 (7+b1) fedora:42 clang version 20.1.8 (Fedora 20.1.8-4.fc42) fedora:latest clang version 21.1.8 (Fedora 21.1.8-4.fc43) fedora:44 clang version 22.1.1 (Fedora 22.1.1-2.fc44) fedora:rawhide clang version 22.1.3 (Fedora 22.1.3-1.fc45) opensuse/leap:latest clang version 17.0.6 opensuse/tumbleweed:latest clang version 21.1.8 ubuntu:jammy Ubuntu clang version 14.0.0-1ubuntu1.1 ubuntu:noble Ubuntu clang version 18.1.3 (1ubuntu1) ubuntu:questing Ubuntu clang version 20.1.8 (0ubuntu4) ubuntu:resolute Ubuntu clang version 21.1.8 (6ubuntu1) 17.0.1 is chosen as the minimum instead of 17.0.0 to ensure that the particular version of LLVM 17 has the two aforementioned bugs fixed, as the second was fixed during the 17.0.0 release candidate phase and it was not until LLVM 18 that LLVM adopted the scheme of x.0.0 being a prerelease version and x.1.0 is a release version [3] to help with scenarios such as this. Link: https://github.com/llvm/llvm-project/commit/f023f5cdb2e6c19026f04a15b5a935c041835d14 [1] Link: https://github.com/llvm/llvm-project/commit/0b2d5b967d98375793897295d651f58f6fbd3034 [2] Link: https://github.com/llvm/llvm-project/commit/4532617ae420056bf32f6403dde07fb99d276a49 [3] Acked-by: Nicolas Schier Link: https://patch.msgid.link/20260517-bump-minimum-supported-llvm-version-to-17-v2-1-b3b8cda46bdd@kernel.org Signed-off-by: Nathan Chancellor --- Documentation/process/changes.rst | 2 +- Documentation/translations/it_IT/process/changes.rst | 2 +- Documentation/translations/pt_BR/process/changes.rst | 2 +- scripts/min-tool-version.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) (limited to 'scripts') diff --git a/Documentation/process/changes.rst b/Documentation/process/changes.rst index 9a99037270ff..b9afce768446 100644 --- a/Documentation/process/changes.rst +++ b/Documentation/process/changes.rst @@ -36,7 +36,7 @@ bindgen (optional) 0.71.1 bindgen --version binutils 2.30 ld -v bison 2.0 bison --version btrfs-progs 0.18 btrfs --version -Clang/LLVM (optional) 15.0.0 clang --version +Clang/LLVM (optional) 17.0.1 clang --version e2fsprogs 1.41.4 e2fsck -V flex 2.5.35 flex --version gdb 7.2 gdb --version diff --git a/Documentation/translations/it_IT/process/changes.rst b/Documentation/translations/it_IT/process/changes.rst index 7e93833b4511..7ee54c972418 100644 --- a/Documentation/translations/it_IT/process/changes.rst +++ b/Documentation/translations/it_IT/process/changes.rst @@ -33,7 +33,7 @@ PC Card, per esempio, probabilmente non dovreste preoccuparvi di pcmciautils. Programma Versione minima Comando per verificare la versione ====================== ================= ======================================== GNU C 8.1 gcc --version -Clang/LLVM (optional) 13.0.0 clang --version +Clang/LLVM (optional) 17.0.1 clang --version Rust (opzionale) 1.78.0 rustc --version bindgen (opzionale) 0.65.1 bindgen --version GNU make 4.0 make --version diff --git a/Documentation/translations/pt_BR/process/changes.rst b/Documentation/translations/pt_BR/process/changes.rst index 1964c1c93b34..6bbfe60fd973 100644 --- a/Documentation/translations/pt_BR/process/changes.rst +++ b/Documentation/translations/pt_BR/process/changes.rst @@ -31,7 +31,7 @@ PC Card por exemplo, provavelmente não precisará se preocupar com o pcmciautil Programa Versão mínima Comando para verificar a versão ====================== =============== ======================================== GNU C 8.1 gcc --version -Clang/LLVM (optional) 15.0.0 clang --version +Clang/LLVM (optional) 17.0.1 clang --version Rust (optional) 1.78.0 rustc --version bindgen (optional) 0.65.1 bindgen --version GNU make 4.0 make --version diff --git a/scripts/min-tool-version.sh b/scripts/min-tool-version.sh index b96ec2d379b6..ea2689bc9641 100755 --- a/scripts/min-tool-version.sh +++ b/scripts/min-tool-version.sh @@ -27,7 +27,7 @@ llvm) if [ "$SRCARCH" = loongarch ]; then echo 18.0.0 else - echo 15.0.0 + echo 17.0.1 fi ;; rustc) -- cgit v1.2.3 From 2a35c63c6bc420df8d9b3daa5e2339233787f67c Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Sun, 17 May 2026 13:05:13 -1000 Subject: scripts/Makefile.warn: Drop -Wformat handling for clang < 16 Now that the minimum supported version of LLVM for building the kernel has been raised to 17.0.1, the block dealing with -Wformat with clang prior to 16 can be removed since the condition for its inclusion is always false. Reviewed-by: Nicolas Schier Link: https://patch.msgid.link/20260517-bump-minimum-supported-llvm-version-to-17-v2-10-b3b8cda46bdd@kernel.org Signed-off-by: Nathan Chancellor --- scripts/Makefile.warn | 10 ---------- 1 file changed, 10 deletions(-) (limited to 'scripts') diff --git a/scripts/Makefile.warn b/scripts/Makefile.warn index e77ca875aea4..35af7d6c6d18 100644 --- a/scripts/Makefile.warn +++ b/scripts/Makefile.warn @@ -135,16 +135,6 @@ KBUILD_CFLAGS += $(call cc-option, -Wno-stringop-truncation) KBUILD_CFLAGS += -Wno-override-init # alias for -Wno-initializer-overrides in clang ifdef CONFIG_CC_IS_CLANG -# Clang before clang-16 would warn on default argument promotions. -ifneq ($(call clang-min-version, 160000),y) -# Disable -Wformat -KBUILD_CFLAGS += -Wno-format -# Then re-enable flags that were part of the -Wformat group that aren't -# problematic. -KBUILD_CFLAGS += -Wformat-extra-args -Wformat-invalid-specifier -KBUILD_CFLAGS += -Wformat-zero-length -Wnonnull -KBUILD_CFLAGS += -Wformat-insufficient-args -endif KBUILD_CFLAGS += -Wno-pointer-to-enum-cast KBUILD_CFLAGS += -Wno-tautological-constant-out-of-range-compare KBUILD_CFLAGS += -Wno-unaligned-access -- cgit v1.2.3 From c10ba5c9c62e2158cfa8a8be1f4c6fab67c799ec Mon Sep 17 00:00:00 2001 From: Philipp Hahn Date: Fri, 15 May 2026 14:47:50 +0200 Subject: run-clang-tools: run multiprocessing.Pool as context manager `multiprocessing.pool.Pool()` should be used as a context manager so Python can free its internal resources and do a proper cleanup.[1] While at it move the code to read the `compiler_commands.json` so the opened file can be closed before the sub-processes are fork()ed. Link: https://docs.python.org/3/library/multiprocessing.html#multiprocessing.pool.Pool [1] Signed-off-by: Philipp Hahn Link: https://patch.msgid.link/40180613bef84946c45d6fbeb4bb274573cd0beb.1778849135.git.phahn-oss@avm.de Signed-off-by: Nathan Chancellor --- scripts/clang-tools/run-clang-tools.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) (limited to 'scripts') diff --git a/scripts/clang-tools/run-clang-tools.py b/scripts/clang-tools/run-clang-tools.py index f31ffd09e1ea..e78be82aa693 100755 --- a/scripts/clang-tools/run-clang-tools.py +++ b/scripts/clang-tools/run-clang-tools.py @@ -79,14 +79,15 @@ def run_analysis(entry): def main(): - try: - args = parse_arguments() + args = parse_arguments() + + # Read JSON data into the datastore variable + with open(args.path) as f: + datastore = json.load(f) - lock = multiprocessing.Lock() - pool = multiprocessing.Pool(initializer=init, initargs=(lock, args)) - # Read JSON data into the datastore variable - with open(args.path, "r") as f: - datastore = json.load(f) + lock = multiprocessing.Lock() + try: + with multiprocessing.Pool(initializer=init, initargs=(lock, args)) as pool: pool.map(run_analysis, datastore) except BrokenPipeError: # Python flushes standard streams on exit; redirect remaining output -- cgit v1.2.3 From 159921d63da16ff1d4764e73ddf9e1556b54dfc8 Mon Sep 17 00:00:00 2001 From: Yafang Shao Date: Tue, 26 May 2026 14:27:32 +0800 Subject: kbuild: rpm-pkg: append %{?dist} macro to Release tag Add support for the %{?dist} macro in the kernel.spec file. This enables building and releasing kernel RPMs with a custom distribution suffix (e.g., via rpmbuild's --define option) to better match production environment tracking. Signed-off-by: Yafang Shao Link: https://patch.msgid.link/20260526062732.84006-1-laoar.shao@gmail.com Signed-off-by: Nathan Chancellor --- scripts/package/kernel.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/package/kernel.spec b/scripts/package/kernel.spec index b3c956205af0..c732415662ef 100644 --- a/scripts/package/kernel.spec +++ b/scripts/package/kernel.spec @@ -6,7 +6,7 @@ Name: kernel Summary: The Linux Kernel Version: %(echo %{KERNELRELEASE} | sed -e 's/-/_/g') -Release: %{pkg_release} +Release: %{pkg_release}%{?dist} License: GPL Group: System Environment/Kernel Vendor: The Linux Community -- cgit v1.2.3 From d7231d8cb262b1e350c00271bf53d54414b4f3b1 Mon Sep 17 00:00:00 2001 From: Alexandre Courbot Date: Wed, 27 May 2026 20:52:17 +0900 Subject: scripts: modpost: detect and report truncated buf_printf() output buf_printf() uses a fixed-size stack buffer. vsnprintf() returns the number of bytes that *would* have been written to that buffer, which can be larger than the size of said buffer if the formatted string is too long. The problem is that whenever this happens buf_printf() currently passes this length, unchecked, to buf_write(), which silently reads past the stack buffer and copies invalid data into the output buffer. Fix this by detecting vsnprintf() failures and truncations before appending to the output buffer, and report a fatal error instead of producing corrupt symbol names. Signed-off-by: Alexandre Courbot Link: https://patch.msgid.link/20260527-nova-exports-v2-1-06de4c556d55@nvidia.com Signed-off-by: Nathan Chancellor --- scripts/mod/modpost.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/mod/modpost.c b/scripts/mod/modpost.c index abbcd3fc1394..0d2f1f09019b 100644 --- a/scripts/mod/modpost.c +++ b/scripts/mod/modpost.c @@ -1689,8 +1689,17 @@ void __attribute__((format(printf, 2, 3))) buf_printf(struct buffer *buf, va_start(ap, fmt); len = vsnprintf(tmp, SZ, fmt, ap); - buf_write(buf, tmp, len); va_end(ap); + + if (len < 0) { + perror("vsnprintf failed"); + exit(1); + } + if (len >= SZ) + fatal("buf_printf output truncated for string %s: %d bytes needed, %d available\n", + tmp, len + 1, SZ); + + buf_write(buf, tmp, len); } void buf_write(struct buffer *buf, const char *s, int len) -- cgit v1.2.3 From 2ddfdfe1ad31ba5be5a1c3b4b2b950cf6b6d7c35 Mon Sep 17 00:00:00 2001 From: Petr Vorel Date: Tue, 21 Apr 2026 23:14:06 +0200 Subject: checkpatch: allow passing config directory checkpatch.pl searches for .checkpatch.conf in $CWD, $HOME and $CWD/.scripts. Allow passing a single directory via CHECKPATCH_CONFIG_DIR environment variable (empty value is ignored). This allows to directly use project configuration file for projects which vendored checkpatch.pl (e.g. LTP or u-boot). Although it'd be more convenient for user to have --conf-dir option (instead of using environment variable), code would get ugly because options from the configuration file needs to be read before processing command line options with Getopt::Long. While at it, document directories and environment variable in -h help and HTML doc. Link: https://lore.kernel.org/20260421211408.383972-1-pvorel@suse.cz Signed-off-by: Petr Vorel Reviewed-by: Simon Glass Acked-by: Joe Perches Cc: Dwaipayan Ray Cc: Lukas Bulwahn Signed-off-by: Andrew Morton --- Documentation/dev-tools/checkpatch.rst | 7 +++++++ scripts/checkpatch.pl | 20 +++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) (limited to 'scripts') diff --git a/Documentation/dev-tools/checkpatch.rst b/Documentation/dev-tools/checkpatch.rst index dccede68698c..010dfcd615af 100644 --- a/Documentation/dev-tools/checkpatch.rst +++ b/Documentation/dev-tools/checkpatch.rst @@ -210,6 +210,13 @@ Available options: Display the help text. +Configuration file +================== + +Default configuration options can be stored in ``.checkpatch.conf``, search +path: ``.:$HOME:.scripts`` or in a directory specified by ``$CHECKPATCH_CONFIG_DIR`` +environment variable (falling back to the default search path). + Message Levels ============== diff --git a/scripts/checkpatch.pl b/scripts/checkpatch.pl index 0492d6afc9a1..8b44b3a6a4dd 100755 --- a/scripts/checkpatch.pl +++ b/scripts/checkpatch.pl @@ -57,6 +57,9 @@ my %ignore_type = (); my @ignore = (); my $help = 0; my $configuration_file = ".checkpatch.conf"; +my $def_configuration_dirs_help = '.:$HOME:.scripts'; +(my $def_configuration_dirs = $def_configuration_dirs_help) =~ s/\$(\w+)/$ENV{$1}/g; +my $env_config_dir = 'CHECKPATCH_CONFIG_DIR'; my $max_line_length = 100; my $ignore_perl_version = 0; my $minimum_perl_version = 5.10.0; @@ -146,6 +149,11 @@ Options: -h, --help, --version display this help and exit When FILE is - read standard input. + +CONFIGURATION FILE +Default configuration options can be stored in $configuration_file, +search path: '$def_configuration_dirs_help' or in a directory specified by +\$$env_config_dir environment variable (fallback to the default search path). EOM exit($exitcode); @@ -237,7 +245,7 @@ sub list_types { exit($exitcode); } -my $conf = which_conf($configuration_file); +my $conf = which_conf($configuration_file, $env_config_dir, $def_configuration_dirs); if (-f $conf) { my @conf_args; open(my $conffile, '<', "$conf") @@ -1531,9 +1539,15 @@ sub which { } sub which_conf { - my ($conf) = @_; + my ($conf, $env_key, $paths) = @_; + my $env_dir = $ENV{$env_key}; + + if (defined($env_dir) && $env_dir ne "") { + return "$env_dir/$conf" if (-e "$env_dir/$conf"); + warn "$P: Can't find a readable $conf in '$env_dir', falling back to default search paths\n"; + } - foreach my $path (split(/:/, ".:$ENV{HOME}:.scripts")) { + foreach my $path (split(/:/, $paths)) { if (-e "$path/$conf") { return "$path/$conf"; } -- cgit v1.2.3 From b8979a02f9c56d489e27690981d2aa29a3e79542 Mon Sep 17 00:00:00 2001 From: Petr Vorel Date: Tue, 21 Apr 2026 23:14:07 +0200 Subject: checkpatch: add option to not force /* */ for SPDX Add option --spdx-cxx-comments to not force C comments (/* */) for SPDX, but allow also C++ comments (//). As documented in aa19a176df95d6, this is required for some old toolchains still have older assembler tools which cannot handle C++ style comments. This avoids forcing this for projects which vendored checkpatch.pl (e.g. LTP or u-boot). Link: https://lore.kernel.org/20260421211408.383972-2-pvorel@suse.cz Signed-off-by: Petr Vorel Reviewed-by: Simon Glass Acked-by: Joe Perches Cc: Dwaipayan Ray Cc: Lukas Bulwahn Signed-off-by: Andrew Morton --- Documentation/dev-tools/checkpatch.rst | 7 +++++++ scripts/checkpatch.pl | 23 ++++++++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) (limited to 'scripts') diff --git a/Documentation/dev-tools/checkpatch.rst b/Documentation/dev-tools/checkpatch.rst index 010dfcd615af..6139a08c34cd 100644 --- a/Documentation/dev-tools/checkpatch.rst +++ b/Documentation/dev-tools/checkpatch.rst @@ -184,6 +184,13 @@ Available options: Override checking of perl version. Runtime errors may be encountered after enabling this flag if the perl version does not meet the minimum specified. + - --spdx-cxx-comments + + Don't force C comments ``/* */`` for SPDX license (required by old + toolchains), allow also C++ comments ``//``. + + NOTE: it should *not* be used for Linux mainline. + - --codespell Use the codespell dictionary for checking spelling errors. diff --git a/scripts/checkpatch.pl b/scripts/checkpatch.pl index 8b44b3a6a4dd..0d18771f1b01 100755 --- a/scripts/checkpatch.pl +++ b/scripts/checkpatch.pl @@ -62,6 +62,7 @@ my $def_configuration_dirs_help = '.:$HOME:.scripts'; my $env_config_dir = 'CHECKPATCH_CONFIG_DIR'; my $max_line_length = 100; my $ignore_perl_version = 0; +my $spdx_cxx_comments = 0; my $minimum_perl_version = 5.10.0; my $min_conf_desc_length = 4; my $spelling_file = "$D/spelling.txt"; @@ -138,6 +139,10 @@ Options: file. It's your fault if there's no backup or git --ignore-perl-version override checking of perl version. expect runtime errors. + --spdx-cxx-comments don't force C comments (/* */) for SPDX license + (required by old toolchains), allow also C++ + comments (//). + NOTE: it should *not* be used for Linux mainline. --codespell Use the codespell dictionary for spelling/typos (default:$codespellfile) --codespellfile Use this codespell dictionary @@ -347,6 +352,7 @@ GetOptions( 'fix!' => \$fix, 'fix-inplace!' => \$fix_inplace, 'ignore-perl-version!' => \$ignore_perl_version, + 'spdx-cxx-comments!' => \$spdx_cxx_comments, 'debug=s' => \%debug, 'test-only=s' => \$tst_only, 'codespell!' => \$codespell, @@ -3815,26 +3821,33 @@ sub process { $checklicenseline = 2; } elsif ($rawline =~ /^\+/) { my $comment = ""; - if ($realfile =~ /\.(h|s|S)$/) { - $comment = '/*'; - } elsif ($realfile =~ /\.(c|rs|dts|dtsi)$/) { + if ($realfile =~ /\.(c|rs|dts|dtsi)$/) { $comment = '//'; } elsif (($checklicenseline == 2) || $realfile =~ /\.(sh|pl|py|awk|tc|yaml)$/) { $comment = '#'; } elsif ($realfile =~ /\.rst$/) { $comment = '..'; } + my $pattern = qr{\Q$comment\E}; + if ($realfile =~ /\.(h|s|S)$/) { + $comment = '/*'; + $pattern = qr{/\*}; + if ($spdx_cxx_comments) { + $comment = '// or /*'; + $pattern = qr{//|/\*}; + } + } # check SPDX comment style for .[chsS] files if ($realfile =~ /\.[chsS]$/ && $rawline =~ /SPDX-License-Identifier:/ && - $rawline !~ m@^\+\s*\Q$comment\E\s*@) { + $rawline !~ m@^\+\s*$pattern\s*@) { WARN("SPDX_LICENSE_TAG", "Improper SPDX comment style for '$realfile', please use '$comment' instead\n" . $herecurr); } if ($comment !~ /^$/ && - $rawline !~ m@^\+\Q$comment\E SPDX-License-Identifier: @) { + $rawline !~ m@^\+$pattern SPDX-License-Identifier: @) { WARN("SPDX_LICENSE_TAG", "Missing or malformed SPDX-License-Identifier tag in line $checklicenseline\n" . $herecurr); } elsif ($rawline =~ /(SPDX-License-Identifier: .*)/) { -- cgit v1.2.3 From 1877a09b64f89278ce3dc83e0cf6a8b1516660cb Mon Sep 17 00:00:00 2001 From: Joe Perches Date: Sat, 18 Apr 2026 10:34:59 -0700 Subject: checkpatch: add check for function pointer arrays in declarations checkpatch did not allow function pointer arrays when testing declaration blocks. Add it. Link: https://lore.kernel.org/eb62763085eb42193a611bca00a62d6f0ae72e1e.1776530118.git.joe@perches.com Signed-off-by: Joe Perches Cc: Andy Whitcroft Cc: Dan Carpenter Cc: Dwaipayan Ray Cc: Lukas Bulwahn Signed-off-by: Andrew Morton --- scripts/checkpatch.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/checkpatch.pl b/scripts/checkpatch.pl index 0d18771f1b01..3727156e4cca 100755 --- a/scripts/checkpatch.pl +++ b/scripts/checkpatch.pl @@ -4183,7 +4183,7 @@ sub process { $pl =~ s/\b(?:$Attribute|$Sparse)\b//g; if (($pl =~ /^\+\s+$Declare\s*$Ident\s*[=,;:\[]/ || # function pointer declarations - $pl =~ /^\+\s+$Declare\s*\(\s*\*\s*$Ident\s*\)\s*[=,;:\[\(]/ || + $pl =~ /^\+\s+$Declare\s*\(\s*\*\s*$Ident(?:\s*\[\s*(?:$Ident|$Constant)?\s*\])?\s*\)\s*[=,;:\[\(]/ || # foo bar; where foo is some local typedef or #define $pl =~ /^\+\s+$Ident(?:\s+|\s*\*\s*)$Ident\s*[=,;\[]/ || # known declaration macros @@ -4197,7 +4197,7 @@ sub process { # looks like a declaration !($sl =~ /^\+\s+$Declare\s*$Ident\s*[=,;:\[]/ || # function pointer declarations - $sl =~ /^\+\s+$Declare\s*\(\s*\*\s*$Ident\s*\)\s*[=,;:\[\(]/ || + $sl =~ /^\+\s+$Declare\s*\(\s*\*\s*$Ident(?:\s*\[\s*(?:$Ident|$Constant)?\s*\])?\s*\)\s*[=,;:\[\(]/ || # foo bar; where foo is some local typedef or #define $sl =~ /^\+\s+$Ident(?:\s+|\s*\*\s*)$Ident\s*[=,;\[]/ || # known declaration macros -- cgit v1.2.3 From 09d8b39563e84fc245d642182715a7e0e024b0f2 Mon Sep 17 00:00:00 2001 From: Sasha Levin Date: Wed, 8 Apr 2026 15:45:42 -0400 Subject: get_maintainer: add --json output mode Add a --json flag to get_maintainer.pl that emits structured JSON output, making results machine-parseable for CI systems, IDE integrations, and AI-assisted development tools. The JSON output includes a maintainers array with structured name, email, and role fields, plus optional arrays for scm, status, subsystem, web, and bug information when those flags are enabled. Normal text output behavior is completely unchanged when --json is not specified. Assisted-by: Claude:claude-opus-4-6 Link: https://lore.kernel.org/20260408194542.1354549-1-sashal@kernel.org Signed-off-by: Sasha Levin Acked-by: Joe Perches Signed-off-by: Andrew Morton --- scripts/get_maintainer.pl | 73 ++++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 30 deletions(-) (limited to 'scripts') diff --git a/scripts/get_maintainer.pl b/scripts/get_maintainer.pl index f0ca0db6ddc2..16b80a700d4a 100755 --- a/scripts/get_maintainer.pl +++ b/scripts/get_maintainer.pl @@ -21,6 +21,7 @@ use Cwd; use File::Find; use File::Spec::Functions; use open qw(:std :encoding(UTF-8)); +use JSON::PP; my $cur_path = fastgetcwd() . '/'; my $lk_path = "./"; @@ -68,6 +69,7 @@ my $pattern_depth = 0; my $self_test = undef; my $version = 0; my $help = 0; +my $json = 0; my $find_maintainer_files = 0; my $maintainer_path; my $vcs_used = 0; @@ -285,6 +287,7 @@ if (!GetOptions( 'find-maintainer-files' => \$find_maintainer_files, 'mpath|maintainer-path=s' => \$maintainer_path, 'self-test:s' => \$self_test, + 'json!' => \$json, 'v|version' => \$version, 'h|help|usage' => \$help, )) { @@ -650,39 +653,48 @@ my %deduplicate_name_hash = (); my %deduplicate_address_hash = (); my @maintainers = get_maintainers(); -if (@maintainers) { - @maintainers = merge_email(@maintainers); - output(@maintainers); -} - -if ($scm) { - @scm = uniq(@scm); - output(@scm); -} - -if ($output_substatus) { - @substatus = uniq(@substatus); - output(@substatus); -} - -if ($status) { - @status = uniq(@status); - output(@status); -} -if ($subsystem) { - @subsystem = uniq(@subsystem); - output(@subsystem); -} +@maintainers = merge_email(@maintainers) if (@maintainers); +@scm = uniq(@scm) if ($scm); +@substatus = uniq(@substatus) if ($output_substatus); +@status = uniq(@status) if ($status); +@subsystem = uniq(@subsystem) if ($subsystem); +@web = uniq(@web) if ($web); +@bug = uniq(@bug) if ($bug); + +if ($json) { + my @json_maintainers; + for my $m (@maintainers) { + my ($addr, $role); + if ($output_roles && $m =~ /^(.*?)\s+\((.+)\)\s*$/) { + $addr = $1; + $role = $2; + } else { + $addr = $m; + } + my ($name, $email_addr) = parse_email($addr); + my %entry = (name => $name, email => $email_addr); + $entry{role} = $role if (defined $role && $role ne ''); + push(@json_maintainers, \%entry); + } -if ($web) { - @web = uniq(@web); - output(@web); -} + my %result = (maintainers => \@json_maintainers); + $result{scm} = \@scm if ($scm); + $result{status} = \@status if ($status); + $result{subsystem} = \@subsystem if ($subsystem); + $result{web} = \@web if ($web); + $result{bug} = \@bug if ($bug); -if ($bug) { - @bug = uniq(@bug); - output(@bug); + my $json_encoder = JSON::PP->new->canonical->utf8; + print($json_encoder->encode(\%result) . "\n"); +} else { + output(@maintainers) if (@maintainers); + output(@scm) if ($scm); + output(@substatus) if ($output_substatus); + output(@status) if ($status); + output(@subsystem) if ($subsystem); + output(@web) if ($web); + output(@bug) if ($bug); } exit($exit); @@ -1104,6 +1116,7 @@ Output type options: --separator [, ] => separator for multiple entries on 1 line using --separator also sets --nomultiline if --separator is not [, ] --multiline => print 1 entry per line + --json => output results as JSON Other options: --pattern-depth => Number of pattern directory traversals (default: 0 (all)) -- cgit v1.2.3 From c210dfaa2720fcad9cbc8ec30fb45ddbabee4bf8 Mon Sep 17 00:00:00 2001 From: Yury Norov Date: Mon, 4 May 2026 16:36:05 -0400 Subject: scripts/bloat-o-meter: ignore _sdata _sdata is a linker symbol, but bloat-o-meter may consider it as a real variable: $ scripts/bloat-o-meter vmlinux.orig vmlinux add/remove: 7/1 grow/shrink: 0/0 up/down: 3437/-4096 (-659) Function old new delta crc32table_le - 1024 +1024 crc32table_be - 1024 +1024 crc32ctable_le - 1024 +1024 byte_rev_table - 256 +256 crc32_be - 39 +39 crc32c - 35 +35 crc32_le - 35 +35 _sdata 4096 - -4096 Total: Before=8592564398, After=8592563739, chg -0.00% With the patch: $ scripts/bloat-o-meter vmlinux.orig vmlinux add/remove: 7/0 grow/shrink: 0/0 up/down: 3437/0 (3437) Function old new delta crc32table_le - 1024 +1024 crc32table_be - 1024 +1024 crc32ctable_le - 1024 +1024 byte_rev_table - 256 +256 crc32_be - 39 +39 crc32c - 35 +35 crc32_le - 35 +35 Total: Before=8592560302, After=8592563739, chg +0.00% Link: https://lore.kernel.org/20260504203606.427972-1-ynorov@nvidia.com Signed-off-by: Yury Norov Cc: Valtteri Koskivuori Cc: Eric Dumazet Signed-off-by: Andrew Morton --- scripts/bloat-o-meter | 1 + 1 file changed, 1 insertion(+) (limited to 'scripts') diff --git a/scripts/bloat-o-meter b/scripts/bloat-o-meter index 9b4fb996d95b..5868a8b11b0f 100755 --- a/scripts/bloat-o-meter +++ b/scripts/bloat-o-meter @@ -43,6 +43,7 @@ def getsizes(file, format): if name.startswith("__se_compat_sys"): continue if name.startswith("__addressable_"): continue if name.startswith("__noinstr_text_start"): continue + if name.startswith("_sdata"): continue if name == "linux_banner": continue if name == "vermagic": continue # statics and some other optimizations adds random .NUMBER -- cgit v1.2.3 From 9c72d26e9fe3be79dd1d8a2ba00a033503ffd27d Mon Sep 17 00:00:00 2001 From: Masahiro Yamada Date: Fri, 29 May 2026 11:53:44 -0700 Subject: kbuild: move vmlinux.a build rule to scripts/Makefile.vmlinux_a Move the build rule for vmlinux.a to a separate file in preparation for supporting distributed builds with Clang ThinLTO. Signed-off-by: Masahiro Yamada Tested-by: Rong Xu Tested-by: Piotr Gorski Tested-by: Nathan Chancellor Signed-off-by: Rong Xu Link: https://patch.msgid.link/20260529185347.2418373-2-xur@google.com [nathan: Squash in forward fix from Rong around '--thin' to $(AR) https://patch.msgid.link/20260529185347.2418373-3-xur@google.com] Signed-off-by: Nathan Chancellor --- Makefile | 16 ++++++---------- scripts/Makefile.vmlinux_a | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 scripts/Makefile.vmlinux_a (limited to 'scripts') diff --git a/Makefile b/Makefile index e27c91ea56fc..0fce9557a115 100644 --- a/Makefile +++ b/Makefile @@ -1291,7 +1291,7 @@ export ARCH_DRIVERS := $(drivers-y) $(drivers-m) KBUILD_VMLINUX_OBJS := built-in.a $(patsubst %/, %/lib.a, $(filter %/, $(libs-y))) KBUILD_VMLINUX_LIBS := $(filter-out %/, $(libs-y)) -export KBUILD_VMLINUX_LIBS +export KBUILD_VMLINUX_OBJS KBUILD_VMLINUX_LIBS export KBUILD_LDS := arch/$(SRCARCH)/kernel/vmlinux.lds ifdef CONFIG_TRIM_UNUSED_KSYMS @@ -1300,16 +1300,12 @@ ifdef CONFIG_TRIM_UNUSED_KSYMS KBUILD_MODULES := y endif -# '$(AR) mPi' needs 'T' to workaround the bug of llvm-ar <= 14 -quiet_cmd_ar_vmlinux.a = AR $@ - cmd_ar_vmlinux.a = \ - rm -f $@; \ - $(AR) cDPrST $@ $(KBUILD_VMLINUX_OBJS); \ - $(AR) mPiT $$($(AR) t $@ | sed -n 1p) $@ $$($(AR) t $@ | grep -F -f $(srctree)/scripts/head-object-list.txt) +PHONY += vmlinux_a +vmlinux_a: $(KBUILD_VMLINUX_OBJS) scripts/head-object-list.txt FORCE + $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.vmlinux_a -targets += vmlinux.a -vmlinux.a: $(KBUILD_VMLINUX_OBJS) scripts/head-object-list.txt FORCE - $(call if_changed,ar_vmlinux.a) +vmlinux.a: vmlinux_a + @: PHONY += vmlinux_o vmlinux_o: vmlinux.a $(KBUILD_VMLINUX_LIBS) diff --git a/scripts/Makefile.vmlinux_a b/scripts/Makefile.vmlinux_a new file mode 100644 index 000000000000..650d44330d1f --- /dev/null +++ b/scripts/Makefile.vmlinux_a @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: GPL-2.0-only + +PHONY := __default +__default: vmlinux.a + +include include/config/auto.conf +include $(srctree)/scripts/Kbuild.include +include $(srctree)/scripts/Makefile.lib + +# Link of built-in-fixup.a +# --------------------------------------------------------------------------- + +# '$(AR) mPi' needs 'T' to workaround the bug of llvm-ar <= 14 +quiet_cmd_ar_builtin_fixup = AR $@ + cmd_ar_builtin_fixup = \ + rm -f $@; \ + $(AR) cDPrST $@ $(KBUILD_VMLINUX_OBJS); \ + $(AR) mPiT $$($(AR) t $@ | sed -n 1p) $@ $$($(AR) t $@ | grep -F -f $(srctree)/scripts/head-object-list.txt) + +targets += built-in-fixup.a +built-in-fixup.a: $(KBUILD_VMLINUX_OBJS) scripts/head-object-list.txt FORCE + $(call if_changed,ar_builtin_fixup) + +# vmlinux.a +# --------------------------------------------------------------------------- + +targets += vmlinux.a +vmlinux.a: built-in-fixup.a FORCE + $(call if_changed,copy) + +# Add FORCE to the prerequisites of a target to force it to be always rebuilt. +# --------------------------------------------------------------------------- + +PHONY += FORCE +FORCE: + +# Read all saved command lines and dependencies for the $(targets) we +# may be building above, using $(if_changed{,_dep}). As an +# optimization, we don't need to read them if the target does not +# exist, we will rebuild anyway in that case. + +existing-targets := $(wildcard $(sort $(targets))) + +-include $(foreach f,$(existing-targets),$(dir $(f)).$(notdir $(f)).cmd) + +.PHONY: $(PHONY) -- cgit v1.2.3 From 9f2aee8f7d1842be08da860e45265d30dba0d1f7 Mon Sep 17 00:00:00 2001 From: Rong Xu Date: Fri, 29 May 2026 11:53:46 -0700 Subject: kbuild: distributed build support for Clang ThinLTO Add distributed ThinLTO build support for the Linux kernel. This new mode offers several advantages: (1) Increased flexibility in handling user-specified build options. (2) Improved user-friendliness for developers. (3) Greater convenience for integrating with objtool and livepatch. Note that "distributed" in this context refers to a term that differentiates in-process ThinLTO builds by invoking backend compilation through the linker, not necessarily building in distributed environments. Distributed ThinLTO is enabled via the `CONFIG_LTO_CLANG_THIN_DIST` Kconfig option. For example: > make LLVM=1 defconfig > scripts/config -e LTO_CLANG_THIN_DIST > make LLVM=1 oldconfig > make LLVM=1 vmlinux -j <..> The build flow proceeds in four stages: 1. Perform FE compilation, mirroring the in-process ThinLTO mode. 2. Thin-link the generated IR files and object files. 3. Find all IR files and perform BE compilation, using the flags stored in the .*.o.cmd files. 4. Link the BE results to generate the final vmlinux.o. NOTE: This patch currently implements the build for the main kernel image (vmlinux) only. Kernel module support is planned for a subsequent patch. Tested on the following arch: x86, arm64, loongarch, and riscv. The earlier implementation details can be found here: https://discourse.llvm.org/t/rfc-distributed-thinlto-build-for-kernel/85934 Signed-off-by: Rong Xu Co-developed-by: Masahiro Yamada Signed-off-by: Masahiro Yamada Tested-by: Piotr Gorski Tested-by: Nathan Chancellor Link: https://patch.msgid.link/20260529185347.2418373-4-xur@google.com Signed-off-by: Nathan Chancellor --- .gitignore | 2 ++ Makefile | 14 ++++++++++---- arch/Kconfig | 19 +++++++++++++++++++ scripts/Makefile.lib | 8 ++++++++ scripts/Makefile.thinlto | 40 ++++++++++++++++++++++++++++++++++++++++ scripts/Makefile.vmlinux_a | 37 +++++++++++++++++++++++++++++++++++++ scripts/mod/modpost.c | 15 ++++++++++++--- 7 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 scripts/Makefile.thinlto (limited to 'scripts') diff --git a/.gitignore b/.gitignore index 3044b9590f05..1cc34c9a5523 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,7 @@ *.zst Module.symvers dtbs-list +builtin.order modules.order # @@ -68,6 +69,7 @@ modules.order /vmlinux.32 /vmlinux.map /vmlinux.symvers +/vmlinux.thinlto-index /vmlinux.unstripped /vmlinux-gdb.py /vmlinuz diff --git a/Makefile b/Makefile index 0fce9557a115..857f08dcc952 100644 --- a/Makefile +++ b/Makefile @@ -1071,11 +1071,16 @@ export CC_FLAGS_SCS endif ifdef CONFIG_LTO_CLANG -ifdef CONFIG_LTO_CLANG_THIN +ifdef CONFIG_LTO_CLANG_FULL +CC_FLAGS_LTO := -flto +else CC_FLAGS_LTO := -flto=thin -fsplit-lto-unit + +# These LLVM options were initially added with only in-process ThinLTO +# support, so avoid distributed ThinLTO support for now. +ifdef CONFIG_LTO_CLANG_THIN KBUILD_LDFLAGS += $(call ld-option,--lto-whole-program-visibility -mllvm -always-rename-promoted-locals=false) -else -CC_FLAGS_LTO := -flto +endif endif CC_FLAGS_LTO += -fvisibility=hidden @@ -1684,6 +1689,7 @@ endif # CONFIG_MODULES CLEAN_FILES += vmlinux.symvers modules-only.symvers \ modules.builtin modules.builtin.modinfo modules.nsdeps \ modules.builtin.ranges vmlinux.o.map vmlinux.unstripped \ + vmlinux.thinlto-index builtin.order \ compile_commands.json rust/test \ rust-project.json .vmlinux.objs .vmlinux.export.c \ .builtin-dtbs-list .builtin-dtbs.S @@ -2145,7 +2151,7 @@ clean: $(clean-dirs) $(call cmd,rmfiles) @find . $(RCS_FIND_IGNORE) \ \( -name '*.[aios]' -o -name '*.rsi' -o -name '*.ko' -o -name '.*.cmd' \ - -o -name '*.ko.*' \ + -o -name '*.ko.*' -o -name '*.o.thinlto.bc' \ -o -name '*.dtb' -o -name '*.dtbo' \ -o -name '*.dtb.S' -o -name '*.dtbo.S' \ -o -name '*.dt.yaml' -o -name 'dtbs-list' \ diff --git a/arch/Kconfig b/arch/Kconfig index 5d6e9f56210b..0848932d1c8e 100644 --- a/arch/Kconfig +++ b/arch/Kconfig @@ -858,6 +858,25 @@ config LTO_CLANG_THIN https://clang.llvm.org/docs/ThinLTO.html If unsure, say Y. + +config LTO_CLANG_THIN_DIST + bool "Clang ThinLTO in distributed mode (EXPERIMENTAL)" + depends on HAS_LTO_CLANG && ARCH_SUPPORTS_LTO_CLANG_THIN + select LTO_CLANG + help + This option enables Clang's ThinLTO in distributed build mode. + In this mode, the linker performs the thin-link, generating + ThinLTO index files. Subsequently, the build system explicitly + invokes ThinLTO backend compilation using these index files + and pre-linked IR objects. The resulting native object files + are with the .thinlto-native.o suffix. + + This build mode offers improved visibility into the ThinLTO + process through explicit subcommand exposure. It also makes + final native object files directly available, benefiting + tools like objtool and kpatch. Additionally, it provides + crucial granular control over back-end options, enabling + module-specific compiler options, and simplifies debugging. endchoice config ARCH_SUPPORTS_AUTOFDO_CLANG diff --git a/scripts/Makefile.lib b/scripts/Makefile.lib index 0718e39cedda..86e1428cc55d 100644 --- a/scripts/Makefile.lib +++ b/scripts/Makefile.lib @@ -249,6 +249,13 @@ ifdef CONFIG_LTO_CLANG cmd_ld_single = $(if $(objtool-enabled)$(is-single-obj-m), ; $(LD) $(ld_flags) -r -o $(tmp-target) $@; mv $(tmp-target) $@) endif +ifdef CONFIG_LTO_CLANG_THIN_DIST +# Save the _c_flags, sliently. +quiet_cmd_save_c_flags = + saved_c_flags = $(_c_flags) $(modkern_cflags) + cmd_save_c_flags = printf '\n%s\n' 'saved_c_flags_$@ := $(call escsq,$(saved_c_flags))' >> $(dot-target).cmd +endif + quiet_cmd_cc_o_c = CC $(quiet_modtag) $@ cmd_cc_o_c = $(CC) $(c_flags) -c -o $@ $< \ $(cmd_ld_single) \ @@ -256,6 +263,7 @@ quiet_cmd_cc_o_c = CC $(quiet_modtag) $@ define rule_cc_o_c $(call cmd_and_fixdep,cc_o_c) + $(call cmd,save_c_flags) $(call cmd,checksrc) $(call cmd,checkdoc) $(call cmd,gen_objtooldep) diff --git a/scripts/Makefile.thinlto b/scripts/Makefile.thinlto new file mode 100644 index 000000000000..bb83f13f3cd6 --- /dev/null +++ b/scripts/Makefile.thinlto @@ -0,0 +1,40 @@ +PHONY := __default +__default: + +include include/config/auto.conf +include $(srctree)/scripts/Kbuild.include +include $(srctree)/scripts/Makefile.lib + +native-objs := $(patsubst %.o,%.thinlto-native.o,$(call read-file, vmlinux.thinlto-index)) + +__default: $(native-objs) + +# Generate .thinlto-native.o (obj) from .o (bitcode) and .thinlto.bc (summary) files +# --------------------------------------------------------------------------- +quiet_cmd_cc_o_bc = CC $(quiet_modtag) $@ + be_flags = $(shell sed -n '/saved_c_flags_/s/.*:= //p' \ + $(dir $(<)).$(notdir $(<)).cmd) + cmd_cc_o_bc = \ + $(CC) $(be_flags) -x ir -fno-lto -Wno-unused-command-line-argument \ + -fthinlto-index=$(word 2, $^) -c -o $@ $< + +targets += $(native-objs) +$(native-objs): %.thinlto-native.o: %.o %.o.thinlto.bc FORCE + $(call if_changed,cc_o_bc) + +# Add FORCE to the prerequisites of a target to force it to be always rebuilt. +# --------------------------------------------------------------------------- + +PHONY += FORCE +FORCE: + +# Read all saved command lines and dependencies for the $(targets) we +# may be building above, using $(if_changed{,_dep}). As an +# optimization, we don't need to read them if the target does not +# exist, we will rebuild anyway in that case. + +existing-targets := $(wildcard $(sort $(targets))) + +-include $(foreach f, $(existing-targets),$(dir $(f)).$(notdir $(f)).cmd) + +.PHONY: $(PHONY) diff --git a/scripts/Makefile.vmlinux_a b/scripts/Makefile.vmlinux_a index 650d44330d1f..bd141b893748 100644 --- a/scripts/Makefile.vmlinux_a +++ b/scripts/Makefile.vmlinux_a @@ -21,6 +21,41 @@ targets += built-in-fixup.a built-in-fixup.a: $(KBUILD_VMLINUX_OBJS) scripts/head-object-list.txt FORCE $(call if_changed,ar_builtin_fixup) +ifdef CONFIG_LTO_CLANG_THIN_DIST + +quiet_cmd_builtin.order = GEN $@ + cmd_builtin.order = $(AR) t $< > $@ + +targets += builtin.order +builtin.order: built-in-fixup.a FORCE + $(call if_changed,builtin.order) + +quiet_cmd_ld_thinlto_index = LD $@ + cmd_ld_thinlto_index = \ + $(LD) $(KBUILD_LDFLAGS) -r --thinlto-index-only=$@ @$< + +targets += vmlinux.thinlto-index +vmlinux.thinlto-index: builtin.order FORCE + $(call if_changed,ld_thinlto_index) + +quiet_cmd_ar_vmlinux.a = GEN $@ + cmd_ar_vmlinux.a = \ + rm -f $@; \ + while read -r obj; do \ + if grep -Fqx $${obj} $(word 2, $^); then \ + echo $${obj%.o}.thinlto-native.o; \ + else \ + echo $${obj}; \ + fi; \ + done < $< | xargs $(AR) cDPrS --thin $@ + +targets += vmlinux.a +vmlinux.a: builtin.order vmlinux.thinlto-index FORCE + $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.thinlto + $(call if_changed,ar_vmlinux.a) + +else + # vmlinux.a # --------------------------------------------------------------------------- @@ -28,6 +63,8 @@ targets += vmlinux.a vmlinux.a: built-in-fixup.a FORCE $(call if_changed,copy) +endif + # Add FORCE to the prerequisites of a target to force it to be always rebuilt. # --------------------------------------------------------------------------- diff --git a/scripts/mod/modpost.c b/scripts/mod/modpost.c index 0d2f1f09019b..da0f4bc75509 100644 --- a/scripts/mod/modpost.c +++ b/scripts/mod/modpost.c @@ -1487,13 +1487,22 @@ static void extract_crcs_for_object(const char *object, struct module *mod) char cmd_file[PATH_MAX]; char *buf, *p; const char *base; - int dirlen, ret; + int dirlen, baselen_without_suffix, ret; base = get_basename(object); dirlen = base - object; - ret = snprintf(cmd_file, sizeof(cmd_file), "%.*s.%s.cmd", - dirlen, object, base); + baselen_without_suffix = strlen(object) - dirlen - strlen(".o"); + + /* + * When CONFIG_LTO_CLANG_THIN_DIST=y, the ELF is *.thinlto-native.o + * but the symbol CRCs are recorded in *.o.cmd file. + */ + if (strends(object, ".thinlto-native.o")) + baselen_without_suffix -= strlen(".thinlto-native"); + + ret = snprintf(cmd_file, sizeof(cmd_file), "%.*s.%.*s.o.cmd", + dirlen, object, baselen_without_suffix, base); if (ret >= sizeof(cmd_file)) { error("%s: too long path was truncated\n", cmd_file); return; -- cgit v1.2.3 From 905b06d32a52afe32fcf5f30cf298c9ea6359f11 Mon Sep 17 00:00:00 2001 From: Miguel Ojeda Date: Sat, 30 May 2026 13:49:25 +0200 Subject: rust: x86: support Rust >= 1.98.0 target spec Starting with Rust 1.98.0 (expected 2026-08-20), the target spec will not support `x86-softfloat` anymore [1]. Instead, `softfloat` should be used, which is an alias. Otherwise, one gets: error: error loading target specification: rustc-abi: invalid rustc abi: 'x86-softfloat'. allowed values: 'x86-sse2', 'softfloat' at line 3 column 32 | = help: run `rustc --print target-list` for a list of built-in targets Thus conditionally use one or the other depending on the version. The alias has existed since Rust 1.95.0 (released 2026-04-16) [2], but use the newer version instead to avoid changing how the build works for existing compilers, at least until more testing takes place. Cc: Ralf Jung Cc: stable@vger.kernel.org # Needed in 6.12.y and later (Rust is pinned in older LTSs). Link: https://github.com/rust-lang/rust/pull/157151 [1] Link: https://github.com/rust-lang/rust/pull/151154 [2] Reviewed-by: Alice Ryhl Link: https://patch.msgid.link/20260530114925.260754-1-ojeda@kernel.org Signed-off-by: Miguel Ojeda --- scripts/generate_rust_target.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/generate_rust_target.rs b/scripts/generate_rust_target.rs index 38b3416bb979..16f7e855e012 100644 --- a/scripts/generate_rust_target.rs +++ b/scripts/generate_rust_target.rs @@ -196,7 +196,9 @@ fn main() { } } else if cfg.has("X86_64") { ts.push("arch", "x86_64"); - if cfg.rustc_version_atleast(1, 86, 0) { + if cfg.rustc_version_atleast(1, 98, 0) { + ts.push("rustc-abi", "softfloat"); + } else if cfg.rustc_version_atleast(1, 86, 0) { ts.push("rustc-abi", "x86-softfloat"); } ts.push( @@ -236,7 +238,9 @@ fn main() { panic!("32-bit x86 only works under UML"); } ts.push("arch", "x86"); - if cfg.rustc_version_atleast(1, 86, 0) { + if cfg.rustc_version_atleast(1, 98, 0) { + ts.push("rustc-abi", "softfloat"); + } else if cfg.rustc_version_atleast(1, 86, 0) { ts.push("rustc-abi", "x86-softfloat"); } ts.push( -- cgit v1.2.3 From 29807c524d66e27762cdc8992c2cac89b4c3fda9 Mon Sep 17 00:00:00 2001 From: Frederic Weisbecker Date: Fri, 8 May 2026 15:16:42 +0200 Subject: tick/sched: Remove unused fields Remove fields after the dyntick-idle cputime migration to scheduler code. Signed-off-by: Frederic Weisbecker Signed-off-by: Thomas Gleixner Tested-by: Shrikanth Hegde Link: https://patch.msgid.link/20260508131647.43868-11-frederic@kernel.org --- kernel/time/tick-sched.h | 12 ------------ kernel/time/timer_list.c | 6 +----- scripts/gdb/linux/timerlist.py | 4 ---- 3 files changed, 1 insertion(+), 21 deletions(-) (limited to 'scripts') diff --git a/kernel/time/tick-sched.h b/kernel/time/tick-sched.h index b4a7822f495d..79b9252047b1 100644 --- a/kernel/time/tick-sched.h +++ b/kernel/time/tick-sched.h @@ -44,9 +44,7 @@ struct tick_device { * to resume the tick timer operation in the timeline * when the CPU returns from nohz sleep. * @next_tick: Next tick to be fired when in dynticks mode. - * @idle_jiffies: jiffies at the entry to idle for idle time accounting * @idle_waketime: Time when the idle was interrupted - * @idle_sleeptime_seq: sequence counter for data consistency * @idle_entrytime: Time when the idle call was entered * @last_jiffies: Base jiffies snapshot when next event was last computed * @timer_expires_base: Base time clock monotonic for @timer_expires @@ -55,9 +53,6 @@ struct tick_device { * @idle_expires: Next tick in idle, for debugging purpose only * @idle_calls: Total number of idle calls * @idle_sleeps: Number of idle calls, where the sched tick was stopped - * @idle_exittime: Time when the idle state was left - * @idle_sleeptime: Sum of the time slept in idle with sched tick stopped - * @iowait_sleeptime: Sum of the time slept in idle with sched tick stopped, with IO outstanding * @tick_dep_mask: Tick dependency mask - is set, if someone needs the tick * @check_clocks: Notification mechanism about clocksource changes */ @@ -73,12 +68,10 @@ struct tick_sched { struct hrtimer sched_timer; ktime_t last_tick; ktime_t next_tick; - unsigned long idle_jiffies; ktime_t idle_waketime; unsigned int got_idle_tick; /* Idle entry */ - seqcount_t idle_sleeptime_seq; ktime_t idle_entrytime; /* Tick stop */ @@ -90,11 +83,6 @@ struct tick_sched { unsigned long idle_calls; unsigned long idle_sleeps; - /* Idle exit */ - ktime_t idle_exittime; - ktime_t idle_sleeptime; - ktime_t iowait_sleeptime; - /* Full dynticks handling */ atomic_t tick_dep_mask; diff --git a/kernel/time/timer_list.c b/kernel/time/timer_list.c index 427d7ddea3af..514802def1e0 100644 --- a/kernel/time/timer_list.c +++ b/kernel/time/timer_list.c @@ -152,14 +152,10 @@ static void print_cpu(struct seq_file *m, int cpu, u64 now) P_flag(highres, TS_FLAG_HIGHRES); P_ns(last_tick); P_flag(tick_stopped, TS_FLAG_STOPPED); - P(idle_jiffies); P(idle_calls); P(idle_sleeps); P_ns(idle_entrytime); P_ns(idle_waketime); - P_ns(idle_exittime); - P_ns(idle_sleeptime); - P_ns(iowait_sleeptime); P(last_jiffies); P(next_timer); P_ns(idle_expires); @@ -256,7 +252,7 @@ static void timer_list_show_tickdevices_header(struct seq_file *m) static inline void timer_list_header(struct seq_file *m, u64 now) { - SEQ_printf(m, "Timer List Version: v0.10\n"); + SEQ_printf(m, "Timer List Version: v0.11\n"); SEQ_printf(m, "HRTIMER_MAX_CLOCK_BASES: %d\n", HRTIMER_MAX_CLOCK_BASES); SEQ_printf(m, "now at %Ld nsecs\n", (unsigned long long)now); SEQ_printf(m, "\n"); diff --git a/scripts/gdb/linux/timerlist.py b/scripts/gdb/linux/timerlist.py index 9fb3436a217c..744b032e4d38 100644 --- a/scripts/gdb/linux/timerlist.py +++ b/scripts/gdb/linux/timerlist.py @@ -90,14 +90,10 @@ def print_cpu(hrtimer_bases, cpu, max_clock_bases): text += f" .{'nohz':15s}: {int(bool(ts['flags'] & TS_FLAG_NOHZ))}\n" text += f" .{'last_tick':15s}: {ts['last_tick']}\n" text += f" .{'tick_stopped':15s}: {int(bool(ts['flags'] & TS_FLAG_STOPPED))}\n" - text += f" .{'idle_jiffies':15s}: {ts['idle_jiffies']}\n" text += f" .{'idle_calls':15s}: {ts['idle_calls']}\n" text += f" .{'idle_sleeps':15s}: {ts['idle_sleeps']}\n" text += f" .{'idle_entrytime':15s}: {ts['idle_entrytime']} nsecs\n" text += f" .{'idle_waketime':15s}: {ts['idle_waketime']} nsecs\n" - text += f" .{'idle_exittime':15s}: {ts['idle_exittime']} nsecs\n" - text += f" .{'idle_sleeptime':15s}: {ts['idle_sleeptime']} nsecs\n" - text += f" .{'iowait_sleeptime':15s}: {ts['iowait_sleeptime']} nsecs\n" text += f" .{'last_jiffies':15s}: {ts['last_jiffies']}\n" text += f" .{'next_timer':15s}: {ts['next_timer']}\n" text += f" .{'idle_expires':15s}: {ts['idle_expires']} nsecs\n" -- cgit v1.2.3 From a48bd961fb203a7ce68f8110fc53a85f90e24b33 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Tue, 2 Jun 2026 18:41:50 -0700 Subject: kbuild: Remove unnecessary 'T' modifier in cmd_ar_builtin_fixup In cmd_ar_builtin_fixup, the 'T' modifier was added to '$(AR) mPi' to work around a bug in llvm-ar that caused thin archives to be silently converted to full archives [1]. Since commit 20c098928356 ("kbuild: Bump minimum version of LLVM for building the kernel to 15.0.0"), all supported versions of llvm-ar have this issue fixed, so the 'T' modifier and comment can be removed. Link: https://github.com/llvm/llvm-project/commit/d17c54d17de22d2961a04163f3dbc8e973de89b8 [1] Signed-off-by: Nathan Chancellor --- scripts/Makefile.vmlinux_a | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/Makefile.vmlinux_a b/scripts/Makefile.vmlinux_a index bd141b893748..395e29998d7d 100644 --- a/scripts/Makefile.vmlinux_a +++ b/scripts/Makefile.vmlinux_a @@ -10,12 +10,11 @@ include $(srctree)/scripts/Makefile.lib # Link of built-in-fixup.a # --------------------------------------------------------------------------- -# '$(AR) mPi' needs 'T' to workaround the bug of llvm-ar <= 14 quiet_cmd_ar_builtin_fixup = AR $@ cmd_ar_builtin_fixup = \ rm -f $@; \ $(AR) cDPrST $@ $(KBUILD_VMLINUX_OBJS); \ - $(AR) mPiT $$($(AR) t $@ | sed -n 1p) $@ $$($(AR) t $@ | grep -F -f $(srctree)/scripts/head-object-list.txt) + $(AR) mPi $$($(AR) t $@ | sed -n 1p) $@ $$($(AR) t $@ | grep -F -f $(srctree)/scripts/head-object-list.txt) targets += built-in-fixup.a built-in-fixup.a: $(KBUILD_VMLINUX_OBJS) scripts/head-object-list.txt FORCE -- cgit v1.2.3 From f58316a441b4626324993db585fa4b7b7c780fac Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Wed, 27 May 2026 09:27:03 -0500 Subject: kconfig: add kconfig-sym-check static checker Add 'make kconfig-sym-check', a static checker that finds Kconfig symbols referenced in expressions (select, depends on, default, etc.) but never defined via config/menuconfig anywhere in the tree. New dangling symbols are reported as errors (exit 1) unless they are listed in an exclusion file, e.g. KCONFIG_SYM_CHECK_EXCLUDES=sym-check-excludes make kconfig-sym-check The exclusion file lists one symbol per line; blank lines and lines starting with '#' are ignored. The checker also warns about uppercase N/Y/M used as tristate literal values following the same logic as checkpatch. This new static checker is the script used for [1] with a few improvements to avoid some false positives. Link: https://bugzilla.kernel.org/show_bug.cgi?id=216748 [1] Assisted-by: Claude:claude-sonnet-4-6 Signed-off-by: Andrew Jones Acked-by: Andy Shevchenko Acked-by: Randy Dunlap Tested-by: Randy Dunlap Tested-by: Julian Braha Tested-by: Nicolas Schier Acked-by: Nicolas Schier Link: https://patch.msgid.link/20260527142703.107110-1-andrew.jones@linux.dev Signed-off-by: Nathan Chancellor --- Makefile | 23 +++--- scripts/kconfig/kconfig-sym-check.pl | 132 +++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 9 deletions(-) create mode 100755 scripts/kconfig/kconfig-sym-check.pl (limited to 'scripts') diff --git a/Makefile b/Makefile index 857f08dcc952..37fdb454b637 100644 --- a/Makefile +++ b/Makefile @@ -293,6 +293,7 @@ version_h := include/generated/uapi/linux/version.h clean-targets := %clean mrproper cleandocs no-dot-config-targets := $(clean-targets) \ cscope gtags TAGS tags help% %docs check% coccicheck \ + kconfig-sym-check \ $(version_h) headers headers_% archheaders archscripts \ %asm-generic kernelversion %src-pkg dt_binding_check \ outputmakefile rustavailable rustfmt rustfmtcheck \ @@ -1800,14 +1801,15 @@ help: echo ' (default: $(INSTALL_HDR_PATH))'; \ echo '' @echo 'Static analysers:' - @echo ' checkstack - Generate a list of stack hogs and consider all functions' - @echo ' with a stack size larger than MINSTACKSIZE (default: 100)' - @echo ' versioncheck - Sanity check on version.h usage' - @echo ' includecheck - Check for duplicate included header files' - @echo ' headerdep - Detect inclusion cycles in headers' - @echo ' coccicheck - Check with Coccinelle' - @echo ' clang-analyzer - Check with clang static analyzer' - @echo ' clang-tidy - Check with clang-tidy' + @echo ' checkstack - Generate a list of stack hogs and consider all functions' + @echo ' with a stack size larger than MINSTACKSIZE (default: 100)' + @echo ' versioncheck - Sanity check on version.h usage' + @echo ' includecheck - Check for duplicate included header files' + @echo ' headerdep - Detect inclusion cycles in headers' + @echo ' coccicheck - Check with Coccinelle' + @echo ' kconfig-sym-check - Check for dangling Kconfig symbol references' + @echo ' clang-analyzer - Check with clang static analyzer' + @echo ' clang-tidy - Check with clang-tidy' @echo '' @echo 'Tools:' @echo ' nsdeps - Generate missing symbol namespace dependencies' @@ -2227,7 +2229,7 @@ endif # Scripts to check various things for consistency # --------------------------------------------------------------------------- -PHONY += includecheck versioncheck coccicheck +PHONY += includecheck versioncheck coccicheck kconfig-sym-check includecheck: find $(srctree)/* $(RCS_FIND_IGNORE) \ @@ -2242,6 +2244,9 @@ versioncheck: coccicheck: $(Q)$(BASH) $(srctree)/scripts/$@ +kconfig-sym-check: + $(Q)$(PERL) $(srctree)/scripts/kconfig/kconfig-sym-check.pl $(srctree) $(KCONFIG_SYM_CHECK_EXCLUDES) + PHONY += checkstack kernelrelease kernelversion image_name # UML needs a little special treatment here. It wants to use the host diff --git a/scripts/kconfig/kconfig-sym-check.pl b/scripts/kconfig/kconfig-sym-check.pl new file mode 100755 index 000000000000..daa5285fdefc --- /dev/null +++ b/scripts/kconfig/kconfig-sym-check.pl @@ -0,0 +1,132 @@ +#!/usr/bin/env perl +# SPDX-License-Identifier: GPL-2.0 + +use warnings; +use strict; + +my $srctree = shift @ARGV; +unless (defined $srctree) { + $srctree = `git rev-parse --show-toplevel 2>/dev/null`; + chomp $srctree; + my $msg = "Usage: $0 [excludes file]\n"; + $msg .= "Please provide ."; + $msg .= " Is it '$srctree'?" if $srctree; + $msg .= "\n"; + die $msg; +} +my $kconfig_sym_check_excludes = defined $ARGV[0] ? $ARGV[0] : undef; + +sub indent_depth { + my ($ws) = @_; + my $col = 0; + for my $c (split //, $ws) { + $col = $c eq "\t" ? int($col / 8) * 8 + 8 : $col + 1; + } + return $col; +} + +my @files = `git -C \Q$srctree\E ls-files '*Kconfig*' 2>/dev/null`; +if (@files) { + chomp @files; + @files = map { "$srctree/$_" } @files; +} else { + @files = `find \Q$srctree\E -name '*Kconfig*'`; + chomp @files; +} + +@files = grep { !m{/scripts/kconfig/tests/} } @files; + +my %configs = (); +my %refs = (); + +foreach my $file (@files) { + open F, $file or die "Cannot open $file: $!"; + + my $help = 0; + my $help_level; + my $level; + + while () { + chomp; + + while (/\\\s*$/) { + s/\\\s*$/ /; + my $cont = // last; + chomp $cont; + $_ .= $cont; + } + + next if /^\s*$/; + next if /^\s*#/; + + /^(\s*)/; + $level = indent_depth($1); + + if ($help && $level < $help_level) { + $help = 0; + } + + next if ($help); + + if (/^\s*(help|\-\-\-help\-\-\-)$/) { + $help = 1; + my $next; + while (defined($next = )) { + last unless $next =~ /^\s*(?:#.*)?$/; + } + last unless defined $next; + $next =~ /^(\s*)/; + if (indent_depth($1) >= $level) { + $help_level = indent_depth($1); + } else { + $help = 0; + } + $_ = $next; + redo; + } + + if (/^\s*(config|menuconfig)\s+([a-zA-Z0-9_]+)\s*(#.*)?$/) { + $configs{$2}++; + next; + } + + if (/^\s*(default|def_bool|def_tristate|select|depends\s+on|imply|visible\s+if|range|if|bool|tristate|int|hex|string|prompt)\s+(.+)\s*$/) { + my $s = $2; + $s =~ s/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'//g; + $s =~ s/#.*//; + $s =~ s/\$\((?:[^()]*|\((?:[^()]*|\([^()]*\))*\))*\)//g; + $s =~ s/%%[^%]*%%//g; + my @syms = split /[^a-zA-Z0-9_]+/, $s; + map { + $refs{$_}++ if (/[a-zA-Z]/ && $_ ne "if" && $_ ne "y" && $_ ne "n" && $_ ne "m" && !/^0[xX][0-9a-fA-F]+$/); + } @syms + } + } + + close F; +} + +my %known_syms = (); +if (defined $kconfig_sym_check_excludes) { + my $file = $kconfig_sym_check_excludes; + open(F, "<", $file) or die "Cannot open $file: $!"; + while () { + chomp; + next if /^\s*$/; + next if /^\s*#/; + $known_syms{$1}++ if (/^\s*([a-zA-Z0-9_]+)\s*(#.*)?$/); + } +} + +my $ret = 0; +foreach my $k (sort keys %refs) { + next if (exists $configs{$k} || exists $known_syms{$k}); + + print "$k"; + print " - warning: '$k' is probably not what you want; Kconfig tristate literals are always lowercase ('n', 'y', 'm')" if ($k eq "N" || $k eq "Y" || $k eq "M"); + print "\n"; + + $ret = 1; +} + +exit $ret; -- cgit v1.2.3 From 7594302d9ddd9f198173a658250e7b7debc6ed76 Mon Sep 17 00:00:00 2001 From: Miguel Ojeda Date: Tue, 2 Jun 2026 17:16:38 +0200 Subject: kbuild: rust: rename flag to `-Zdebuginfo-for-profiling` for Rust >= 1.98 Starting with Rust 1.98.0 (expected 2026-08-20), the `-Zdebug-info-for-profiling` flag has been renamed to `-Zdebuginfo-for-profiling` (i.e. one less dash, to match `debuginfo`s in other flags) [1]. Without this change, one gets in the latest nightlies: error: unknown unstable option: `debug-info-for-profiling` Thus pass the right name. Link: https://github.com/rust-lang/rust/pull/156887 [1] Reviewed-by: Alice Ryhl Acked-by: Nathan Chancellor Link: https://patch.msgid.link/20260602151638.14358-1-ojeda@kernel.org Signed-off-by: Miguel Ojeda --- scripts/Makefile.autofdo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/Makefile.autofdo b/scripts/Makefile.autofdo index 3f08acab4549..1442043da139 100644 --- a/scripts/Makefile.autofdo +++ b/scripts/Makefile.autofdo @@ -3,7 +3,7 @@ # Enable available and selected Clang AutoFDO features. CFLAGS_AUTOFDO_CLANG := -fdebug-info-for-profiling -mllvm -enable-fs-discriminator=true -mllvm -improved-fs-discriminator=true -RUSTFLAGS_AUTOFDO_CLANG := -Zdebug-info-for-profiling -Cllvm-args=-enable-fs-discriminator=true -Cllvm-args=-improved-fs-discriminator=true +RUSTFLAGS_AUTOFDO_CLANG := $(if $(call rustc-min-version,109800),-Zdebuginfo-for-profiling,-Zdebug-info-for-profiling) -Cllvm-args=-enable-fs-discriminator=true -Cllvm-args=-improved-fs-discriminator=true ifndef CONFIG_DEBUG_INFO CFLAGS_AUTOFDO_CLANG += -gmlt -- cgit v1.2.3 From 65b09bfa8aa7ebe087093b591525385efb2d58b0 Mon Sep 17 00:00:00 2001 From: Zhou Yuhang Date: Wed, 20 May 2026 15:08:00 +0800 Subject: kconfig: Fix repeated include selftest expectation The err_repeated_inc test was added with an expected stderr fixture that does not match the diagnostic printed by kconfig. Running "make testconfig" currently fails in that test even though the parser reports the duplicated include correctly: [stderr] Kconfig.inc1:4: error: repeated inclusion of Kconfig.inc3 Kconfig.inc2:3: note: location of first inclusion of Kconfig.inc3 The fixture expects "Repeated" and "Location" with capital letters, but the diagnostic emitted by scripts/kconfig/util.c uses lowercase words. Update the fixture to match the real message. Fixes: 102d712ded3e ("kconfig: Error out on duplicated kconfig inclusion") Signed-off-by: Zhou Yuhang Tested-by: Nicolas Schier Reviewed-by: Nathan Chancellor Link: https://patch.msgid.link/20260520070800.2265479-1-zhouyuhang1010@163.com Signed-off-by: Nicolas Schier --- scripts/kconfig/tests/err_repeated_inc/expected_stderr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/kconfig/tests/err_repeated_inc/expected_stderr b/scripts/kconfig/tests/err_repeated_inc/expected_stderr index 95d90d6a93c5..53071430ea7d 100644 --- a/scripts/kconfig/tests/err_repeated_inc/expected_stderr +++ b/scripts/kconfig/tests/err_repeated_inc/expected_stderr @@ -1,2 +1,2 @@ -Kconfig.inc1:4: error: Repeated inclusion of Kconfig.inc3 -Kconfig.inc2:3: note: Location of first inclusion of Kconfig.inc3 +Kconfig.inc1:4: error: repeated inclusion of Kconfig.inc3 +Kconfig.inc2:3: note: location of first inclusion of Kconfig.inc3 -- cgit v1.2.3 From 29c52907334a8e50ba1ee5fbaa53407dec28d876 Mon Sep 17 00:00:00 2001 From: James Lee Date: Thu, 4 Jun 2026 14:03:00 +0800 Subject: modpost: Add __llvm_covfun and __llvm_covmap to section_white_list Modpost emits hundreds of warnings when using Clang to build for ARCH=um and CONFIG_GCOV=y. e.g.: vmlinux (__llvm_covfun): unexpected non-allocatable section. Did you forget to use "ax"/"aw" in a .S file? Note that for example contains section definitions for use in .S files. For example, when we use LLVM for a kunit user mode build with coverage: python3 tools/testing/kunit/kunit.py build --make_options LLVM=1 \ --kunitconfig=tools/testing/kunit/configs/default.config \ --kunitconfig=tools/testing/kunit/configs/coverage_uml.config The behaviour occurs when building the kernel for ARCH=um with code coverage enabled. The warnings come from modpost's check_sec_ref function, which ensures no sections reference others that will be discarded. covfun and covmap sections must reference __init and __exit sections to collect coverage data, triggering the modpost warning. To suppress these warnings, these section names have been added to modpost's whitelist. This is unlikely to suppress legitimate warnings as Clang will only insert these sections when building with coverage, and can be assumed to manage these references safely. Signed-off-by: James Lee Link: https://patch.msgid.link/20260604-dev-coverage-patch-v1-1-9f9368253cb4@codeconstruct.com.au Signed-off-by: Nathan Chancellor --- scripts/mod/modpost.c | 2 ++ 1 file changed, 2 insertions(+) (limited to 'scripts') diff --git a/scripts/mod/modpost.c b/scripts/mod/modpost.c index da0f4bc75509..d592548cbd60 100644 --- a/scripts/mod/modpost.c +++ b/scripts/mod/modpost.c @@ -765,6 +765,8 @@ static const char *const section_white_list[] = ".gnu.lto*", ".discard.*", ".llvm.call-graph-profile", /* call graph */ + "__llvm_covfun", + "__llvm_covmap", NULL }; -- cgit v1.2.3 From 8b1549eee9c7f27b4f4f7c7f575c3c50065b89ff Mon Sep 17 00:00:00 2001 From: Miguel Ojeda Date: Mon, 8 Jun 2026 16:14:20 +0200 Subject: scripts: generate_rust_analyzer: support passing env vars A future commit adding `zerocopy` support will need to pass an environment variable during its build. Thus add support for an `--envs` parameter, similar to `--cfgs`, that allows to pass a map of variables to set for a given crate. This allows us to keep a single source of truth for those values. No change intended in the generated `rust-project.json`. Acked-by: Tamir Duberstein Link: https://patch.msgid.link/20260608141439.182634-2-ojeda@kernel.org Signed-off-by: Miguel Ojeda --- scripts/generate_rust_analyzer.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) (limited to 'scripts') diff --git a/scripts/generate_rust_analyzer.py b/scripts/generate_rust_analyzer.py index d5f9a0ca742c..2477209e1e94 100755 --- a/scripts/generate_rust_analyzer.py +++ b/scripts/generate_rust_analyzer.py @@ -26,6 +26,14 @@ def args_crates_cfgs(cfgs: List[str]) -> Dict[str, List[str]]: return crates_cfgs +def args_crates_envs(envs: List[str]) -> Dict[str, Dict[str, str]]: + crates_envs = {} + for env in envs: + crate, vals = env.split("=", 1) + crates_envs[crate] = dict(v.split("=", 1) for v in vals.split()) + + return crates_envs + class Dependency(TypedDict): crate: int name: str @@ -61,6 +69,7 @@ def generate_crates( sysroot_src: pathlib.Path, external_src: Optional[pathlib.Path], cfgs: List[str], + envs: List[str], core_edition: str, ) -> List[Crate]: # Generate the configuration list. @@ -74,6 +83,7 @@ def generate_crates( # Now fill the crates list. crates: List[Crate] = [] crates_cfgs = args_crates_cfgs(cfgs) + crates_envs = args_crates_envs(envs) def get_crate_name(path: pathlib.Path) -> str: return invoke_rustc(["--print", "crate-name", str(path)]) @@ -92,6 +102,10 @@ def generate_crates( is_workspace_member if is_workspace_member is not None else True ) edition = edition if edition is not None else "2021" + crate_env = { + "RUST_MODFILE": "This is only for rust-analyzer", + **crates_envs.get(display_name, {}), + } return { "display_name": display_name, "root_module": str(root_module), @@ -99,9 +113,7 @@ def generate_crates( "deps": deps, "cfg": cfg, "edition": edition, - "env": { - "RUST_MODFILE": "This is only for rust-analyzer" - } + "env": crate_env, } def append_proc_macro_crate( @@ -347,6 +359,7 @@ def main() -> None: parser = argparse.ArgumentParser() parser.add_argument('--verbose', '-v', action='store_true') parser.add_argument('--cfgs', action='append', default=[]) + parser.add_argument('--envs', action='append', default=[]) parser.add_argument("core_edition") parser.add_argument("srctree", type=pathlib.Path) parser.add_argument("objtree", type=pathlib.Path) @@ -357,6 +370,7 @@ def main() -> None: class Args(argparse.Namespace): verbose: bool cfgs: List[str] + envs: List[str] srctree: pathlib.Path objtree: pathlib.Path sysroot: pathlib.Path @@ -372,7 +386,7 @@ def main() -> None: ) rust_project = { - "crates": generate_crates(args.srctree, args.objtree, args.sysroot_src, args.exttree, args.cfgs, args.core_edition), + "crates": generate_crates(args.srctree, args.objtree, args.sysroot_src, args.exttree, args.cfgs, args.envs, args.core_edition), "sysroot": str(args.sysroot), } -- cgit v1.2.3 From 567621523ab7a2bc4a923757cba30bde8900447a Mon Sep 17 00:00:00 2001 From: Miguel Ojeda Date: Mon, 8 Jun 2026 16:14:31 +0200 Subject: rust: zerocopy: enable support in kbuild With all the new files in place and ready from the new crate, enable the support for it in the build system. In addition, skip formatting for this vendored crate. Finally, there are no generated symbols expected from `zerocopy`, thus skip adding the `exports` generation. Link: https://patch.msgid.link/20260608141439.182634-13-ojeda@kernel.org Signed-off-by: Miguel Ojeda --- Makefile | 1 + rust/Makefile | 45 +++++++++++++++++++++++++++++++++------ scripts/Makefile.build | 1 + scripts/generate_rust_analyzer.py | 10 +++++++-- 4 files changed, 49 insertions(+), 8 deletions(-) (limited to 'scripts') diff --git a/Makefile b/Makefile index 3a265e7e3347..33135f39bbee 100644 --- a/Makefile +++ b/Makefile @@ -1956,6 +1956,7 @@ rustfmt: -path $(srctree)/rust/proc-macro2 \ -o -path $(srctree)/rust/quote \ -o -path $(srctree)/rust/syn \ + -o -path $(srctree)/rust/zerocopy \ \) -prune -o \ -type f -a -name '*.rs' -a ! -name '*generated*' -print \ | xargs $(RUSTFMT) $(rustfmt_flags) diff --git a/rust/Makefile b/rust/Makefile index 7b1db0d5d423..a87b33926cdf 100644 --- a/rust/Makefile +++ b/rust/Makefile @@ -6,6 +6,8 @@ rustdoc_output := $(objtree)/Documentation/output/rust/rustdoc obj-$(CONFIG_RUST) += core.o compiler_builtins.o ffi.o always-$(CONFIG_RUST) += exports_core_generated.h +obj-$(CONFIG_RUST) += zerocopy.o + ifdef CONFIG_RUST_INLINE_HELPERS always-$(CONFIG_RUST) += helpers/helpers.bc helpers/helpers_module.bc else @@ -81,6 +83,12 @@ core-flags := \ --edition=$(core-edition) \ $(call cfgs-to-flags,$(core-cfgs)) +zerocopy-flags := \ + --cap-lints=allow + +zerocopy-envs := \ + CARGO_PKG_VERSION=0.8.50 + proc_macro2-cfgs := \ feature="proc-macro" \ wrap_proc_macro \ @@ -167,7 +175,7 @@ quiet_cmd_rustdoc = RUSTDOC $(if $(rustdoc_host),H, ) $< # command-like flags to solve the issue. Meanwhile, we use the non-custom case # and then retouch the generated files. rustdoc: rustdoc-core rustdoc-macros rustdoc-compiler_builtins \ - rustdoc-kernel rustdoc-pin_init + rustdoc-kernel rustdoc-pin_init rustdoc-zerocopy $(Q)grep -Ehro ' rust-project.json @@ -670,6 +694,13 @@ $(obj)/compiler_builtins.o: private rustc_objcopy = -w -W '__*' $(obj)/compiler_builtins.o: $(src)/compiler_builtins.rs $(obj)/core.o FORCE +$(call if_changed_rule,rustc_library) +$(obj)/zerocopy.o: private skip_clippy = 1 +$(obj)/zerocopy.o: private skip_gendwarfksyms = 1 +$(obj)/zerocopy.o: private rustc_target_envs := $(zerocopy-envs) +$(obj)/zerocopy.o: private rustc_target_flags = $(zerocopy-flags) +$(obj)/zerocopy.o: $(src)/zerocopy/src/lib.rs $(obj)/compiler_builtins.o FORCE + +$(call if_changed_rule,rustc_library) + $(obj)/pin_init.o: private skip_gendwarfksyms = 1 $(obj)/pin_init.o: private rustc_target_flags = $(pin_init-flags) $(obj)/pin_init.o: $(src)/pin-init/src/lib.rs $(obj)/compiler_builtins.o \ @@ -705,9 +736,11 @@ $(obj)/uapi.o: $(src)/uapi/lib.rs \ +$(call if_changed_rule,rustc_library) $(obj)/kernel.o: private rustc_target_flags = --extern ffi --extern pin_init \ - --extern build_error --extern macros --extern bindings --extern uapi + --extern build_error --extern macros --extern bindings --extern uapi \ + --extern zerocopy $(obj)/kernel.o: $(src)/kernel/lib.rs $(obj)/build_error.o $(obj)/pin_init.o \ - $(obj)/$(libmacros_name) $(obj)/bindings.o $(obj)/uapi.o FORCE + $(obj)/$(libmacros_name) $(obj)/bindings.o $(obj)/uapi.o \ + $(obj)/zerocopy.o FORCE +$(call if_changed_rule,rustc_library) ifdef CONFIG_JUMP_LABEL diff --git a/scripts/Makefile.build b/scripts/Makefile.build index 3498d25b15e8..ddf0461dda6a 100644 --- a/scripts/Makefile.build +++ b/scripts/Makefile.build @@ -329,6 +329,7 @@ rust_common_cmd = \ -Zcrate-attr=no_std \ -Zcrate-attr='feature($(rust_allowed_features))' \ -Zunstable-options --extern pin_init --extern kernel \ + --extern zerocopy \ --crate-type rlib -L $(objtree)/rust/ \ --sysroot=/dev/null \ --out-dir $(dir $@) --emit=dep-info=$(depfile) diff --git a/scripts/generate_rust_analyzer.py b/scripts/generate_rust_analyzer.py index 2477209e1e94..80f7647f633a 100755 --- a/scripts/generate_rust_analyzer.py +++ b/scripts/generate_rust_analyzer.py @@ -276,6 +276,12 @@ def generate_crates( [core, compiler_builtins], ) + zerocopy = append_crate( + "zerocopy", + srctree / "rust" / "zerocopy" / "src" / "lib.rs", + [core, compiler_builtins], + ) + def append_crate_with_generated( display_name: str, deps: List[Dependency], @@ -304,7 +310,7 @@ def generate_crates( bindings = append_crate_with_generated("bindings", [core, ffi, pin_init]) uapi = append_crate_with_generated("uapi", [core, ffi, pin_init]) kernel = append_crate_with_generated( - "kernel", [core, macros, build_error, pin_init, ffi, bindings, uapi] + "kernel", [core, macros, build_error, pin_init, ffi, bindings, uapi, zerocopy] ) scripts = srctree / "scripts" @@ -349,7 +355,7 @@ def generate_crates( append_crate( crate_name, path, - [core, kernel, pin_init], + [core, kernel, pin_init, zerocopy], cfg=generated_cfg, ) -- cgit v1.2.3 From 506054980429497d106261568bd39c22a40e0fdd Mon Sep 17 00:00:00 2001 From: Miguel Ojeda Date: Mon, 8 Jun 2026 16:14:36 +0200 Subject: rust: zerocopy-derive: enable support in kbuild With all the new files in place and ready from the new crate, enable the support for it in the build system. In addition, skip formatting for this vendored crate. Link: https://patch.msgid.link/20260608141439.182634-18-ojeda@kernel.org Signed-off-by: Miguel Ojeda --- Makefile | 1 + rust/Makefile | 41 +++++++++++++++++++++++++++++++-------- scripts/Makefile.build | 2 +- scripts/generate_rust_analyzer.py | 10 ++++++++-- 4 files changed, 43 insertions(+), 11 deletions(-) (limited to 'scripts') diff --git a/Makefile b/Makefile index 33135f39bbee..c71c43bc3658 100644 --- a/Makefile +++ b/Makefile @@ -1957,6 +1957,7 @@ rustfmt: -o -path $(srctree)/rust/quote \ -o -path $(srctree)/rust/syn \ -o -path $(srctree)/rust/zerocopy \ + -o -path $(srctree)/rust/zerocopy-derive \ \) -prune -o \ -type f -a -name '*.rs' -a ! -name '*generated*' -print \ | xargs $(RUSTFMT) $(rustfmt_flags) diff --git a/rust/Makefile b/rust/Makefile index a87b33926cdf..2fbdebb93bf2 100644 --- a/rust/Makefile +++ b/rust/Makefile @@ -52,10 +52,11 @@ ifdef CONFIG_RUST procmacro-name = $(shell MAKEFLAGS= $(RUSTC) --print file-names --crate-name $(1) --crate-type proc-macro - Date: Mon, 8 Jun 2026 19:17:10 -0700 Subject: kconfig: tests: fix typo in comment scripts/kconfig/tests/no_write_if_dep_unmet/__init__.py contains a typo "COFIG_" for "CONFIG_". Fix it. Discovered while searching for typos in CONFIG_* variable references. Signed-off-by: Ethan Nelson-Moore Link: https://patch.msgid.link/20260609021712.7965-1-enelsonmoore@gmail.com Signed-off-by: Nathan Chancellor --- scripts/kconfig/tests/no_write_if_dep_unmet/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/kconfig/tests/no_write_if_dep_unmet/__init__.py b/scripts/kconfig/tests/no_write_if_dep_unmet/__init__.py index ffd469d1f226..791ed659c76b 100644 --- a/scripts/kconfig/tests/no_write_if_dep_unmet/__init__.py +++ b/scripts/kconfig/tests/no_write_if_dep_unmet/__init__.py @@ -8,7 +8,7 @@ for symbols with unmet dependency. This was not working correctly for choice values because choice needs a bit different symbol computation. -This checks that no unneeded "# COFIG_... is not set" is contained in +This checks that no unneeded "# CONFIG_... is not set" is contained in the .config file. Related Linux commit: cb67ab2cd2b8abd9650292c986c79901e3073a59 -- cgit v1.2.3 From 3f70ebe638581e73c8df6b3fa7eed9b45d90b083 Mon Sep 17 00:00:00 2001 From: Jan Polensky Date: Mon, 1 Jun 2026 19:46:25 +0200 Subject: s390: Enable Rust support Enable building Rust code on s390 by wiring the architecture into the kernel Rust infrastructure. Add s390 to the Rust arch support documentation, provide the s390 Rust target and required compiler flags, and set the bindgen target for arch/s390. Adjust the Rust target generation and minimum rustc version gating so the s390 setup is handled explicitly. The Rust toolchain uses the "s390x" triple naming for the 64 bit target. Rust support is currently incompatible with CONFIG_EXPOLINE, which relies on compiler support for the -mindirect-branch= and -mfunction_return= options. Therefore, select HAVE_RUST only when EXPOLINE is disabled. Acked-by: Miguel Ojeda Acked-by: Gary Guo Acked-by: Heiko Carstens Signed-off-by: Jan Polensky Signed-off-by: Alexander Gordeev --- Documentation/rust/arch-support.rst | 1 + arch/s390/Kconfig | 1 + arch/s390/Makefile | 28 +++++++++++++++++----------- rust/Makefile | 1 + scripts/generate_rust_target.rs | 2 ++ scripts/min-tool-version.sh | 6 +++++- 6 files changed, 27 insertions(+), 12 deletions(-) (limited to 'scripts') diff --git a/Documentation/rust/arch-support.rst b/Documentation/rust/arch-support.rst index 6e6a515d0899..4f980815e92a 100644 --- a/Documentation/rust/arch-support.rst +++ b/Documentation/rust/arch-support.rst @@ -19,6 +19,7 @@ Architecture Level of support Constraints ``arm64`` Maintained Little Endian only. ``loongarch`` Maintained \- ``riscv`` Maintained ``riscv64`` and LLVM/Clang only. +``s390`` Maintained ``CONFIG_EXPOLINE`` must be disabled. ``um`` Maintained \- ``x86`` Maintained ``x86_64`` only. ============= ================ ============================================== diff --git a/arch/s390/Kconfig b/arch/s390/Kconfig index ecbcbb781e40..26951781d74d 100644 --- a/arch/s390/Kconfig +++ b/arch/s390/Kconfig @@ -248,6 +248,7 @@ config S390 select HAVE_RELIABLE_STACKTRACE select HAVE_RETHOOK select HAVE_RSEQ + select HAVE_RUST if !EXPOLINE select HAVE_SAMPLE_FTRACE_DIRECT select HAVE_SAMPLE_FTRACE_DIRECT_MULTI select HAVE_SETUP_PER_CPU_AREA diff --git a/arch/s390/Makefile b/arch/s390/Makefile index 297976b41088..8b712cd85fcd 100644 --- a/arch/s390/Makefile +++ b/arch/s390/Makefile @@ -35,25 +35,31 @@ KBUILD_CFLAGS_DECOMPRESSOR += $(if $(CONFIG_DEBUG_INFO),-g) KBUILD_CFLAGS_DECOMPRESSOR += $(if $(CONFIG_DEBUG_INFO_DWARF4), $(call cc-option, -gdwarf-4,)) KBUILD_CFLAGS_DECOMPRESSOR += $(if $(CONFIG_CC_NO_ARRAY_BOUNDS),-Wno-array-bounds) +KBUILD_RUSTFLAGS += --target=s390x-unknown-none-softfloat -Zpacked-stack -Ctarget-feature=+backchain + UTS_MACHINE := s390x STACK_SIZE := $(if $(CONFIG_KASAN),65536,$(if $(CONFIG_KMSAN),65536,16384)) CHECKFLAGS += -D__s390__ -D__s390x__ export LD_BFD -mflags-$(CONFIG_MARCH_Z10) := -march=z10 -mflags-$(CONFIG_MARCH_Z196) := -march=z196 -mflags-$(CONFIG_MARCH_ZEC12) := -march=zEC12 -mflags-$(CONFIG_MARCH_Z13) := -march=z13 -mflags-$(CONFIG_MARCH_Z14) := -march=z14 -mflags-$(CONFIG_MARCH_Z15) := -march=z15 -mflags-$(CONFIG_MARCH_Z16) := -march=z16 -mflags-$(CONFIG_MARCH_Z17) := -march=z17 +march-name-$(CONFIG_MARCH_Z10) := z10 +march-name-$(CONFIG_MARCH_Z196) := z196 +march-name-$(CONFIG_MARCH_ZEC12) := zEC12 +march-name-$(CONFIG_MARCH_Z13) := z13 +march-name-$(CONFIG_MARCH_Z14) := z14 +march-name-$(CONFIG_MARCH_Z15) := z15 +march-name-$(CONFIG_MARCH_Z16) := z16 +march-name-$(CONFIG_MARCH_Z17) := z17 + +mflags := -march=$(march-name-y) + +export CC_FLAGS_MARCH := $(mflags) -export CC_FLAGS_MARCH := $(mflags-y) +aflags-y += $(mflags) +cflags-y += $(mflags) -aflags-y += $(mflags-y) -cflags-y += $(mflags-y) +KBUILD_RUSTFLAGS += -Ctarget-cpu=$(march-name-y) cflags-$(CONFIG_MARCH_Z10_TUNE) += -mtune=z10 cflags-$(CONFIG_MARCH_Z196_TUNE) += -mtune=z196 diff --git a/rust/Makefile b/rust/Makefile index b9e9f512cec3..77460502f576 100644 --- a/rust/Makefile +++ b/rust/Makefile @@ -403,6 +403,7 @@ BINDGEN_TARGET_x86 := x86_64-linux-gnu BINDGEN_TARGET_arm64 := aarch64-linux-gnu BINDGEN_TARGET_arm := arm-linux-gnueabi BINDGEN_TARGET_loongarch := loongarch64-linux-gnusf +BINDGEN_TARGET_s390 := s390x-linux-gnu # This is only for i386 UM builds, which need the 32-bit target not -m32 BINDGEN_TARGET_i386 := i386-linux-gnu BINDGEN_TARGET_um := $(BINDGEN_TARGET_$(SUBARCH)) diff --git a/scripts/generate_rust_target.rs b/scripts/generate_rust_target.rs index 38b3416bb979..8f1df6819d0b 100644 --- a/scripts/generate_rust_target.rs +++ b/scripts/generate_rust_target.rs @@ -256,6 +256,8 @@ fn main() { } } else if cfg.has("LOONGARCH") { panic!("loongarch uses the builtin rustc loongarch64-unknown-none-softfloat target"); + } else if cfg.has("S390") { + panic!("s390 uses the builtin rustc s390x-unknown-none-softfloat target"); } else { panic!("Unsupported architecture"); } diff --git a/scripts/min-tool-version.sh b/scripts/min-tool-version.sh index b96ec2d379b6..296acf8f71aa 100755 --- a/scripts/min-tool-version.sh +++ b/scripts/min-tool-version.sh @@ -31,7 +31,11 @@ llvm) fi ;; rustc) - echo 1.85.0 + if [ "$SRCARCH" = "s390" ]; then + echo 1.96.0 + else + echo 1.85.0 + fi ;; bindgen) echo 0.71.1 -- cgit v1.2.3 From 1e8a9af95a4691fbe4b576114e2998247a00872e Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Wed, 27 May 2026 20:32:10 +0100 Subject: dt-bindings: add DTS style checker Add a Python tool that checks DTS coding style on examples in YAML binding files and on .dts/.dtsi/.dtso source files. Rules are kept in a small declarative registry, each tagged 'relaxed' (default; must be zero-violation on the current tree) or 'strict' (opt-in for new submissions). Promoting a rule from strict to relaxed is a one-line edit once the tree is clean. Relaxed mode covers trailing whitespace, tab characters in YAML examples, mixed tab+space indents, and missing tabs in .dts files. Strict adds indent unit and consistency checks, blank-line placement, sibling address ordering, "compatible" and "reg" ordering, and unused labels. The tool reads file paths from @argfile and parallelises across CPUs via -j N. With no -j given it picks up $PARALLELISM (set by scripts/jobserver-exec from the GNU make jobserver) and falls back to os.cpu_count() otherwise. Running as one Python invocation amortises the ruamel.yaml import across the whole tree -- ~2s on a 32-CPU host vs ~28s sequential. Signed-off-by: Daniel Golle Reviewed-by: Krzysztof Kozlowski Link: https://patch.msgid.link/224923f3d1c73ff55cebb3e0796f119e32c1bb43.1779908995.git.daniel@makrotopia.org Signed-off-by: Rob Herring (Arm) --- scripts/dtc/dt-check-style | 1192 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1192 insertions(+) create mode 100755 scripts/dtc/dt-check-style (limited to 'scripts') diff --git a/scripts/dtc/dt-check-style b/scripts/dtc/dt-check-style new file mode 100755 index 000000000000..2d5723d41ea3 --- /dev/null +++ b/scripts/dtc/dt-check-style @@ -0,0 +1,1192 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +# +# Check DTS coding style on YAML binding examples and on +# .dts/.dtsi/.dtso source files. Enforces rules from +# Documentation/devicetree/bindings/dts-coding-style.rst. +# +# Two modes: +# --mode=relaxed (default) +# Only rules that produce zero warnings on the current tree. +# Suitable for dt_binding_check. +# --mode=strict +# All rules. Required for new submissions. +# +# Two input types (auto-detected by file extension): +# *.yaml -- DT binding; check each example block +# *.dts/*.dtsi/*.dtso -- DTS source; whole file is one block +# +# Rules are declared in a registry (see RULES below); each rule is +# tagged with the lowest mode that runs it. Promoting a rule from +# 'strict' to 'relaxed' is a one-line change. + +import argparse +import re +import sys +from enum import Enum, auto + +import ruamel.yaml + + +# --------------------------------------------------------------------------- +# Line classification +# --------------------------------------------------------------------------- + +class LineType(Enum): + BLANK = auto() + COMMENT = auto() # // ... or /* ... */ on one line + COMMENT_START = auto() # /* without closing */ + COMMENT_BODY = auto() # inside a multi-line comment + COMMENT_END = auto() # closing */ + PREPROCESSOR = auto() # #include / #define / #ifdef / ... + NODE_OPEN = auto() # something { (with optional label/name/addr) + NODE_CLOSE = auto() # }; + PROPERTY = auto() # name = value; or name; + CONTINUATION = auto() # continuation of a multi-line property + + +re_cpp_directive = re.compile( + r'^#\s*(include|define|undef|ifdef|ifndef|if|else|elif|endif|' + r'pragma|error|warning)\b') + +# label: name@addr { -- label and addr optional; name can be "/" +# Per the DT spec a node name may start with a digit (e.g. 1wire@...). +# The address part is captured loosely (any non-space, non-brace run) so +# malformed addresses (e.g. memory@0x1000) still reach +# check_unit_address_format() instead of silently bypassing the check. +re_node_header = re.compile( + r'^(?:([a-zA-Z_][a-zA-Z0-9_]*):\s*)?' + r'([a-zA-Z0-9][a-zA-Z0-9,._+-]*|/)' + r'(?:@([^\s{]+))?' + r'\s*\{$') + +re_ref_node = re.compile( + r'^&([a-zA-Z_][a-zA-Z0-9_]*)\s*\{$') + + +def is_preprocessor(stripped): + """Tell C preprocessor directives apart from DTS '#'-prefixed props.""" + return re_cpp_directive.match(stripped) is not None + + +class DtsLine: + __slots__ = ('lineno', 'raw', 'linetype', 'indent_str', 'stripped', + 'prop_name', 'continuations', + 'node_name', 'node_addr', 'label', 'ref_name', 'depth', + 'closures') + + def __init__(self, lineno, raw, linetype, indent_str, stripped): + self.lineno = lineno # 1-based within the block + self.raw = raw + self.linetype = linetype + self.indent_str = indent_str # leading whitespace as-is + self.stripped = stripped + self.prop_name = None + self.continuations = [] + self.node_name = None + self.node_addr = None + self.label = None + self.ref_name = None + self.depth = 0 # filled in by classify_lines + self.closures = 1 # count of '}' on a NODE_CLOSE line + + +def _split_code(text): + """Return (code, opens_block) for a leading-stripped line: the + code portion with // and /* */ comments removed (string literals + kept verbatim), and whether a /* */ block comment is left open. + The code portion is right-stripped so the endswith() checks in + classify_lines see code only, not a trailing comment or blanks.""" + out = [] + i = 0 + n = len(text) + while i < n: + c = text[i] + if c == '"': + j = i + 1 + while j < n: + if text[j] == '\\': + j += 2 + continue + if text[j] == '"': + j += 1 + break + j += 1 + out.append(text[i:j]) + i = j + continue + if c == '/' and i + 1 < n and text[i + 1] == '/': + break + if c == '/' and i + 1 < n and text[i + 1] == '*': + end = text.find('*/', i + 2) + if end < 0: + return (''.join(out).rstrip(), True) + i = end + 2 + continue + out.append(c) + i += 1 + return (''.join(out).rstrip(), False) + + +re_only_closures = re.compile(r'(?:\}\s*;?\s*)+$') + + +def classify_lines(text): + """Return a list of DtsLine. Tracks { } depth and groups + continuation lines onto their leading PROPERTY line.""" + out = [] + in_block_comment = False + in_cpp_macro = False + prev_complete = True + depth = 0 + + # Split preserving the indent string verbatim + re_lead = re.compile(r'^([ \t]*)(.*)$') + + for i, raw in enumerate(text.split('\n'), start=1): + m = re_lead.match(raw) + indent_str = m.group(1) + stripped = m.group(2) + + # Continuation of a multi-line C preprocessor directive: the + # previous PREPROCESSOR line ended with a '\\' line splice, so + # this line is part of the same macro. Treat it as + # PREPROCESSOR until the splice chain ends (no trailing '\\' + # or a blank line). + if in_cpp_macro: + dl = DtsLine(i, raw, LineType.PREPROCESSOR, + indent_str, stripped) + dl.depth = depth + out.append(dl) + in_cpp_macro = (bool(stripped) and + stripped.rstrip().endswith('\\')) + continue + + if not stripped: + dl = DtsLine(i, raw, LineType.BLANK, '', '') + dl.depth = depth + out.append(dl) + continue + + if in_block_comment: + ltype = (LineType.COMMENT_END if '*/' in stripped + else LineType.COMMENT_BODY) + if ltype == LineType.COMMENT_END: + in_block_comment = False + dl = DtsLine(i, raw, ltype, indent_str, stripped) + dl.depth = depth + out.append(dl) + continue + + if stripped.startswith('#') and is_preprocessor(stripped): + dl = DtsLine(i, raw, LineType.PREPROCESSOR, + indent_str, stripped) + dl.depth = depth + out.append(dl) + prev_complete = True + in_cpp_macro = stripped.rstrip().endswith('\\') + continue + + # Strip comments first so all later structural checks see code + # only. An unclosed /* sets in_block_comment for the next line. + code, opens_block = _split_code(stripped) + if opens_block: + in_block_comment = True + + # Pure-comment line: nothing left after stripping. Classify as + # COMMENT_START (carries to next line) or COMMENT, and skip the + # structural classification entirely. + if not code: + ltype = LineType.COMMENT_START if opens_block else LineType.COMMENT + dl = DtsLine(i, raw, ltype, indent_str, stripped) + dl.depth = depth + out.append(dl) + continue + + if not prev_complete: + dl = DtsLine(i, raw, LineType.CONTINUATION, indent_str, code) + dl.depth = depth + out.append(dl) + prev_complete = (code.endswith(';') or + code.endswith('{') or + code.endswith('};')) + continue + + # NODE_CLOSE: the canonical form is "}" or "};" alone. A line + # that is nothing but closures (e.g. "}; };") is still treated + # as NODE_CLOSE for depth tracking, but the multi-closure case + # is flagged separately by check_node_close_alone via + # dl.closures. + if re_only_closures.match(code): + closures = code.count('}') + depth = max(depth - closures, 0) + dl = DtsLine(i, raw, LineType.NODE_CLOSE, indent_str, code) + dl.depth = depth + dl.closures = closures + out.append(dl) + prev_complete = True + continue + + if code.endswith('{'): + dl = DtsLine(i, raw, LineType.NODE_OPEN, indent_str, code) + parse_node_header(dl) + dl.depth = depth + out.append(dl) + depth += 1 + prev_complete = True + continue + + # Property (or first line of a multi-line property). + dl = DtsLine(i, raw, LineType.PROPERTY, indent_str, code) + parse_property_name(dl) + dl.depth = depth + out.append(dl) + prev_complete = code.endswith(';') + + # Group continuation lines onto their leading PROPERTY. + last_prop = None + grouped = [] + for dl in out: + if dl.linetype == LineType.CONTINUATION and last_prop is not None: + last_prop.continuations.append(dl) + continue + if dl.linetype == LineType.PROPERTY: + last_prop = dl + elif dl.linetype != LineType.BLANK and \ + dl.linetype not in (LineType.COMMENT, LineType.COMMENT_BODY, + LineType.COMMENT_END, + LineType.COMMENT_START): + last_prop = None + grouped.append(dl) + return grouped + + +def parse_node_header(dl): + m = re_node_header.match(dl.stripped) + if m: + dl.label = m.group(1) + dl.node_name = m.group(2) + dl.node_addr = m.group(3) + return + m = re_ref_node.match(dl.stripped) + if m: + dl.ref_name = m.group(1) + + +def parse_property_name(dl): + m = re.match(r'^([a-zA-Z0-9#][a-zA-Z0-9,._+#-]*)\s*[=;]', dl.stripped) + if m: + dl.prop_name = m.group(1) + + +def collect_labels_and_refs(text): + """Return (defined_labels, referenced_labels) found anywhere outside + /* */ comments and string literals. Labels named fake_intc* (injected + by dt-extract-example) are skipped.""" + # Strip block comments first so labels inside them don't count + stripped = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL) + # Strip line comments + stripped = re.sub(r'//[^\n]*', '', stripped) + # Strip string literals so words inside quotes (e.g. "Error: foo") + # are not picked up as label definitions or &-references. + stripped = re.sub(r'"(?:[^"\\]|\\.)*"', '""', stripped) + defined = set() + referenced = set() + # A label precedes a node header; the next non-space token may start + # with a letter (foo, &ref), a digit (1wire), or '/' (root node). + for m in re.finditer( + r'(?:^|[\s{])([a-zA-Z_][a-zA-Z0-9_]*):\s*[a-zA-Z0-9/&]', + stripped): + name = m.group(1) + if not name.startswith('fake_intc'): + defined.add(name) + for m in re.finditer(r'&([a-zA-Z_][a-zA-Z0-9_]*)', stripped): + referenced.add(m.group(1)) + return defined, referenced + + +# --------------------------------------------------------------------------- +# Rule registry +# --------------------------------------------------------------------------- + +class Ctx: + """Context passed to each rule check. Carries the parsed lines, + raw text, mode, and indent kind.""" + + def __init__(self, lines, text, mode, indent_kind): + self.lines = lines + self.text = text + self.mode = mode # 'relaxed' or 'strict' + self.indent_kind = indent_kind # 'spaces' or 'tab' + + +class Rule: + __slots__ = ('name', 'mode', 'description', 'check', 'applies_to') + + def __init__(self, name, mode, description, check, + applies_to=('yaml', 'dts', 'dtsi', 'dtso')): + self.name = name + self.mode = mode # 'relaxed' or 'strict' + self.description = description + self.check = check + self.applies_to = applies_to # input types this rule covers + + +# --- individual rule check functions -------------------------------------- + +def check_trailing_whitespace(ctx): + for dl in ctx.lines: + if dl.raw != dl.raw.rstrip(): + yield (dl.lineno, 'trailing whitespace') + + +def check_tab_in_dts(ctx): + """Reject literal tabs in DTS lines when input is YAML. + + For YAML examples, indent and content must use spaces. Tabs inside + a #define value are tolerated (those are CPP macros, not DTS). + For .dts files, this rule does not apply -- tabs are required. + """ + if ctx.indent_kind != 'spaces': + return + for dl in ctx.lines: + if dl.linetype == LineType.PREPROCESSOR: + continue + if dl.linetype == LineType.BLANK: + continue + if '\t' in dl.raw: + yield (dl.lineno, 'tab character not allowed in DTS example') + + +def check_mixed_indent_chars(ctx): + """Indent must be all-spaces or all-tabs, never mixed on one line.""" + for dl in ctx.lines: + if not dl.indent_str: + continue + if dl.linetype == LineType.PREPROCESSOR: + continue + if ' ' in dl.indent_str and '\t' in dl.indent_str: + yield (dl.lineno, 'mixed tabs and spaces in indent') + + +def detect_indent_unit(ctx): + """Find the indent unit used at depth 1 in this block. + + Returns one of: ' ' (2 spaces), ' ' (4 spaces), '\\t' (tab), + or None if depth-1 is empty or ambiguous.""" + for dl in ctx.lines: + if dl.depth != 1: + continue + if dl.linetype in (LineType.BLANK, LineType.PREPROCESSOR): + continue + if dl.linetype in (LineType.COMMENT_BODY, LineType.COMMENT_END): + continue + if not dl.indent_str: + continue + if dl.indent_str == '\t': + return '\t' + if dl.indent_str == ' ': + return ' ' + if dl.indent_str == ' ': + return ' ' + # Anything else at depth 1 is non-canonical; flag elsewhere. + return dl.indent_str + return None + + +def check_indent_unit_relaxed(ctx): + """YAML examples: 2 or 4 spaces. Never tabs or other widths.""" + unit = detect_indent_unit(ctx) + if unit is None: + return + if unit not in (' ', ' '): + yield (1, 'indent unit must be 2 or 4 spaces, got %r' % unit) + + +def check_indent_unit_dts(ctx): + """DTS files: 1 tab per level. Always required.""" + unit = detect_indent_unit(ctx) + if unit is None: + return + if unit != '\t': + yield (1, 'indent unit must be 1 tab in DTS, got %r' % unit) + + +def check_indent_unit_strict(ctx): + """YAML: must be exactly 4 spaces. DTS: 1 tab (same as relaxed).""" + unit = detect_indent_unit(ctx) + if unit is None: + return + if ctx.indent_kind == 'spaces': + if unit != ' ': + yield (1, 'indent unit must be 4 spaces in strict mode, ' + 'got %r' % unit) + + +def check_indent_consistent(ctx): + """All indented lines must be a multiple of the detected unit.""" + unit = detect_indent_unit(ctx) + if unit is None: + return + if ctx.indent_kind == 'spaces': + if unit not in (' ', ' '): + return # let check_indent_unit_* report this + else: + if unit != '\t': + return + + for dl in ctx.lines: + if dl.linetype in (LineType.BLANK, LineType.PREPROCESSOR): + continue + if dl.linetype == LineType.CONTINUATION: + continue # continuations align to <, not to indent unit + if dl.linetype in (LineType.COMMENT_BODY, LineType.COMMENT_END): + continue + if not dl.indent_str: + continue + # The indent must be 'unit' repeated dl.depth times, exactly. + # NODE_CLOSE lines have depth equal to the post-decrement value, + # which matches the indent expected. + expected = unit * dl.depth + if dl.indent_str != expected: + yield (dl.lineno, + 'indent mismatch (expected depth %d * %r)' % + (dl.depth, unit)) + + +def check_blank_lines(ctx): + """No two consecutive blank lines, no leading/trailing blank lines + in any node body.""" + lines = ctx.lines + # Consecutive blanks + for i in range(1, len(lines)): + if lines[i].linetype == LineType.BLANK and \ + lines[i - 1].linetype == LineType.BLANK: + yield (lines[i].lineno, 'consecutive blank lines') + # Blank right after { or right before } + for i, dl in enumerate(lines): + if dl.linetype != LineType.BLANK: + continue + prev = lines[i - 1] if i > 0 else None + nxt = lines[i + 1] if i + 1 < len(lines) else None + if prev is not None and prev.linetype == LineType.NODE_OPEN: + yield (dl.lineno, 'blank line at start of node body') + if nxt is not None and nxt.linetype == LineType.NODE_CLOSE: + yield (dl.lineno, 'blank line at end of node body') + + +def _walk_bodies(lines): + """Yield lists of immediate-child NODE_OPEN lines for each node body + in the input. Skips ref-nodes (&label) since those don't have an + intrinsic ordering.""" + body_stack = [[]] + for dl in lines: + if dl.linetype == LineType.NODE_OPEN: + body_stack[-1].append(dl) + body_stack.append([]) + continue + if dl.linetype == LineType.NODE_CLOSE: + if len(body_stack) <= 1: + # Unbalanced; ignore to avoid crashing on malformed input + continue + yield body_stack.pop() + continue + while body_stack: + yield body_stack.pop() + + +def _natural_sort_key(s): + """Split a string into a tuple of (kind, value) pairs that compares + numeric runs as ints, so 'foo10' sorts after 'foo2'.""" + parts = [] + for part in re.split(r'(\d+)', s): + if part.isdigit(): + parts.append((0, int(part))) + else: + parts.append((1, part)) + return tuple(parts) + + +def check_child_address_order(ctx): + """Addressed siblings (foo@N) must appear in ascending address + order within their parent node body.""" + for children in _walk_bodies(ctx.lines): + addressed = [] + for c in children: + if c.node_addr is None: + continue + try: + parts = tuple(int(p, 16) for p in c.node_addr.split(',')) + except ValueError: + continue + addressed.append((parts, c)) + for i in range(1, len(addressed)): + if addressed[i][0] < addressed[i - 1][0]: + dl = addressed[i][1] + yield (dl.lineno, + 'child node @%s out of address order' % + dl.node_addr) + + +def check_child_name_order(ctx): + """Unaddressed siblings must appear in natural-sort order by node + name within their parent node body. Addressed children are scoped + by check_child_address_order; reference nodes (&label { ... }) and + the root node are skipped.""" + for children in _walk_bodies(ctx.lines): + unaddressed = [] + for c in children: + if c.node_addr is not None: + continue + if c.node_name in (None, '/'): + continue + if c.ref_name is not None: + continue + unaddressed.append((_natural_sort_key(c.node_name), c)) + for i in range(1, len(unaddressed)): + if unaddressed[i][0] < unaddressed[i - 1][0]: + dl = unaddressed[i][1] + yield (dl.lineno, + 'child node %r out of name order' % dl.node_name) + + +def _property_bucket(name): + """Return the canonical bucket index for a property: + 0 compatible + 1 reg / reg-names + 2 ranges + 3 standard properties (no vendor comma in #-stripped name) + 4 vendor-specific properties + 5 status + Plus a sub-key inside the bucket for fixed slots (compatible, reg, + reg-names, ranges, status). 'standard' and 'vendor' return None for + the sub-key, signalling that the within-bucket key is computed by + the pairing rules.""" + stripped = name.lstrip('#') + if name == 'compatible': + return (0, 0) + if name == 'reg': + return (1, 0) + if name == 'reg-names': + return (1, 1) + if name == 'ranges': + return (2, 0) + if name == 'status': + return (5, 0) + return (4 if ',' in stripped else 3, None) + + +# Declarative pairing rules: each is a callable +# (name, all_names) -> anchor_name_or_None +# If a rule returns an anchor, the property sorts immediately after the +# anchor. Rules are tried in order; the first match wins. If none +# matches, the within-bucket key falls back to natural sort by the +# #-stripped name. + +def _pair_pinctrl_names(name, all_names): + """pinctrl-names follows the highest pinctrl-N in the same node.""" + if name != 'pinctrl-names': + return None + cands = [n for n in all_names if re.match(r'^pinctrl-\d+$', n)] + if not cands: + return None + return max(cands, key=_natural_sort_key) + + +def _pair_x_names(name, all_names): + """Generic -names follows its owning property. The owner is + usually plural (clocks/clock-names, dmas/dma-names, + resets/reset-names) but occasionally singular (reg/reg-names is + handled by the fixed slot above; this rule catches anything else).""" + if not name.endswith('-names'): + return None + base = name[:-len('-names')] + # Try plural and singular forms. + if (base + 's') in all_names: + return base + 's' + if base in all_names: + return base + return None + + +PAIRING_RULES = (_pair_pinctrl_names, _pair_x_names) + + +def _property_sort_key(name, all_names): + """Sort key for a property among its node-body siblings. + + Format: (bucket, within_key, tiebreak). 'within_key' for + standard/vendor buckets follows pairing rules: a property paired + with anchor X sorts as if it were X with a higher tiebreak.""" + bucket, fixed_sub = _property_bucket(name) + if fixed_sub is not None: + return (bucket, (), fixed_sub) + + for rule in PAIRING_RULES: + anchor = rule(name, all_names) + if anchor is not None: + return (bucket, _natural_sort_key(anchor.lstrip('#')), 1) + + return (bucket, _natural_sort_key(name.lstrip('#')), 0) + + +def check_property_order(ctx): + """Properties within a node body must appear in canonical order: + compatible, reg(/reg-names), ranges, then the standard group, then + the vendor-specific group, then status. Inside the standard and + vendor groups, pairing rules apply (e.g. -names follows ); + everything else falls back to natural sort by the #-stripped name.""" + lines = ctx.lines + for i, dl in enumerate(lines): + if dl.linetype != LineType.NODE_OPEN: + continue + body_depth = dl.depth + 1 + props = [] + for j in range(i + 1, len(lines)): + d = lines[j] + if d.linetype == LineType.NODE_CLOSE and \ + d.depth == body_depth - 1: + break + if d.linetype == LineType.PROPERTY and d.depth == body_depth \ + and d.prop_name is not None: + props.append(d) + if len(props) < 2: + continue + all_names = [p.prop_name for p in props] + keyed = [(p, _property_sort_key(p.prop_name, all_names)) + for p in props] + for k in range(1, len(keyed)): + if keyed[k][1] < keyed[k - 1][1]: + p = keyed[k][0] + prev = keyed[k - 1][0] + yield (p.lineno, + 'property %r out of canonical order ' + '(should sort before %r)' % + (p.prop_name, prev.prop_name)) + + +def _strip_strings_and_comments(text): + """Remove string literals and /* */ + // comments from a single + line, replacing them with empty strings. Used so syntactic checks + (whitespace, hex case, etc.) don't false-positive on contents of + quoted strings or comments. An unclosed /* on the line is treated + as a comment running to end of line.""" + text = re.sub(r'"(?:[^"\\]|\\.)*"', '""', text) + text = re.sub(r'/\*.*?\*/', '', text) + text = re.sub(r'/\*.*$', '', text) + text = re.sub(r'//.*$', '', text) + return text + + +def check_required_blank_lines(ctx): + """A blank line must precede each child node and the 'status' + property within a node body, except when these are the first + substantive item in the body.""" + lines = ctx.lines + for i, open_dl in enumerate(lines): + if open_dl.linetype != LineType.NODE_OPEN: + continue + body_depth = open_dl.depth + 1 + prev_substantive = None + between_blanks = 0 + depth_inside = 0 + for j in range(i + 1, len(lines)): + d = lines[j] + if d.linetype == LineType.NODE_CLOSE and \ + d.depth == body_depth - 1 and depth_inside == 0: + break + # Track depth inside nested children so we only look at + # immediate-body items. + if d.linetype == LineType.NODE_OPEN and \ + d.depth >= body_depth and depth_inside > 0: + depth_inside += 1 + continue + if d.linetype == LineType.NODE_CLOSE and depth_inside > 0: + depth_inside -= 1 + continue + if depth_inside > 0: + continue + if d.linetype == LineType.BLANK: + if prev_substantive is not None: + between_blanks += 1 + continue + if d.linetype in (LineType.COMMENT, LineType.COMMENT_START, + LineType.COMMENT_BODY, LineType.COMMENT_END, + LineType.PREPROCESSOR): + continue + if d.linetype == LineType.CONTINUATION: + continue + + needs_blank = False + if d.linetype == LineType.NODE_OPEN: + needs_blank = True + depth_inside = 1 # entered the child body + elif d.linetype == LineType.PROPERTY and d.prop_name == 'status': + needs_blank = True + + if needs_blank and prev_substantive is not None and \ + between_blanks == 0: + if d.linetype == LineType.NODE_OPEN: + yield (d.lineno, + 'child node must be preceded by a blank line') + else: + yield (d.lineno, + '"status" must be preceded by a blank line') + + prev_substantive = d + between_blanks = 0 + + +def check_hex_case(ctx): + """Hex literals (0xN) must use lowercase digits and prefix.""" + for dl in ctx.lines: + if dl.linetype in (LineType.BLANK, LineType.COMMENT, + LineType.COMMENT_START, LineType.COMMENT_BODY, + LineType.COMMENT_END, LineType.PREPROCESSOR): + continue + text = _strip_strings_and_comments(dl.raw) + for m in re.finditer(r'\b0[xX][0-9a-fA-F]+\b', text): + lit = m.group(0) + if any(c.isupper() for c in lit[2:]) or lit[1] == 'X': + yield (dl.lineno, + 'hex literal %r must be lowercase' % lit) + + +def check_unit_address_format(ctx): + """Unit addresses must be lowercase hex without leading zeros and + without a '0x' prefix. For multi-cell addresses (comma-separated), + each part is checked independently. A single '0' is permitted + (canonical zero).""" + for dl in ctx.lines: + if dl.linetype != LineType.NODE_OPEN: + continue + if dl.node_addr is None: + continue + addr = dl.node_addr + for part in addr.split(','): + if part[:2] in ('0x', '0X'): + yield (dl.lineno, + 'unit address %r must not have a "0x" prefix' % + addr) + break + if not re.match(r'^[0-9a-fA-F]+$', part): + yield (dl.lineno, + 'unit address %r is not valid hex' % addr) + break + if any(c in 'ABCDEF' for c in part): + yield (dl.lineno, + 'unit address %r must be lowercase hex' % addr) + break + if len(part) > 1 and part.startswith('0'): + yield (dl.lineno, + 'unit address %r has leading zeros' % addr) + break + + +def check_value_whitespace(ctx): + """A <...> cell list must have no whitespace directly after '<' + or directly before '>'. Continuation lines are joined onto the + property so a <...> split across lines is checked too; a '<' or + '>' at a line break is glued straight to the neighbouring value, + so the break itself is not counted as padding. Outside strings + and comments only.""" + for dl in ctx.lines: + if dl.linetype != LineType.PROPERTY: + continue + segs = [_strip_strings_and_comments(dl.raw).strip()] + for cont in dl.continuations: + segs.append(_strip_strings_and_comments(cont.stripped).strip()) + text = '' + for s in segs: + if not s: + continue + if not text or text.endswith('<') or s.startswith('>'): + text += s + else: + text += ' ' + s + for m in re.finditer(r'<([^<>]*)>', text): + content = m.group(1) + if content and content != content.strip(): + yield (dl.lineno, 'extra whitespace inside <...>') + break + + +def check_node_close_alone(ctx): + """The closing '};' of a node must be on its own line. The + classifier accepts a canonical "}" or "};" as NODE_CLOSE; a line + that is all closures (e.g. "}; };") is still NODE_CLOSE for depth + tracking but is flagged here via dl.closures. Any other line that + still contains '};' (in code, not in strings or comments) is + mixing a node close with something else.""" + for dl in ctx.lines: + if dl.linetype == LineType.NODE_CLOSE: + if dl.closures > 1: + yield (dl.lineno, + 'closing brace must be on its own line') + continue + if dl.linetype in (LineType.BLANK, LineType.COMMENT, + LineType.COMMENT_START, LineType.COMMENT_BODY, + LineType.COMMENT_END, LineType.PREPROCESSOR): + continue + text = _strip_strings_and_comments(dl.raw) + if '};' in text: + yield (dl.lineno, + 'closing brace must be on its own line') + + +def _display_col(text): + """Visual column width of text, with tabs expanded to the next + 8-column stop, matching how printf and most editors render a + line and the kernel-wide line length convention.""" + col = 0 + for ch in text: + if ch == '\t': + col = (col // 8 + 1) * 8 + else: + col += 1 + return col + + +def check_line_length(ctx): + """Lines must not exceed 80 columns; tabs count as 8 (see + _display_col).""" + for dl in ctx.lines: + if dl.linetype == LineType.BLANK: + continue + cols = _display_col(dl.raw) + if cols > 80: + yield (dl.lineno, + 'line exceeds 80 columns (%d)' % cols) + + +def check_continuation_alignment(ctx): + """A multi-line property's continuation lines must align their + first non-whitespace character to the display column of the first + '<' or '"' after the '=' in the leading line. Display columns are + used so tab-indented .dts files (where a continuation aligns with + tabs plus spaces) are compared correctly.""" + for dl in ctx.lines: + if dl.linetype != LineType.PROPERTY: + continue + if not dl.continuations: + continue + eq = dl.raw.find('=') + if eq < 0: + continue + # First '<' or '"' after '=' + rest = dl.raw[eq + 1:] + m = re.search(r'[<"]', rest) + if not m: + continue + target_col = _display_col(dl.raw[:eq + 1 + m.start()]) + for cont in dl.continuations: + if _display_col(cont.indent_str) != target_col: + yield (cont.lineno, + 'continuation should align to column %d ' + '(under "<" or \\")' % (target_col + 1)) + + +def check_unclosed_block_comment(ctx): + """Every /* must have a matching */ in the same block. Catches both + a comment opened on its own line (COMMENT_START) and a tail comment + opened on a PROPERTY or other code line (where in_block_comment is + set by _split_code so the next line becomes COMMENT_BODY without a + preceding COMMENT_START).""" + open_lineno = None + for dl in ctx.lines: + if dl.linetype == LineType.COMMENT_START: + open_lineno = dl.lineno + elif dl.linetype == LineType.COMMENT_END: + open_lineno = None + elif dl.linetype == LineType.COMMENT_BODY and open_lineno is None: + # Block was opened by a /* tail on a code line; report at + # the first orphan body line since the originating line is + # already classified as something else. + open_lineno = dl.lineno + if open_lineno is not None: + yield (open_lineno, 'unclosed /* block comment') + + +def check_unused_labels(ctx): + """Labels defined but never referenced are clutter.""" + defined, referenced = collect_labels_and_refs(ctx.text) + for label in sorted(defined - referenced): + # Find the line where this label is defined for line-number + # reporting. + m = re.search(r'(?m)^.*\b' + re.escape(label) + r'\s*:', ctx.text) + lineno = ctx.text[:m.start()].count('\n') + 1 if m else 1 + yield (lineno, 'label %r defined but never &-referenced' % label) + + +# --- registry -------------------------------------------------------------- + +RULES = [ + # 'relaxed' is the default; rules in this group must produce zero + # output on a clean kernel tree (post the small prep-cleanup + # commit at the head of this series). + Rule('trailing-whitespace', 'relaxed', + 'no trailing whitespace on any line', + check_trailing_whitespace), + Rule('tab-in-dts', 'relaxed', + 'YAML examples may not contain tab characters', + check_tab_in_dts, applies_to=('yaml',)), + Rule('mixed-indent-chars', 'relaxed', + 'indent must not mix tabs and spaces', + check_mixed_indent_chars), + Rule('unclosed-block-comment', 'relaxed', + 'every /* block comment must close with */', + check_unclosed_block_comment), + + # DTS files always use tabs; this is not negotiable per kernel + # coding style (.dts files are real source). Relaxed mode. + Rule('indent-unit-dts', 'relaxed', + 'DTS files: 1 tab per nesting level', + check_indent_unit_dts, + applies_to=('dts', 'dtsi', 'dtso')), + + # 'strict' rules are opt-in (e.g. for new submissions via + # checkpatch.pl in a follow-up series). They flag many existing + # files and can be promoted to relaxed once those are cleaned up. + Rule('indent-unit', 'strict', + 'YAML: 2 or 4 spaces per level', + check_indent_unit_relaxed, applies_to=('yaml',)), + Rule('indent-unit-strict', 'strict', + 'YAML: must be 4 spaces per level', + check_indent_unit_strict, applies_to=('yaml',)), + Rule('indent-consistent', 'strict', + 'every line indented at depth * unit', + check_indent_consistent), + Rule('blank-lines', 'strict', + 'no consecutive blanks; no blanks at node body edges', + check_blank_lines), + Rule('child-address-order', 'strict', + 'addressed siblings must be in ascending address order', + check_child_address_order), + Rule('child-name-order', 'strict', + 'unaddressed siblings must be in natural-sort name order', + check_child_name_order), + Rule('property-order', 'strict', + 'canonical bucket + pairing + natural-sort order of properties', + check_property_order), + Rule('required-blank-lines', 'strict', + 'blank line before child nodes and before "status"', + check_required_blank_lines), + Rule('hex-case', 'strict', + 'hex literals must be lowercase', + check_hex_case), + Rule('unit-address-format', 'strict', + 'unit addresses must be lowercase hex without leading zeros', + check_unit_address_format), + Rule('value-whitespace', 'strict', + 'no whitespace directly inside <...> brackets', + check_value_whitespace), + Rule('node-close-alone', 'strict', + 'closing brace must be on its own line', + check_node_close_alone), + Rule('line-length', 'strict', + 'lines must not exceed 80 columns', + check_line_length), + Rule('continuation-alignment', 'strict', + 'multi-line property continuations align under "<" or "\\""', + check_continuation_alignment), + Rule('unused-labels', 'strict', + 'every label must be &-referenced in the same example/file ' + '(skipped for .dtsi/.dtso since labels there are exported)', + check_unused_labels, applies_to=('yaml', 'dts')), +] + + +def select_rules(mode, input_kind): + """Return rules that apply to the given mode and input type.""" + rank = {'relaxed': 0, 'strict': 1} + out = [] + for r in RULES: + if rank[r.mode] > rank[mode]: + continue + if input_kind not in r.applies_to: + continue + out.append(r) + return out + + +# --------------------------------------------------------------------------- +# Block runner +# --------------------------------------------------------------------------- + +def check_block(text, mode, indent_kind, input_type): + """Run all selected rules on a single block of DTS text. Returns a + list of (lineno, rule_name, message) tuples.""" + lines = classify_lines(text) + ctx = Ctx(lines, text, mode, indent_kind) + rules = select_rules(mode, input_type) + findings = [] + for r in rules: + for lineno, msg in r.check(ctx): + findings.append((lineno, r.name, msg)) + findings.sort(key=lambda t: (t[0], t[1])) + return findings + + +# --------------------------------------------------------------------------- +# Input drivers (YAML examples vs raw DTS) +# --------------------------------------------------------------------------- + +def _yaml_loader(): + return ruamel.yaml.YAML() + + +def iter_yaml_examples(filepath): + """Yield (example_text, base_lineno_in_file, example_index) tuples.""" + yaml = _yaml_loader() + try: + with open(filepath, encoding='utf-8') as f: + data = yaml.load(f) + except Exception as e: + print('%s: error loading YAML: %s' % (filepath, e), + file=sys.stderr) + return + if not isinstance(data, dict) or 'examples' not in data: + return + examples = data['examples'] + if not hasattr(examples, '__iter__'): + return + for i, ex in enumerate(examples): + if not isinstance(ex, str): + continue + try: + base = examples.lc.item(i)[0] + 2 + except Exception: + base = 1 + yield (str(ex), base, i) + + +def iter_dts_file(filepath): + """Treat the whole file as a single block.""" + try: + with open(filepath, encoding='utf-8') as f: + text = f.read() + except Exception as e: + print('%s: error reading: %s' % (filepath, e), file=sys.stderr) + return + yield (text, 1, None) + + +# --------------------------------------------------------------------------- +# Top-level processing +# --------------------------------------------------------------------------- + +def input_kind(filepath): + p = filepath.lower() + if p.endswith('.yaml') or p.endswith('.yml'): + return 'yaml' + if p.endswith('.dts'): + return 'dts' + if p.endswith('.dtsi'): + return 'dtsi' + if p.endswith('.dtso'): + return 'dtso' + return None + + +# All input types that use tab indentation and follow DTS coding style. +DTS_FAMILY = ('dts', 'dtsi', 'dtso') + + +def collect_findings(filepath, mode): + """Return a (lines, count) pair for filepath. lines is a list of + formatted output strings; count is the number of findings.""" + kind = input_kind(filepath) + if kind == 'yaml': + indent_kind = 'spaces' + iterator = iter_yaml_examples(filepath) + elif kind in DTS_FAMILY: + indent_kind = 'tab' + iterator = iter_dts_file(filepath) + else: + return (['%s: unknown file type, skipping' % filepath], 0) + + out = [] + for text, base, idx in iterator: + for lineno, rule, msg in check_block(text, mode, indent_kind, kind): + abs_line = base + lineno - 1 + ex_tag = '' if idx is None else ' example %d' % idx + out.append('%s:%d:%s [%s] %s' % + (filepath, abs_line, ex_tag, rule, msg)) + return (out, len(out)) + + +# Worker entry point for ProcessPoolExecutor.map(). Top-level so it is +# picklable on every platform. +def _worker(args): + filepath, mode = args + return collect_findings(filepath, mode) + + +def main(): + import os + ap = argparse.ArgumentParser( + description='Check DTS coding style on YAML examples and ' + '.dts/.dtsi/.dtso files.', + fromfile_prefix_chars='@') + ap.add_argument('--mode', choices=('relaxed', 'strict'), + default='relaxed', + help='which rule set to apply (default: relaxed)') + ap.add_argument('-j', '--jobs', type=int, default=0, + metavar='N', + help='run N workers in parallel (default: respect ' + 'the make jobserver via $PARALLELISM, otherwise ' + 'os.cpu_count(); use 1 to disable multiprocessing)') + ap.add_argument('--list-rules', action='store_true', + help='print all rules with their mode and exit') + ap.add_argument('files', nargs='*', metavar='file', + help='YAML binding files or .dts/.dtsi/.dtso files; ' + 'use @argfile to read paths from a file') + args = ap.parse_args() + + if args.list_rules: + for r in RULES: + applies = ','.join(r.applies_to) + print('%-22s %-7s [%s] %s' % + (r.name, r.mode, applies, r.description)) + return 0 + + if not args.files: + ap.error('no input files') + + if args.jobs > 0: + jobs = args.jobs + else: + # When invoked under scripts/jobserver-exec, $PARALLELISM + # holds the slot count make has reserved for us; this lets + # `make -j N dt_binding_check` constrain our worker pool to N. + try: + jobs = int(os.environ['PARALLELISM']) + except (KeyError, ValueError): + jobs = os.cpu_count() or 1 + # Single-process path: keep import surface small for tests and + # easy debugging. + if jobs == 1 or len(args.files) == 1: + total = 0 + for f in args.files: + lines, n = collect_findings(f, args.mode) + for line in lines: + print(line, file=sys.stderr) + total += n + return 1 if total else 0 + + # Multi-process path. ex.map preserves input order so output is + # deterministic across runs. + from concurrent.futures import ProcessPoolExecutor + total = 0 + work = [(f, args.mode) for f in args.files] + chunk = max(1, len(work) // (jobs * 8)) if work else 1 + with ProcessPoolExecutor(max_workers=jobs) as ex: + for lines, n in ex.map(_worker, work, chunksize=chunk): + for line in lines: + print(line, file=sys.stderr) + total += n + return 1 if total else 0 + + +if __name__ == '__main__': + sys.exit(main()) -- cgit v1.2.3 From 28019885a3fff013a3e3d0b4d2cd9df41e95658a Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Wed, 27 May 2026 20:32:18 +0100 Subject: scripts/jobserver-exec: propagate child exit status main() called JobserverExec().run() and discarded its return value, then the script exited with the implicit status 0. As a result, any Makefile that wired a build step through jobserver-exec saw the step silently succeed even when the wrapped command had failed. Two in-tree callers were affected: Documentation/devicetree/bindings/Makefile cmd_chk_style runs a python checker via jobserver-exec and uses "&& touch $@ || true" so failures leave the stamp file untouched and the next make rerun reports them again. The swallowed exit code made the stamp file get created even on failure, caching the failed run and hiding the reported issues until the inputs change. scripts/Makefile.vmlinux_o cmd_gen_initcalls_lds runs scripts/generate_initcall_order.pl via jobserver-exec; a perl failure was masked by the wrapper. Return the subprocess exit code from main() and pass it to sys.exit() so the wrapped command's status reaches make. Signed-off-by: Daniel Golle Reviewed-by: Krzysztof Kozlowski Link: https://patch.msgid.link/660368ca16e2d3845577a9fd157d2f37f0e09e85.1779908995.git.daniel@makrotopia.org Signed-off-by: Rob Herring (Arm) --- scripts/jobserver-exec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/jobserver-exec b/scripts/jobserver-exec index 758e947a6fb9..21b319e6c9a5 100755 --- a/scripts/jobserver-exec +++ b/scripts/jobserver-exec @@ -28,8 +28,8 @@ def main(): sys.exit("usage: " + name +" command [args ...]\n" + __doc__) with JobserverExec() as jobserver: - jobserver.run(sys.argv[1:]) + return jobserver.run(sys.argv[1:]) if __name__ == "__main__": - main() + sys.exit(main()) -- cgit v1.2.3 From efb6347d8b43d4d516a8f937485af29357c31df2 Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Wed, 27 May 2026 20:32:34 +0100 Subject: dt-bindings: add self-test fixtures for style checker Provide good/ and bad/ DTS and YAML fixtures plus a small runner that feeds them to dt-check-style and diffs the output against expected text files. Wired into a new top-level dt_style_selftest make target so the suite can be exercised independently of the full tree. Signed-off-by: Daniel Golle Link: https://patch.msgid.link/80fec5d2cfcdee0f9c5e2d4921ebbd4115d392b7.1779908995.git.daniel@makrotopia.org Signed-off-by: Rob Herring (Arm) --- Makefile | 6 ++ scripts/dtc/dt-style-selftest/bad/dts-spaces.dts | 12 ++++ .../bad/yaml-child-addr-order.yaml | 41 +++++++++++++ .../bad/yaml-child-name-order.yaml | 37 +++++++++++ .../dtc/dt-style-selftest/bad/yaml-cont-align.yaml | 30 +++++++++ .../bad/yaml-digit-node-order.yaml | 37 +++++++++++ .../dtc/dt-style-selftest/bad/yaml-hex-case.yaml | 29 +++++++++ .../dt-style-selftest/bad/yaml-indent-strict.yaml | 29 +++++++++ .../bad/yaml-label-in-string.yaml | 30 +++++++++ .../dt-style-selftest/bad/yaml-line-length.yaml | 29 +++++++++ .../dt-style-selftest/bad/yaml-mixed-indent.yaml | 29 +++++++++ .../dt-style-selftest/bad/yaml-multi-close.yaml | 35 +++++++++++ .../dtc/dt-style-selftest/bad/yaml-node-close.yaml | 31 ++++++++++ .../dtc/dt-style-selftest/bad/yaml-prop-order.yaml | 29 +++++++++ .../dt-style-selftest/bad/yaml-prop-pairing.yaml | 33 ++++++++++ .../dt-style-selftest/bad/yaml-required-blank.yaml | 33 ++++++++++ scripts/dtc/dt-style-selftest/bad/yaml-tab.yaml | 29 +++++++++ .../bad/yaml-trailing-comment.yaml | 26 ++++++++ .../dt-style-selftest/bad/yaml-trailing-ws.yaml | 29 +++++++++ .../bad/yaml-unclosed-comment.yaml | 30 +++++++++ .../bad/yaml-unit-addr-prefix.yaml | 29 +++++++++ .../dtc/dt-style-selftest/bad/yaml-unit-addr.yaml | 29 +++++++++ .../dt-style-selftest/bad/yaml-unused-label.yaml | 29 +++++++++ .../bad/yaml-value-ws-multiline.yaml | 27 ++++++++ .../dtc/dt-style-selftest/bad/yaml-value-ws.yaml | 29 +++++++++ .../dt-style-selftest/expected/dts-spaces.dts.txt | 2 + .../expected/yaml-child-addr-order.yaml.txt | 2 + .../expected/yaml-child-name-order.yaml.txt | 2 + .../expected/yaml-cont-align.yaml.txt | 2 + .../expected/yaml-digit-node-order.yaml.txt | 2 + .../expected/yaml-hex-case.yaml.txt | 2 + .../expected/yaml-indent-strict.yaml.txt | 2 + .../expected/yaml-label-in-string.yaml.txt | 2 + .../expected/yaml-line-length.yaml.txt | 2 + .../expected/yaml-mixed-indent.yaml.txt | 3 + .../expected/yaml-multi-close.yaml.txt | 3 + .../expected/yaml-node-close.yaml.txt | 2 + .../expected/yaml-prop-order.yaml.txt | 2 + .../expected/yaml-prop-pairing.yaml.txt | 3 + .../expected/yaml-required-blank.yaml.txt | 3 + .../dt-style-selftest/expected/yaml-tab.yaml.txt | 2 + .../expected/yaml-trailing-comment.yaml.txt | 2 + .../expected/yaml-trailing-ws.yaml.txt | 2 + .../expected/yaml-unclosed-comment.yaml.txt | 2 + .../expected/yaml-unit-addr-prefix.yaml.txt | 2 + .../expected/yaml-unit-addr.yaml.txt | 2 + .../expected/yaml-unused-label.yaml.txt | 2 + .../expected/yaml-value-ws-multiline.yaml.txt | 2 + .../expected/yaml-value-ws.yaml.txt | 2 + .../dtc/dt-style-selftest/good/dts-cont-align.dts | 26 ++++++++ scripts/dtc/dt-style-selftest/good/dts-tab.dts | 29 +++++++++ .../dtc/dt-style-selftest/good/yaml-4space.yaml | 41 +++++++++++++ .../good/yaml-tricky-parsing.yaml | 57 +++++++++++++++++ scripts/dtc/dt-style-selftest/run.sh | 71 ++++++++++++++++++++++ 54 files changed, 1003 insertions(+) create mode 100644 scripts/dtc/dt-style-selftest/bad/dts-spaces.dts create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-child-addr-order.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-child-name-order.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-cont-align.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-digit-node-order.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-hex-case.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-indent-strict.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-label-in-string.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-line-length.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-mixed-indent.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-multi-close.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-node-close.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-prop-order.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-prop-pairing.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-required-blank.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-tab.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-trailing-comment.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-trailing-ws.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-unclosed-comment.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-unit-addr-prefix.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-unit-addr.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-unused-label.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-value-ws-multiline.yaml create mode 100644 scripts/dtc/dt-style-selftest/bad/yaml-value-ws.yaml create mode 100644 scripts/dtc/dt-style-selftest/expected/dts-spaces.dts.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-child-addr-order.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-child-name-order.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-cont-align.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-digit-node-order.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-hex-case.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-indent-strict.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-label-in-string.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-line-length.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-mixed-indent.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-multi-close.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-node-close.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-prop-order.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-prop-pairing.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-required-blank.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-tab.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-trailing-comment.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-trailing-ws.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-unclosed-comment.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-unit-addr-prefix.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-unit-addr.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-unused-label.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-value-ws-multiline.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/expected/yaml-value-ws.yaml.txt create mode 100644 scripts/dtc/dt-style-selftest/good/dts-cont-align.dts create mode 100644 scripts/dtc/dt-style-selftest/good/dts-tab.dts create mode 100644 scripts/dtc/dt-style-selftest/good/yaml-4space.yaml create mode 100644 scripts/dtc/dt-style-selftest/good/yaml-tricky-parsing.yaml create mode 100755 scripts/dtc/dt-style-selftest/run.sh (limited to 'scripts') diff --git a/Makefile b/Makefile index e27c91ea56fc..c5513b573bbd 100644 --- a/Makefile +++ b/Makefile @@ -295,6 +295,7 @@ no-dot-config-targets := $(clean-targets) \ cscope gtags TAGS tags help% %docs check% coccicheck \ $(version_h) headers headers_% archheaders archscripts \ %asm-generic kernelversion %src-pkg dt_binding_check \ + dt_style_selftest \ outputmakefile rustavailable rustfmt rustfmtcheck \ run-command no-sync-config-targets := $(no-dot-config-targets) %install modules_sign kernelrelease \ @@ -1643,6 +1644,10 @@ PHONY += dt_compatible_check dt_compatible_check: dt_binding_schemas $(Q)$(MAKE) $(build)=$(dtbindingtree) $@ +PHONY += dt_style_selftest +dt_style_selftest: + $(Q)$(srctree)/scripts/dtc/dt-style-selftest/run.sh + # --------------------------------------------------------------------------- # Modules @@ -1845,6 +1850,7 @@ help: echo ' dtbs_install - Install dtbs to $(INSTALL_DTBS_PATH)'; \ echo ' dt_binding_check - Validate device tree binding documents and examples'; \ echo ' dt_binding_schemas - Build processed device tree binding schemas'; \ + echo ' dt_style_selftest - Run dt-check-style fixture tests'; \ echo ' dtbs_check - Validate device tree source files';\ echo '') diff --git a/scripts/dtc/dt-style-selftest/bad/dts-spaces.dts b/scripts/dtc/dt-style-selftest/bad/dts-spaces.dts new file mode 100644 index 000000000000..9dad22adce51 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/dts-spaces.dts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +/* + * Test fixture: a .dts using space indent (must use tabs). + */ + +/dts-v1/; + +/ { + compatible = "example,test-board"; + #address-cells = <1>; + #size-cells = <1>; +}; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-child-addr-order.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-child-addr-order.yaml new file mode 100644 index 000000000000..3df56e69a1ff --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-child-addr-order.yaml @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-child-order.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture with addressed children out of order + +maintainers: + - Test User + +properties: + compatible: + const: example,test-child-order + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + bus@10000 { + compatible = "simple-bus"; + reg = <0x10000 0x1000>; + #address-cells = <1>; + #size-cells = <1>; + + device@200 { + compatible = "example,test-child-order"; + reg = <0x200 0x10>; + }; + + device@100 { + compatible = "example,test-child-order"; + reg = <0x100 0x10>; + }; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-child-name-order.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-child-name-order.yaml new file mode 100644 index 000000000000..35d85e5573c2 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-child-name-order.yaml @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-child-name-order.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture with unaddressed children out of name order + +maintainers: + - Test User + +properties: + compatible: + const: example,test-child-name-order + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + bus@10000 { + compatible = "simple-bus"; + reg = <0x10000 0x1000>; + + foo { + label = "foo"; + }; + + bar { + label = "bar"; + }; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-cont-align.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-cont-align.yaml new file mode 100644 index 000000000000..92778540b056 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-cont-align.yaml @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-cont-align.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture with mis-aligned multi-line property + +maintainers: + - Test User + +properties: + compatible: + const: example,test-cont-align + reg: + maxItems: 2 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + foo@1000 { + compatible = "example,test-cont-align"; + reg = <0x1000 0x100>, + <0x2000 0x100>; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-digit-node-order.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-digit-node-order.yaml new file mode 100644 index 000000000000..44a9d25e5ba0 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-digit-node-order.yaml @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-digit-node-order.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture with digit-leading nodes out of address order + +maintainers: + - Test User + +properties: + compatible: + const: example,test-digit-node-order + +required: + - compatible + +additionalProperties: false + +examples: + - | + bus@0 { + compatible = "simple-bus"; + #address-cells = <1>; + #size-cells = <1>; + + 3d-engine@20 { + compatible = "example,3d-engine"; + reg = <0x20 0x4>; + }; + + 1wire@10 { + compatible = "example,1wire"; + reg = <0x10 0x4>; + }; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-hex-case.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-hex-case.yaml new file mode 100644 index 000000000000..b26d1bf58de9 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-hex-case.yaml @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-hex-case.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture with uppercase hex literals + +maintainers: + - Test User + +properties: + compatible: + const: example,test-hex-case + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + foo@1000 { + compatible = "example,test-hex-case"; + reg = <0xABCD 0x100>; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-indent-strict.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-indent-strict.yaml new file mode 100644 index 000000000000..bee4cf118d73 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-indent-strict.yaml @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-indent-strict.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture using 2-space indent (rejected by strict mode) + +maintainers: + - Test User + +properties: + compatible: + const: example,test-indent-strict + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + device@1000 { + compatible = "example,test-indent-strict"; + reg = <0x1000 0x100>; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-label-in-string.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-label-in-string.yaml new file mode 100644 index 000000000000..ba512869b702 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-label-in-string.yaml @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-label-in-string.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture where a label is only "referenced" inside a string + +maintainers: + - Test User + +properties: + compatible: + const: example,test-label-in-string + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + foo: device@1000 { + compatible = "example,test-label-in-string"; + reg = <0x1000 0x100>; + info = "see &foo for details"; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-line-length.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-line-length.yaml new file mode 100644 index 000000000000..64427bf1c385 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-line-length.yaml @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-line-length.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture exceeding 80 columns + +maintainers: + - Test User + +properties: + compatible: + const: example,test-line-length + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + foo@1000 { + compatible = "example,test-line-length-this-is-a-very-long-name-indeed-yeah"; + reg = <0x1000 0x100>; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-mixed-indent.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-mixed-indent.yaml new file mode 100644 index 000000000000..5401d1a423a1 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-mixed-indent.yaml @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-mixed-indent.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture mixing tabs and spaces in indent + +maintainers: + - Test User + +properties: + compatible: + const: example,test-mixed + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + device@1000 { + compatible = "example,test-mixed"; + reg = <0x1000 0x100>; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-multi-close.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-multi-close.yaml new file mode 100644 index 000000000000..4d9fa27b50a2 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-multi-close.yaml @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-multi-close.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture with two closing braces on one line + +maintainers: + - Test User + +properties: + compatible: + const: example,test-multi-close + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + bus@10000 { + compatible = "simple-bus"; + reg = <0x10000 0x1000>; + #address-cells = <1>; + #size-cells = <1>; + + device@100 { + compatible = "example,test-multi-close"; + reg = <0x100 0x10>; + }; }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-node-close.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-node-close.yaml new file mode 100644 index 000000000000..e107659fd9e8 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-node-close.yaml @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-node-close.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture with closing brace not on its own line + +maintainers: + - Test User + +properties: + compatible: + const: example,test-node-close + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + bus@10000 { + compatible = "simple-bus"; + reg = <0x10000 0x1000>; + + empty {}; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-prop-order.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-prop-order.yaml new file mode 100644 index 000000000000..75582a3d2f6e --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-prop-order.yaml @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-prop-order.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture with reg before compatible + +maintainers: + - Test User + +properties: + compatible: + const: example,test-prop-order + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + device@1000 { + reg = <0x1000 0x100>; + compatible = "example,test-prop-order"; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-prop-pairing.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-prop-pairing.yaml new file mode 100644 index 000000000000..767ab21c39f3 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-prop-pairing.yaml @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-prop-pairing.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture exercising -names and pinctrl-names pairing + +maintainers: + - Test User + +properties: + compatible: + const: example,test-prop-pairing + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + foo@1000 { + compatible = "example,test-prop-pairing"; + reg = <0x1000 0x100>; + clock-names = "bus"; + clocks = <&clk 0>; + pinctrl-names = "default"; + pinctrl-0 = <&p0>; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-required-blank.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-required-blank.yaml new file mode 100644 index 000000000000..8bb53240cffa --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-required-blank.yaml @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-required-blank.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture missing required blank lines + +maintainers: + - Test User + +properties: + compatible: + const: example,test-required-blank + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + bus@10000 { + compatible = "simple-bus"; + reg = <0x10000 0x1000>; + status = "okay"; + child@100 { + reg = <0x100>; + }; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-tab.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-tab.yaml new file mode 100644 index 000000000000..487d07ff8cb6 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-tab.yaml @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-tab.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture with a tab in a DTS line + +maintainers: + - Test User + +properties: + compatible: + const: example,test-tab + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + device@1000 { + compatible = "example,test-tab"; + reg = <0x1000 0x100>; /* registers */ + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-trailing-comment.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-trailing-comment.yaml new file mode 100644 index 000000000000..2368ada8106f --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-trailing-comment.yaml @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-trailing-comment.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture with properties out of order behind trailing comments + +maintainers: + - Test User + +properties: + compatible: + const: example,test-trailing-comment + +required: + - compatible + +additionalProperties: false + +examples: + - | + foo@0 { /* the device node */ + reg = <0x0 0x4>; /* registers */ + compatible = "example,test-trailing-comment"; // misplaced + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-trailing-ws.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-trailing-ws.yaml new file mode 100644 index 000000000000..5c4b4bd833c5 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-trailing-ws.yaml @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-trailing.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture with trailing whitespace + +maintainers: + - Test User + +properties: + compatible: + const: example,test-trailing + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + device@1000 { + compatible = "example,test-trailing"; + reg = <0x1000 0x100>; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-unclosed-comment.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-unclosed-comment.yaml new file mode 100644 index 000000000000..63c1c08712a5 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-unclosed-comment.yaml @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-unclosed-comment.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture with an unclosed /* block comment + +maintainers: + - Test User + +properties: + compatible: + const: example,test-unclosed-comment + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + /* this comment never closes + device@1000 { + compatible = "example,test-unclosed-comment"; + reg = <0x1000 0x100>; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-unit-addr-prefix.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-unit-addr-prefix.yaml new file mode 100644 index 000000000000..9b3fe508c5fd --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-unit-addr-prefix.yaml @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-unit-addr-prefix.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture with 0x-prefixed unit address + +maintainers: + - Test User + +properties: + compatible: + const: example,test-unit-addr-prefix + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + memory@0x1000 { + compatible = "example,test-unit-addr-prefix"; + reg = <0x1000 0x100>; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-unit-addr.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-unit-addr.yaml new file mode 100644 index 000000000000..93705cd45410 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-unit-addr.yaml @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-unit-addr.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture with malformed unit address + +maintainers: + - Test User + +properties: + compatible: + const: example,test-unit-addr + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + foo@01000 { + compatible = "example,test-unit-addr"; + reg = <0x1000 0x100>; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-unused-label.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-unused-label.yaml new file mode 100644 index 000000000000..28d7176cbf08 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-unused-label.yaml @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-unused-label.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture with an unused label + +maintainers: + - Test User + +properties: + compatible: + const: example,test-unused-label + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + dev: device@1000 { + compatible = "example,test-unused-label"; + reg = <0x1000 0x100>; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-value-ws-multiline.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-value-ws-multiline.yaml new file mode 100644 index 000000000000..504bf0931c27 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-value-ws-multiline.yaml @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-value-ws-multiline.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture with extra whitespace in a multi-line cell array + +maintainers: + - Test User + +properties: + compatible: + const: example,test-value-ws-multiline + +required: + - compatible + +additionalProperties: false + +examples: + - | + foo@0 { + compatible = "example,test-value-ws-multiline"; + reg = < 0x0 0x4 + 0x8 0xc>; + }; diff --git a/scripts/dtc/dt-style-selftest/bad/yaml-value-ws.yaml b/scripts/dtc/dt-style-selftest/bad/yaml-value-ws.yaml new file mode 100644 index 000000000000..342ab9f399f1 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/bad/yaml-value-ws.yaml @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-bad-value-ws.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture with extra whitespace inside <...> + +maintainers: + - Test User + +properties: + compatible: + const: example,test-value-ws + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + foo@1000 { + compatible = "example,test-value-ws"; + reg = < 0x1000 0x100 >; + }; diff --git a/scripts/dtc/dt-style-selftest/expected/dts-spaces.dts.txt b/scripts/dtc/dt-style-selftest/expected/dts-spaces.dts.txt new file mode 100644 index 000000000000..070025c4568c --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/dts-spaces.dts.txt @@ -0,0 +1,2 @@ +# mode=relaxed +bad/dts-spaces.dts:1: [indent-unit-dts] indent unit must be 1 tab in DTS, got ' ' diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-child-addr-order.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-child-addr-order.yaml.txt new file mode 100644 index 000000000000..f0db79a0018b --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-child-addr-order.yaml.txt @@ -0,0 +1,2 @@ +# mode=strict +bad/yaml-child-addr-order.yaml:37: example 0 [child-address-order] child node @100 out of address order diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-child-name-order.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-child-name-order.yaml.txt new file mode 100644 index 000000000000..bb434b126191 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-child-name-order.yaml.txt @@ -0,0 +1,2 @@ +# mode=strict +bad/yaml-child-name-order.yaml:34: example 0 [child-name-order] child node 'bar' out of name order diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-cont-align.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-cont-align.yaml.txt new file mode 100644 index 000000000000..b5576dd0f6b1 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-cont-align.yaml.txt @@ -0,0 +1,2 @@ +# mode=strict +bad/yaml-cont-align.yaml:29: example 0 [continuation-alignment] continuation should align to column 11 (under "<" or \") diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-digit-node-order.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-digit-node-order.yaml.txt new file mode 100644 index 000000000000..6de275e2dcb5 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-digit-node-order.yaml.txt @@ -0,0 +1,2 @@ +# mode=strict +bad/yaml-digit-node-order.yaml:33: example 0 [child-address-order] child node @10 out of address order diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-hex-case.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-hex-case.yaml.txt new file mode 100644 index 000000000000..6600f7cd1ba5 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-hex-case.yaml.txt @@ -0,0 +1,2 @@ +# mode=strict +bad/yaml-hex-case.yaml:28: example 0 [hex-case] hex literal '0xABCD' must be lowercase diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-indent-strict.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-indent-strict.yaml.txt new file mode 100644 index 000000000000..5ef290d3a847 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-indent-strict.yaml.txt @@ -0,0 +1,2 @@ +# mode=strict +bad/yaml-indent-strict.yaml:26: example 0 [indent-unit-strict] indent unit must be 4 spaces in strict mode, got ' ' diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-label-in-string.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-label-in-string.yaml.txt new file mode 100644 index 000000000000..05da06f81364 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-label-in-string.yaml.txt @@ -0,0 +1,2 @@ +# mode=strict +bad/yaml-label-in-string.yaml:26: example 0 [unused-labels] label 'foo' defined but never &-referenced diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-line-length.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-line-length.yaml.txt new file mode 100644 index 000000000000..89b36360caa4 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-line-length.yaml.txt @@ -0,0 +1,2 @@ +# mode=strict +bad/yaml-line-length.yaml:27: example 0 [line-length] line exceeds 80 columns (81) diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-mixed-indent.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-mixed-indent.yaml.txt new file mode 100644 index 000000000000..c989f8f19853 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-mixed-indent.yaml.txt @@ -0,0 +1,3 @@ +# mode=relaxed +bad/yaml-mixed-indent.yaml:27: example 0 [mixed-indent-chars] mixed tabs and spaces in indent +bad/yaml-mixed-indent.yaml:27: example 0 [tab-in-dts] tab character not allowed in DTS example diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-multi-close.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-multi-close.yaml.txt new file mode 100644 index 000000000000..637d0f8ea103 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-multi-close.yaml.txt @@ -0,0 +1,3 @@ +# mode=strict +bad/yaml-multi-close.yaml:35: example 0 [indent-consistent] indent mismatch (expected depth 0 * ' ') +bad/yaml-multi-close.yaml:35: example 0 [node-close-alone] closing brace must be on its own line diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-node-close.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-node-close.yaml.txt new file mode 100644 index 000000000000..ee894747b5b9 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-node-close.yaml.txt @@ -0,0 +1,2 @@ +# mode=strict +bad/yaml-node-close.yaml:30: example 0 [node-close-alone] closing brace must be on its own line diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-prop-order.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-prop-order.yaml.txt new file mode 100644 index 000000000000..578df7209170 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-prop-order.yaml.txt @@ -0,0 +1,2 @@ +# mode=strict +bad/yaml-prop-order.yaml:28: example 0 [property-order] property 'compatible' out of canonical order (should sort before 'reg') diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-prop-pairing.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-prop-pairing.yaml.txt new file mode 100644 index 000000000000..e6e21349a939 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-prop-pairing.yaml.txt @@ -0,0 +1,3 @@ +# mode=strict +bad/yaml-prop-pairing.yaml:30: example 0 [property-order] property 'clocks' out of canonical order (should sort before 'clock-names') +bad/yaml-prop-pairing.yaml:32: example 0 [property-order] property 'pinctrl-0' out of canonical order (should sort before 'pinctrl-names') diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-required-blank.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-required-blank.yaml.txt new file mode 100644 index 000000000000..04ea0bacdcb9 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-required-blank.yaml.txt @@ -0,0 +1,3 @@ +# mode=strict +bad/yaml-required-blank.yaml:29: example 0 [required-blank-lines] "status" must be preceded by a blank line +bad/yaml-required-blank.yaml:30: example 0 [required-blank-lines] child node must be preceded by a blank line diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-tab.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-tab.yaml.txt new file mode 100644 index 000000000000..9e83246fbaa1 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-tab.yaml.txt @@ -0,0 +1,2 @@ +# mode=relaxed +bad/yaml-tab.yaml:28: example 0 [tab-in-dts] tab character not allowed in DTS example diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-trailing-comment.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-trailing-comment.yaml.txt new file mode 100644 index 000000000000..69dbb1d03239 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-trailing-comment.yaml.txt @@ -0,0 +1,2 @@ +# mode=strict +bad/yaml-trailing-comment.yaml:25: example 0 [property-order] property 'compatible' out of canonical order (should sort before 'reg') diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-trailing-ws.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-trailing-ws.yaml.txt new file mode 100644 index 000000000000..cfdbc8476c73 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-trailing-ws.yaml.txt @@ -0,0 +1,2 @@ +# mode=relaxed +bad/yaml-trailing-ws.yaml:27: example 0 [trailing-whitespace] trailing whitespace diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-unclosed-comment.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-unclosed-comment.yaml.txt new file mode 100644 index 000000000000..9a30ee7145e6 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-unclosed-comment.yaml.txt @@ -0,0 +1,2 @@ +# mode=strict +bad/yaml-unclosed-comment.yaml:26: example 0 [unclosed-block-comment] unclosed /* block comment diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-unit-addr-prefix.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-unit-addr-prefix.yaml.txt new file mode 100644 index 000000000000..8dec6c1176b5 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-unit-addr-prefix.yaml.txt @@ -0,0 +1,2 @@ +# mode=strict +bad/yaml-unit-addr-prefix.yaml:26: example 0 [unit-address-format] unit address '0x1000' must not have a "0x" prefix diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-unit-addr.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-unit-addr.yaml.txt new file mode 100644 index 000000000000..b52f0ef20bee --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-unit-addr.yaml.txt @@ -0,0 +1,2 @@ +# mode=strict +bad/yaml-unit-addr.yaml:26: example 0 [unit-address-format] unit address '01000' has leading zeros diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-unused-label.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-unused-label.yaml.txt new file mode 100644 index 000000000000..4f00202f0902 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-unused-label.yaml.txt @@ -0,0 +1,2 @@ +# mode=strict +bad/yaml-unused-label.yaml:26: example 0 [unused-labels] label 'dev' defined but never &-referenced diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-value-ws-multiline.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-value-ws-multiline.yaml.txt new file mode 100644 index 000000000000..3df55b1762d0 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-value-ws-multiline.yaml.txt @@ -0,0 +1,2 @@ +# mode=strict +bad/yaml-value-ws-multiline.yaml:25: example 0 [value-whitespace] extra whitespace inside <...> diff --git a/scripts/dtc/dt-style-selftest/expected/yaml-value-ws.yaml.txt b/scripts/dtc/dt-style-selftest/expected/yaml-value-ws.yaml.txt new file mode 100644 index 000000000000..cbb5f88fe85f --- /dev/null +++ b/scripts/dtc/dt-style-selftest/expected/yaml-value-ws.yaml.txt @@ -0,0 +1,2 @@ +# mode=strict +bad/yaml-value-ws.yaml:28: example 0 [value-whitespace] extra whitespace inside <...> diff --git a/scripts/dtc/dt-style-selftest/good/dts-cont-align.dts b/scripts/dtc/dt-style-selftest/good/dts-cont-align.dts new file mode 100644 index 000000000000..36fb4eefcd83 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/good/dts-cont-align.dts @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +/* + * Test fixture: tab-indented .dts with a tab-and-space aligned + * multi-line property. Continuation lines mix tabs for indent and + * spaces for alignment by design; that must not be flagged. + */ + +/dts-v1/; + +/ { + compatible = "example,test-board"; + #address-cells = <1>; + #size-cells = <1>; + + interrupt-controller@10000 { + compatible = "example,intc"; + reg = <0x10000 0x1000>; + interrupts = <1 2 3>, + <4 5 6>, + <7 8 9>; + pinmux = < + 0x01 + 0x02 + >; + }; +}; diff --git a/scripts/dtc/dt-style-selftest/good/dts-tab.dts b/scripts/dtc/dt-style-selftest/good/dts-tab.dts new file mode 100644 index 000000000000..ab7b5d1242ba --- /dev/null +++ b/scripts/dtc/dt-style-selftest/good/dts-tab.dts @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +/* + * Test fixture: a properly formatted .dts using one-tab indent. + */ + +/dts-v1/; + +/ { + compatible = "example,test-board"; + #address-cells = <1>; + #size-cells = <1>; + + bus@10000 { + compatible = "simple-bus"; + reg = <0x10000 0x1000>; + #address-cells = <1>; + #size-cells = <1>; + + device@100 { + compatible = "example,test"; + reg = <0x100 0x10>; + }; + + device@200 { + compatible = "example,test"; + reg = <0x200 0x10>; + }; + }; +}; diff --git a/scripts/dtc/dt-style-selftest/good/yaml-4space.yaml b/scripts/dtc/dt-style-selftest/good/yaml-4space.yaml new file mode 100644 index 000000000000..1502f803c24c --- /dev/null +++ b/scripts/dtc/dt-style-selftest/good/yaml-4space.yaml @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-good-4space.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture for dt-check-style + +maintainers: + - Test User + +properties: + compatible: + const: example,test-4space + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + bus@10000 { + compatible = "simple-bus"; + reg = <0x10000 0x1000>; + #address-cells = <1>; + #size-cells = <1>; + + device@100 { + compatible = "example,test-4space"; + reg = <0x100 0x10>; + }; + + device@200 { + compatible = "example,test-4space"; + reg = <0x200 0x10>; + }; + }; diff --git a/scripts/dtc/dt-style-selftest/good/yaml-tricky-parsing.yaml b/scripts/dtc/dt-style-selftest/good/yaml-tricky-parsing.yaml new file mode 100644 index 000000000000..a836d5f36b93 --- /dev/null +++ b/scripts/dtc/dt-style-selftest/good/yaml-tricky-parsing.yaml @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/test-good-tricky-parsing.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Test fixture exercising parser corner cases + +description: | + Covers patterns that previously broke the classifier: + - "/* ... */ code;" with code following the closing of a one-line + block comment must still parse the code. + - a label on a node whose name starts with a digit (1wire@10). + - a multi-line C preprocessor macro using backslash continuations + must not be parsed as DTS lines. + - a /* comment that opens and closes on the same code line must + not leave the parser in block-comment state. + +maintainers: + - Test User + +properties: + compatible: + const: example,test-tricky-parsing + reg: + maxItems: 1 + +required: + - compatible + - reg + +additionalProperties: false + +examples: + - | + #define MY_REG(a, b) \ + ((a) << 16 | \ + (b) << 0) + + one_wire: 1wire@10 { + compatible = "example,test-tricky-parsing"; + reg = ; + /* inline-closed */ status = "okay"; + }; + + other: device@20 { + compatible = "example,test-tricky-parsing"; + reg = <0x20 0x10>; + }; + + &one_wire { + status = "okay"; + }; + + &other { + status = "okay"; + }; diff --git a/scripts/dtc/dt-style-selftest/run.sh b/scripts/dtc/dt-style-selftest/run.sh new file mode 100755 index 000000000000..8117dd9be90a --- /dev/null +++ b/scripts/dtc/dt-style-selftest/run.sh @@ -0,0 +1,71 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-only +# +# Run dt-check-style against fixtures under good/ and bad/. +# good/ files must produce no output and exit 0 in both modes. +# bad/ files must produce the expected output (in expected/.txt) +# and exit 1. +# +# The mode used for a bad fixture is whichever produces a violation: +# trailing-whitespace and tab fixtures use the default (relaxed), +# the rest use --mode=strict. The expected output files name the +# mode in their first line. + +set -u + +here=$(cd "$(dirname "$0")" && pwd) +tool="$here/../dt-check-style" +fail=0 + +run() { + file=$1 + mode=$2 + "$tool" --mode="$mode" "$file" 2>&1 +} + +# good/ -- must exit 0 and produce no output in both modes +for f in "$here"/good/*; do + [ -e "$f" ] || continue + for mode in relaxed strict; do + out=$(run "$f" "$mode") + rc=$? + if [ -n "$out" ] || [ "$rc" -ne 0 ]; then + echo "FAIL good/$mode: $(basename "$f") (exit $rc, want 0):" + echo "$out" | sed 's/^/ /' + fail=$((fail + 1)) + fi + done +done + +# bad/ -- must match expected/.txt +for f in "$here"/bad/*; do + [ -e "$f" ] || continue + name=$(basename "$f") + expected="$here/expected/$name.txt" + if [ ! -f "$expected" ]; then + echo "FAIL bad: missing $expected" + fail=$((fail + 1)) + continue + fi + mode=$(head -1 "$expected" | sed 's/^# mode=//') + body=$(tail -n +2 "$expected") + out=$(run "$f" "$mode") + rc=$? + # Strip the directory prefix so expected files are portable. + out=$(printf '%s\n' "$out" | sed "s|$here/bad/|bad/|g") + if [ "$out" != "$body" ] || [ "$rc" -ne 1 ]; then + echo "FAIL bad/$mode: $name (exit $rc, want 1):" + bf=$(mktemp) + printf '%s\n' "$body" > "$bf" + printf '%s\n' "$out" | diff -u "$bf" - | sed 's/^/ /' + rm -f "$bf" + fail=$((fail + 1)) + fi +done + +if [ "$fail" -eq 0 ]; then + echo "PASS" + exit 0 +fi +echo "FAILED ($fail)" +exit 1 -- cgit v1.2.3 From 66cbd504306bf354231b1fbe43585b9aaa242d28 Mon Sep 17 00:00:00 2001 From: Cryolitia PukNgae Date: Fri, 5 Jun 2026 14:57:04 +0800 Subject: checkpatch: cuppress warnings when Reported-by: is followed by Link: > The tag should be followed by a Closes: tag pointing to the report, > unless the report is not available on the web. The Link: tag can be > used instead of Closes: if the patch fixes a part of the issue(s) > being reported. According to Documentation/process/submitting-patches.rst, Link: is also acceptable to follow a Reported-by:, if the patch fixes a part of the issue(s) being reported. Link: https://lore.kernel.org/20260605-checkpatch-v1-1-8c68ae618513@linux.dev Signed-off-by: Cryolitia PukNgae Reviewed-by: Petr Vorel Cc: Andy Whitcroft Cc: Cheng Nie Cc: Dwaipayan Ray Cc: Joe Perches Cc: Lukas Bulwahn Signed-off-by: Andrew Morton --- scripts/checkpatch.pl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'scripts') diff --git a/scripts/checkpatch.pl b/scripts/checkpatch.pl index 3727156e4cca..d9af266c63df 100755 --- a/scripts/checkpatch.pl +++ b/scripts/checkpatch.pl @@ -3253,10 +3253,10 @@ sub process { if ($sign_off =~ /^reported(?:|-and-tested)-by:$/i) { if (!defined $lines[$linenr]) { WARN("BAD_REPORTED_BY_LINK", - "Reported-by: should be immediately followed by Closes: with a URL to the report\n" . $herecurr . "\n"); - } elsif ($rawlines[$linenr] !~ /^closes:\s*/i) { + "Reported-by: should be immediately followed by Closes: or Link: with a URL to the report\n" . $herecurr . "\n"); + } elsif ($rawlines[$linenr] !~ /^(closes|link):\s*/i) { WARN("BAD_REPORTED_BY_LINK", - "Reported-by: should be immediately followed by Closes: with a URL to the report\n" . $herecurr . $rawlines[$linenr] . "\n"); + "Reported-by: should be immediately followed by Closes: or Link: with a URL to the report\n" . $herecurr . $rawlines[$linenr] . "\n"); } } } -- cgit v1.2.3 From 3429ae7b2f02a4a6ad40d36ee06641d433d75a1b Mon Sep 17 00:00:00 2001 From: Armin Wolf Date: Wed, 10 Jun 2026 22:34:53 +0200 Subject: modpost: Handle malformed WMI GUID strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some WMI GUIDs found inside binary MOF files contain both uppercase and lowercase characters. Blindly copying such GUIDs will prevent the associated WMI driver from loading automatically because the WMI GUID found inside WMI device ids always contains uppercase characters. Avoid this issue by always converting WMI GUID strings to uppercase. Also verify that the WMI GUID string actually looks like a valid GUID. Signed-off-by: Armin Wolf Reviewed-by: Mario Limonciello Link: https://patch.msgid.link/20260610203453.816254-10-W_Armin@gmx.de Reviewed-by: Ilpo Järvinen Signed-off-by: Ilpo Järvinen --- Documentation/wmi/driver-development-guide.rst | 2 +- scripts/mod/file2alias.c | 28 +++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/Documentation/wmi/driver-development-guide.rst b/Documentation/wmi/driver-development-guide.rst index 387f508d57ad..6290c448f5e7 100644 --- a/Documentation/wmi/driver-development-guide.rst +++ b/Documentation/wmi/driver-development-guide.rst @@ -54,7 +54,7 @@ to matching WMI devices using a struct wmi_device_id table: :: static const struct wmi_device_id foo_id_table[] = { - /* Only use uppercase letters! */ + /* Using only uppercase letters is recommended */ { "936DA01F-9ABD-4D9D-80C7-02AF85C822A8", NULL }, { } }; diff --git a/scripts/mod/file2alias.c b/scripts/mod/file2alias.c index 4e99393a35f1..20e542a888c4 100644 --- a/scripts/mod/file2alias.c +++ b/scripts/mod/file2alias.c @@ -1253,6 +1253,8 @@ static void do_tee_entry(struct module *mod, void *symval) static void do_wmi_entry(struct module *mod, void *symval) { DEF_FIELD_ADDR(symval, wmi_device_id, guid_string); + char result[sizeof(*guid_string)]; + int i; if (strlen(*guid_string) != UUID_STRING_LEN) { warn("Invalid WMI device id 'wmi:%s' in '%s'\n", @@ -1260,7 +1262,31 @@ static void do_wmi_entry(struct module *mod, void *symval) return; } - module_alias_printf(mod, false, WMI_MODULE_PREFIX "%s", *guid_string); + for (i = 0; i < UUID_STRING_LEN; i++) { + char value = (*guid_string)[i]; + bool valid = false; + + if (i == 8 || i == 13 || i == 18 || i == 23) { + if (value == '-') + valid = true; + } else { + if (isxdigit(value)) + valid = true; + } + + if (!valid) { + warn("Invalid character %c inside WMI GUID string '%s' in '%s'\n", + value, *guid_string, mod->name); + return; + } + + /* Some GUIDs from BMOF definitions contain lowercase characters */ + result[i] = toupper(value); + } + + result[i] = '\0'; + + module_alias_printf(mod, false, WMI_MODULE_PREFIX "%s", result); } /* Looks like: mhi:S */ -- cgit v1.2.3 From 07669b0abe4ce76c716e8437e198e1337cf43d1f Mon Sep 17 00:00:00 2001 From: Shardul Deshpande Date: Fri, 12 Jun 2026 23:46:32 +0530 Subject: treewide: fix transposed "sign" typos and update spelling.txt Several comments transpose the letters in "assigned" and "unsigned", spelling them with "sing" instead of "sign". Correct all of them. Of these, the misspelling of "assigned" is not yet flagged by checkpatch, so also add it to scripts/spelling.txt. The remaining matches of `grep -ri singed` are RISINGEDGE register and enum names, not typos. Link: https://lore.kernel.org/20260612181633.734458-1-iamsharduld@gmail.com Signed-off-by: Shardul Deshpande Suggested-by: Andrew Morton Reviewed-by: SeongJae Park Cc: Joe Perches Signed-off-by: Andrew Morton --- arch/arm64/boot/dts/qcom/sc7280-qcard.dtsi | 2 +- drivers/gpu/drm/drm_color_mgmt.c | 2 +- drivers/scsi/elx/efct/efct_hw.h | 2 +- drivers/scsi/isci/host.c | 2 +- drivers/scsi/isci/port.c | 2 +- drivers/scsi/isci/port_config.c | 2 +- kernel/bpf/helpers.c | 2 +- scripts/spelling.txt | 1 + 8 files changed, 8 insertions(+), 7 deletions(-) (limited to 'scripts') diff --git a/arch/arm64/boot/dts/qcom/sc7280-qcard.dtsi b/arch/arm64/boot/dts/qcom/sc7280-qcard.dtsi index 469a5d103e3d..9ae9af40f4d2 100644 --- a/arch/arm64/boot/dts/qcom/sc7280-qcard.dtsi +++ b/arch/arm64/boot/dts/qcom/sc7280-qcard.dtsi @@ -562,7 +562,7 @@ mos_bt_uart: &uart7 { * * This has entries that are defined by Qcard even if they go to the main * board. In cases where the pulls may be board dependent we defer those - * settings to the board device tree. Drive strengths tend to be assinged here + * settings to the board device tree. Drive strengths tend to be assigned here * but could conceivably be overwridden by board device trees. */ diff --git a/drivers/gpu/drm/drm_color_mgmt.c b/drivers/gpu/drm/drm_color_mgmt.c index e7db4e4ea700..cb7f51ff83bf 100644 --- a/drivers/gpu/drm/drm_color_mgmt.c +++ b/drivers/gpu/drm/drm_color_mgmt.c @@ -54,7 +54,7 @@ * &drm_crtc_state.degamma_lut. * * “DEGAMMA_LUT_SIZE”: - * Unsinged range property to give the size of the lookup table to be set + * Unsigned range property to give the size of the lookup table to be set * on the DEGAMMA_LUT property (the size depends on the underlying * hardware). If drivers support multiple LUT sizes then they should * publish the largest size, and sub-sample smaller sized LUTs (e.g. for diff --git a/drivers/scsi/elx/efct/efct_hw.h b/drivers/scsi/elx/efct/efct_hw.h index f3f4aa78dce9..20aae04021aa 100644 --- a/drivers/scsi/elx/efct/efct_hw.h +++ b/drivers/scsi/elx/efct/efct_hw.h @@ -59,7 +59,7 @@ #define EFCT_HW_EQ_DEPTH 1024 /* - * A CQ will be assinged to each WQ + * A CQ will be assigned to each WQ * (CQ must have 2X entries of the WQ for abort * processing), plus a separate one for each RQ PAIR and one for MQ */ diff --git a/drivers/scsi/isci/host.c b/drivers/scsi/isci/host.c index ff199bab5d1a..1592fc552e6c 100644 --- a/drivers/scsi/isci/host.c +++ b/drivers/scsi/isci/host.c @@ -2488,7 +2488,7 @@ struct isci_request *sci_request_by_tag(struct isci_host *ihost, u16 io_tag) * free remote node ids * @idev: This is the device object which is requesting the a remote node * id - * @node_id: This is the remote node id that is assinged to the device if one + * @node_id: This is the remote node id that is assigned to the device if one * is available * * enum sci_status SCI_FAILURE_OUT_OF_RESOURCES if there are no available remote diff --git a/drivers/scsi/isci/port.c b/drivers/scsi/isci/port.c index 10bd2aac2cb4..0dea0abb9927 100644 --- a/drivers/scsi/isci/port.c +++ b/drivers/scsi/isci/port.c @@ -464,7 +464,7 @@ static enum sci_status sci_port_set_phy(struct isci_port *iport, struct isci_phy { /* Check to see if we can add this phy to a port * that means that the phy is not part of a port and that the port does - * not already have a phy assinged to the phy index. + * not already have a phy assigned to the phy index. */ if (!iport->phy_table[iphy->phy_index] && !phy_get_non_dummy_port(iphy) && diff --git a/drivers/scsi/isci/port_config.c b/drivers/scsi/isci/port_config.c index 3b4820defe63..d709c9df823c 100644 --- a/drivers/scsi/isci/port_config.c +++ b/drivers/scsi/isci/port_config.c @@ -259,7 +259,7 @@ sci_mpc_agent_validate_phy_configuration(struct isci_host *ihost, if (!phy_mask) continue; /* - * Make sure that one or more of the phys were not already assinged to + * Make sure that one or more of the phys were not already assigned to * a different port. */ if ((phy_mask & ~assigned_phy_mask) == 0) { return SCI_FAILURE_UNSUPPORTED_PORT_CONFIGURATION; diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c index b5314c9fed3c..6f56ba88fb61 100644 --- a/kernel/bpf/helpers.c +++ b/kernel/bpf/helpers.c @@ -3601,7 +3601,7 @@ __bpf_kfunc int bpf_copy_from_user_task_str(void *dst, u32 dst__sz, return ret + 1; } -/* Keep unsinged long in prototype so that kfunc is usable when emitted to +/* Keep unsigned long in prototype so that kfunc is usable when emitted to * vmlinux.h in BPF programs directly, but note that while in BPF prog, the * unsigned long always points to 8-byte region on stack, the kernel may only * read and write the 4-bytes on 32-bit. diff --git a/scripts/spelling.txt b/scripts/spelling.txt index 2f2e81dbda03..3372873cd7bb 100644 --- a/scripts/spelling.txt +++ b/scripts/spelling.txt @@ -176,6 +176,7 @@ assgined||assigned assiged||assigned assigment||assignment assigments||assignments +assinged||assigned assistent||assistant assocaited||associated assocated||associated -- cgit v1.2.3 From 9a289cc425bf469642533e4afa01c90f08971d01 Mon Sep 17 00:00:00 2001 From: Rong Xu Date: Wed, 17 Jun 2026 15:46:23 -0700 Subject: modpost: Ignore Clang LTO suffixes in symbol matching When building the kernel with Clang ThinLTO enabled, the compiler can mangle static variable names by appending suffixes such as ".llvm." to prevent naming collisions across translation units. This name mangling breaks the section mismatch whitelisting in modpost. modpost relies on glob patterns (e.g., "*_ops" or "*_probe") to identify safe references between permanent data and initialization code. Because the LTO suffix modifies the end of the symbol name, legitimately whitelisted structures fail the match, resulting in false positive warnings. For example, a static pernet_operations struct triggers the following: WARNING: modpost: vmlinux: section mismatch in reference: \ ping_v4_net_ops.llvm.5641696707737373282 (section: .data) -> \ ping_v4_proc_init_net (section: .init.text) Fix this by ignoring "*_ops.llvm.*" in "from" symbol names (the same as "*_ops"). Reported-by: kernel test robot Closes: https://lore.kernel.org/oe-kbuild-all/202606111233.kM8oo8Df-lkp@intel.com/ Signed-off-by: Rong Xu Link: https://patch.msgid.link/20260617224623.1346309-1-xur@google.com Signed-off-by: Nathan Chancellor --- scripts/mod/modpost.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/mod/modpost.c b/scripts/mod/modpost.c index d592548cbd60..a7b72a81d248 100644 --- a/scripts/mod/modpost.c +++ b/scripts/mod/modpost.c @@ -969,7 +969,7 @@ static int secref_whitelist(const char *fromsec, const char *fromsym, /* symbols in data sections that may refer to any init/exit sections */ if (match(fromsec, PATTERNS(DATA_SECTIONS)) && match(tosec, PATTERNS(ALL_INIT_SECTIONS, ALL_EXIT_SECTIONS)) && - match(fromsym, PATTERNS("*_ops", "*_console"))) + match(fromsym, PATTERNS("*_ops", "*_ops.llvm.*", "*_console"))) return 0; /* Check for pattern 3 */ -- cgit v1.2.3 From 645323a7f4e55bb3abb0cb003b6b9dc715c8dc21 Mon Sep 17 00:00:00 2001 From: Pengpeng Hou Date: Thu, 11 Jun 2026 14:00:00 +0800 Subject: kconfig: add optional warnings for changed input values When reading .config input, Kconfig stores user-provided values first and then resolves the final value after applying dependencies, ranges, and other constraints. If the final value differs from the user input, Kconfig already tracks that state internally, but it does not provide a focused diagnostic to show which explicit inputs were adjusted. This is particularly confusing for requested values that get forced down by unmet dependencies or clamped by ranges. Add an opt-in diagnostic controlled by KCONFIG_WARN_CHANGED_INPUT. Emit the warnings from conf_write() and conf_write_defconfig() after value resolution. Print the diagnostic to stderr directly, not through the normal message callback, so it remains visible when conf is run with -s, such as from make -s. Keep the diagnostic out of the conf_message() formatting buffer so long warning lists are not truncated, and mark processed symbols as written before the SYMBOL_WRITE check so duplicate menu nodes cannot emit duplicate warnings. Document the new environment variable and add tests for olddefconfig, savedefconfig, and the silent-conf path. Signed-off-by: Pengpeng Hou Tested-by: Julian Braha Link: https://patch.msgid.link/20260611060000.23858-1-pengpeng@iscas.ac.cn Signed-off-by: Nathan Chancellor --- Documentation/kbuild/kconfig.rst | 5 + scripts/kconfig/confdata.c | 106 ++++++++++++++++++++- scripts/kconfig/tests/conftest.py | 8 +- scripts/kconfig/tests/warn_changed_input/Kconfig | 40 ++++++++ .../kconfig/tests/warn_changed_input/__init__.py | 33 +++++++ scripts/kconfig/tests/warn_changed_input/config | 3 + .../tests/warn_changed_input/expected_config | 6 ++ .../tests/warn_changed_input/expected_defconfig | 1 + .../tests/warn_changed_input/expected_stderr | 4 + 9 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 scripts/kconfig/tests/warn_changed_input/Kconfig create mode 100644 scripts/kconfig/tests/warn_changed_input/__init__.py create mode 100644 scripts/kconfig/tests/warn_changed_input/config create mode 100644 scripts/kconfig/tests/warn_changed_input/expected_config create mode 100644 scripts/kconfig/tests/warn_changed_input/expected_defconfig create mode 100644 scripts/kconfig/tests/warn_changed_input/expected_stderr (limited to 'scripts') diff --git a/Documentation/kbuild/kconfig.rst b/Documentation/kbuild/kconfig.rst index d213c4f599a4..e35dd1d5f9d3 100644 --- a/Documentation/kbuild/kconfig.rst +++ b/Documentation/kbuild/kconfig.rst @@ -59,6 +59,11 @@ Environment variables for ``*config``: This environment variable makes Kconfig warn about all unrecognized symbols in the config input. +``KCONFIG_WARN_CHANGED_INPUT`` + If set to a non-blank value, Kconfig prints optional warnings for + user-provided values that change after Kconfig resolves dependencies + or applies other constraints such as ranges. + ``KCONFIG_WERROR`` If set, Kconfig treats warnings as errors. diff --git a/scripts/kconfig/confdata.c b/scripts/kconfig/confdata.c index 9599a0408862..4234a51d16fd 100644 --- a/scripts/kconfig/confdata.c +++ b/scripts/kconfig/confdata.c @@ -206,6 +206,78 @@ static void conf_message(const char *fmt, ...) va_end(ap); } +static void conf_changed_input_warning(const char *s) +{ + fputs(s, stderr); +} + +static bool conf_warn_changed_input_enabled(void) +{ + const char *env = getenv("KCONFIG_WARN_CHANGED_INPUT"); + + return env && *env; +} + +static const char *sym_get_user_value_string(struct symbol *sym) +{ + switch (sym->type) { + case S_BOOLEAN: + case S_TRISTATE: + switch (sym->def[S_DEF_USER].tri) { + case yes: + return "y"; + case mod: + return "m"; + default: + return "n"; + } + default: + return sym->def[S_DEF_USER].val ?: ""; + } +} + +static bool sym_user_value_changed(struct symbol *sym) +{ + if (!sym_has_value(sym) || sym->type == S_UNKNOWN) + return false; + + switch (sym->type) { + case S_BOOLEAN: + case S_TRISTATE: + return sym->def[S_DEF_USER].tri != sym_get_tristate_value(sym); + default: + return strcmp(sym_get_user_value_string(sym), + sym_get_string_value(sym)); + } +} + +static void conf_clear_written_flags(void) +{ + struct symbol *sym; + + for_all_symbols(sym) + sym->flags &= ~SYMBOL_WRITTEN; +} + +static void conf_append_changed_input_warning(struct gstr *gs, + struct symbol *sym, + bool *changed_input_found) +{ + if (!sym_user_value_changed(sym)) + return; + + if (!*changed_input_found) { + str_printf(gs, + "warning: user-provided values changed by Kconfig:\n"); + *changed_input_found = true; + } + + str_printf(gs, " %s%s: %s -> %s\n", + CONFIG_, sym->name, + sym_get_user_value_string(sym), + sym_get_string_value(sym)); +} + const char *conf_get_configname(void) { char *name = getenv("KCONFIG_CONFIG"); @@ -759,11 +831,15 @@ int conf_write_defconfig(const char *filename) { struct symbol *sym; struct menu *menu; + struct gstr gs; FILE *out; + bool warn_changed_input = conf_warn_changed_input_enabled(); + bool changed_input_found = false; out = fopen(filename, "w"); if (!out) return 1; + gs = str_new(); sym_clear_all_valid(); @@ -772,10 +848,14 @@ int conf_write_defconfig(const char *filename) sym = menu->sym; - if (!sym || sym_is_choice(sym)) + if (!sym || sym_is_choice(sym) || sym->flags & SYMBOL_WRITTEN) continue; sym_calc_value(sym); + if (warn_changed_input) + conf_append_changed_input_warning(&gs, sym, + &changed_input_found); + sym->flags |= SYMBOL_WRITTEN; if (!(sym->flags & SYMBOL_WRITE)) continue; sym->flags &= ~SYMBOL_WRITE; @@ -798,6 +878,13 @@ int conf_write_defconfig(const char *filename) print_symbol_for_dotconfig(out, sym); } fclose(out); + + conf_clear_written_flags(); + + if (changed_input_found) + conf_changed_input_warning(str_get(&gs)); + + str_free(&gs); return 0; } @@ -809,7 +896,10 @@ int conf_write(const char *name) const char *str; char tmpname[PATH_MAX + 1], oldname[PATH_MAX + 1]; char *env; + struct gstr gs; bool need_newline = false; + bool warn_changed_input = conf_warn_changed_input_enabled(); + bool changed_input_found = false; if (!name) name = conf_get_configname(); @@ -838,6 +928,7 @@ int conf_write(const char *name) } if (!out) return 1; + gs = str_new(); conf_write_heading(out, &comment_style_pound); @@ -859,13 +950,16 @@ int conf_write(const char *name) } else if (!sym_is_choice(sym) && !(sym->flags & SYMBOL_WRITTEN)) { sym_calc_value(sym); + if (warn_changed_input) + conf_append_changed_input_warning(&gs, sym, + &changed_input_found); + sym->flags |= SYMBOL_WRITTEN; if (!(sym->flags & SYMBOL_WRITE)) goto next; if (need_newline) { fprintf(out, "\n"); need_newline = false; } - sym->flags |= SYMBOL_WRITTEN; print_symbol_for_dotconfig(out, sym); } @@ -892,8 +986,12 @@ end_check: } fclose(out); - for_all_symbols(sym) - sym->flags &= ~SYMBOL_WRITTEN; + conf_clear_written_flags(); + + if (changed_input_found) + conf_changed_input_warning(str_get(&gs)); + + str_free(&gs); if (*tmpname) { if (is_same(name, tmpname)) { diff --git a/scripts/kconfig/tests/conftest.py b/scripts/kconfig/tests/conftest.py index d94b79e012c0..66f95e4ed58c 100644 --- a/scripts/kconfig/tests/conftest.py +++ b/scripts/kconfig/tests/conftest.py @@ -37,7 +37,8 @@ class Conf: # runners def _run_conf(self, mode, dot_config=None, out_file='.config', - interactive=False, in_keys=None, extra_env={}): + interactive=False, in_keys=None, extra_env={}, + silent=False): """Run text-based Kconfig executable and save the result. mode: input mode option (--oldaskconfig, --defconfig= etc.) @@ -48,7 +49,10 @@ class Conf: extra_env: additional environments returncode: exit status of the Kconfig executable """ - command = [CONF_PATH, mode, 'Kconfig'] + command = [CONF_PATH] + if silent: + command.append('-s') + command += [mode, 'Kconfig'] # Override 'srctree' environment to make the test as the top directory extra_env['srctree'] = self._test_dir diff --git a/scripts/kconfig/tests/warn_changed_input/Kconfig b/scripts/kconfig/tests/warn_changed_input/Kconfig new file mode 100644 index 000000000000..69845e2f3fb3 --- /dev/null +++ b/scripts/kconfig/tests/warn_changed_input/Kconfig @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: GPL-2.0 + +config DEP + bool "DEP" + help + Test dependency symbol for Kconfig warning coverage. + This is used by the warn_changed_input selftest. + It intentionally stays unset in the input fragment. + The test checks how dependent user input is adjusted. + +config A + bool "A" + depends on DEP + help + Test bool symbol for changed-input diagnostics. + The input fragment requests this symbol as built-in. + The unmet dependency on DEP forces the final value to n. + The warning should report that downgrade. + +config NUM + int "NUM" + range 10 20 + help + Test integer symbol for changed-input diagnostics. + The input fragment requests a value outside the allowed range. + Kconfig resolves it to the constrained in-range value. + The warning should report that adjustment. + +config DUP + bool "DUP" + depends on DEP + help + Test duplicate-definition handling for changed-input diagnostics. + The input fragment requests this symbol as built-in. + The duplicate definition below must not produce a duplicate warning. + This keeps the warning output stable for repeated menu entries. + +config DUP + bool + depends on DEP diff --git a/scripts/kconfig/tests/warn_changed_input/__init__.py b/scripts/kconfig/tests/warn_changed_input/__init__.py new file mode 100644 index 000000000000..4c3bca6af846 --- /dev/null +++ b/scripts/kconfig/tests/warn_changed_input/__init__.py @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: GPL-2.0 +""" +Test optional warnings for user-provided values changed by Kconfig. + +Warnings should stay disabled by default, and should only appear when +KCONFIG_WARN_CHANGED_INPUT is enabled. +""" + + +def test(conf): + assert conf.olddefconfig('config') == 0 + assert 'user-provided values changed by Kconfig' not in conf.stderr + + assert conf._run_conf('--olddefconfig', dot_config='config', + extra_env={ + 'KCONFIG_WARN_CHANGED_INPUT': '1', + }) == 0 + assert conf.stderr_contains('expected_stderr') + assert conf.config_matches('expected_config') + + assert conf._run_conf('--olddefconfig', dot_config='config', + extra_env={ + 'KCONFIG_WARN_CHANGED_INPUT': '1', + }, silent=True) == 0 + assert conf.stderr_contains('expected_stderr') + + assert conf._run_conf('--savedefconfig=defconfig', dot_config='config', + out_file='defconfig', + extra_env={ + 'KCONFIG_WARN_CHANGED_INPUT': '1', + }) == 0 + assert conf.stderr_contains('expected_stderr') + assert conf.config_matches('expected_defconfig') diff --git a/scripts/kconfig/tests/warn_changed_input/config b/scripts/kconfig/tests/warn_changed_input/config new file mode 100644 index 000000000000..dbe93ff26408 --- /dev/null +++ b/scripts/kconfig/tests/warn_changed_input/config @@ -0,0 +1,3 @@ +CONFIG_A=y +CONFIG_NUM=30 +CONFIG_DUP=y diff --git a/scripts/kconfig/tests/warn_changed_input/expected_config b/scripts/kconfig/tests/warn_changed_input/expected_config new file mode 100644 index 000000000000..fe8bbec66c53 --- /dev/null +++ b/scripts/kconfig/tests/warn_changed_input/expected_config @@ -0,0 +1,6 @@ +# +# Automatically generated file; DO NOT EDIT. +# Main menu +# +# CONFIG_DEP is not set +CONFIG_NUM=20 diff --git a/scripts/kconfig/tests/warn_changed_input/expected_defconfig b/scripts/kconfig/tests/warn_changed_input/expected_defconfig new file mode 100644 index 000000000000..af9e34851d2a --- /dev/null +++ b/scripts/kconfig/tests/warn_changed_input/expected_defconfig @@ -0,0 +1 @@ +CONFIG_NUM=20 diff --git a/scripts/kconfig/tests/warn_changed_input/expected_stderr b/scripts/kconfig/tests/warn_changed_input/expected_stderr new file mode 100644 index 000000000000..9ec8446b4ac2 --- /dev/null +++ b/scripts/kconfig/tests/warn_changed_input/expected_stderr @@ -0,0 +1,4 @@ +warning: user-provided values changed by Kconfig: + CONFIG_A: y -> n + CONFIG_NUM: 30 -> 20 + CONFIG_DUP: y -> n -- cgit v1.2.3 From ac4d1caa82d487e7ed46d0597da1adc9c1a51c70 Mon Sep 17 00:00:00 2001 From: Gary Guo Date: Tue, 16 Jun 2026 14:25:56 +0100 Subject: rust: doctest: fix incorrect pattern in replacement The `-> Result<(), impl core::fmt::Debug>` string is generated by rustdoc and by adding "::" into the string it no longer finds anything, making the line useless. Remove the "::" in the pattern. Omit it in the replacement too, for consistency with upstream rustdoc. Fixes: de7cd3e4d638 ("rust: use absolute paths in macros referencing core and kernel") Signed-off-by: Gary Guo Cc: stable@vger.kernel.org Link: https://patch.msgid.link/20260616132559.2245814-1-gary@kernel.org [ Added link in code comment to `rustdoc`'s 1.87 PR that fully qualified it for context. Improved comments for consistency. Reworded to drop changelog and to fix typo. - Miguel ] Signed-off-by: Miguel Ojeda --- scripts/rustdoc_test_builder.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'scripts') diff --git a/scripts/rustdoc_test_builder.rs b/scripts/rustdoc_test_builder.rs index f7540bcf595a..df864437cef7 100644 --- a/scripts/rustdoc_test_builder.rs +++ b/scripts/rustdoc_test_builder.rs @@ -28,7 +28,7 @@ fn main() { // // ``` // fn main() { #[allow(non_snake_case)] fn _doctest_main_rust_kernel_file_rs_28_0() { - // fn main() { #[allow(non_snake_case)] fn _doctest_main_rust_kernel_file_rs_37_0() -> Result<(), impl ::core::fmt::Debug> { + // fn main() { #[allow(non_snake_case)] fn _doctest_main_rust_kernel_file_rs_37_0() -> Result<(), impl core::fmt::Debug> { // ``` // // It should be unlikely that doctest code matches such lines (when code is formatted properly). @@ -47,12 +47,16 @@ fn main() { }) .expect("No test function found in `rustdoc`'s output."); - // Qualify `Result` to avoid the collision with our own `Result` coming from the prelude. + // Replicate `rustdoc` 1.87+ behaviour [1] by fully qualifying `Result` to avoid the collision + // with our own `Result` coming from the prelude. + // + // [1]: https://github.com/rust-lang/rust/pull/137807 + // + // TODO: Remove this when MSRV is bumped above 1.87. let body = body.replace( - &format!("{rustdoc_function_name}() -> Result<(), impl ::core::fmt::Debug> {{"), - &format!( - "{rustdoc_function_name}() -> ::core::result::Result<(), impl ::core::fmt::Debug> {{" - ), + &format!("{rustdoc_function_name}() -> Result<(), impl core::fmt::Debug> {{"), + // This intentionally does not use absolute paths to match `rustdoc` 1.87 behaviour. + &format!("{rustdoc_function_name}() -> core::result::Result<(), impl core::fmt::Debug> {{"), ); // For tests that get generated with `Result`, like above, `rustdoc` generates an `unwrap()` on -- cgit v1.2.3 From 57ad674d032baf5426a38b0d6b2ddd60cbd3913f Mon Sep 17 00:00:00 2001 From: Wang Han Date: Tue, 9 Jun 2026 14:29:52 +0800 Subject: scripts/sorttable: Handle RISC-V patchable ftrace entries RISC-V uses -fpatchable-function-entry=8,4 when the compressed ISA is enabled and -fpatchable-function-entry=4,2 otherwise. In both cases, the patchable NOP area starts 8 bytes before the function symbol address. The __mcount_loc entries therefore point at the patchable NOP area associated with a function, while nm reports the function symbol at the entry address used for the function range check. After RISC-V selected HAVE_BUILDTIME_MCOUNT_SORT, sorttable started applying that range check at build time. Without allowing entries just before the reported function address, the mcount sorter treats valid RISC-V ftrace callsites as invalid weak-function entries and writes them back as zero. The resulting kernel boots with no ftrace entries, breaking dynamic ftrace and users such as livepatch. The failure is silent during the final link because zeroing weak-function entries is an expected sorttable operation. At boot, those zero entries are skipped by ftrace_process_locs(), so the only obvious symptom is that the vmlinux ftrace table has lost valid callsites and ftrace users cannot attach to them. CONFIG_FTRACE_SORT_STARTUP_TEST also reports the table as sorted in this state: it only checks that the __mcount_loc entries are in ascending order, which a fully zeroed table trivially satisfies. The original commit relied on this check and did not see the regression. On an affected RISC-V QEMU boot with both CONFIG_FTRACE_SORT_STARTUP_TEST and CONFIG_FTRACE_STARTUP_TEST enabled, the sort check still passes while ftrace reports zero usable entries and the early selftests fail: [ 0.000000] ftrace section at ffffffff8101da98 sorted properly [ 0.000000] ftrace: allocating 0 entries in 128 pages [ 0.054999] Testing tracer function: .. no entries found ..FAILED! [ 0.172407] tracer: function failed selftest, disabling [ 0.178186] Failed to init function_graph tracer, init returned -19 Handle RISC-V like arm64 for the function-range check and allow patchable entries up to 8 bytes before the function address. With this fix, a RISC-V QEMU smoke boot with ftrace startup tests shows the vmlinux ftrace table is populated and dynamic ftrace still works: [ 0.000000] ftrace: allocating 46749 entries in 184 pages [ 0.051115] Testing tracer function: PASSED [ 1.283782] Testing dynamic ftrace: PASSED [ 6.275456] Testing tracer function_graph: PASSED Fixes: 0ca1724b56af ("riscv: ftrace: select HAVE_BUILDTIME_MCOUNT_SORT") Suggested-by: Steven Rostedt (Google) Reviewed-by: Steven Rostedt Reviewed-by: Shuai Xue Reviewed-by: Chen Pei Link: https://lore.kernel.org/all/20260527113028.4b21a5de@fedora/ Signed-off-by: Wang Han Reviewed-by: Martin Kaiser Link: https://patch.msgid.link/20260609063002.3943001-1-wanghan@linux.alibaba.com Signed-off-by: Paul Walmsley --- scripts/sorttable.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'scripts') diff --git a/scripts/sorttable.c b/scripts/sorttable.c index e8ed11c680c6..d8dc2a1b7c31 100644 --- a/scripts/sorttable.c +++ b/scripts/sorttable.c @@ -891,17 +891,22 @@ static int do_file(char const *const fname, void *addr) table_sort_t custom_sort = NULL; switch (elf_map_machine(ehdr)) { - case EM_AARCH64: #ifdef MCOUNT_SORT_ENABLED + case EM_AARCH64: + /* arm64 also needs RELA-based weak-function fixups. */ sort_reloc = true; rela_type = 0x403; - /* arm64 uses patchable function entry placing before function */ + /* fallthrough */ + case EM_RISCV: + /* arm64 and RISC-V place patchable entries before the function. */ before_func = 8; +#else + case EM_AARCH64: + case EM_RISCV: #endif /* fallthrough */ case EM_386: case EM_LOONGARCH: - case EM_RISCV: case EM_S390: case EM_X86_64: custom_sort = sort_relative_table_with_data; -- cgit v1.2.3