* [RFC PATCH v3 1/3] scripts: add kconfirm
From: Julian Braha @ 2026-05-16 21:53 UTC (permalink / raw)
To: nathan, nsc
Cc: jani.nikula, akpm, gary, ljs, arnd, gregkh, masahiroy, ojeda,
corbet, qingfang.deng, yann.prono, demiobenour, ej, linux-kernel,
rust-for-linux, linux-doc, linux-kbuild, Julian Braha
In-Reply-To: <20260516215354.449807-1-julianbraha@gmail.com>
Add kconfirm into scripts/
kconfirm is a static analysis tool with various checks for Kconfig, and
intended to have zero false alarms by default. These default checks
currently include dead code, constant conditions, and invalid (reverse)
ranges.
There are also optional checks for dead links in the help texts, and for
config options that select visible config options.
Checks are performed on the same architecture as the kernel build, using
a single thread. More architectures can be enabled by passing
`--enable-arch`. Alarms are tagged using the architectures' config options,
like so: [X86] if specific to x86, or [X86, ARM] if the alarm appears for
both x86 and arm.
Each alarm gets a single line (deduplicated across architectures) and is
formatted like this:
[<SEVERITY>] [<ARCH_1>, <ARCH_2>] config <OPTION_NAME>: <alarm message>
The tool source contains two Rust packages: kconfirm-lib and
kconfirm-linux.
kconfirm-lib is the underlying library that analyzes Kconfig code, and
formats alarms for usability. It analyzes the entire Linux Kconfig spec,
including all architectures. This package exposes the symbol table that it
constructs so that other tools can import this library, and make use of it
for their own Kconfig analyses.
kconfirm-linux imports kconfirm-lib, and provides the CLI, which is
intended for either manual usage, or integration with the Linux build
system so that users can simply run `make kconfirm` from the root.
kconfirm-linux also handles some of the specificities of how Kconfig is
used in the Linux tree, in contrast to other open source software. E.g.
the way that each architecture has its own Kconfig and Kconfig.debug
files.
The tool's dependencies need to be downloaded from crates.io by running
`cargo vendor` in scripts/kconfirm/ before building and running the tool.
Signed-off-by: Julian Braha <julianbraha@gmail.com>
---
Makefile | 15 +-
scripts/Makefile | 2 +-
scripts/kconfirm/.gitignore | 3 +
scripts/kconfirm/Cargo.lock | 60 ++
scripts/kconfirm/Cargo.toml | 12 +
scripts/kconfirm/Makefile | 14 +
scripts/kconfirm/kconfirm-lib/Cargo.toml | 12 +
scripts/kconfirm/kconfirm-lib/src/analyze.rs | 643 ++++++++++++++++
scripts/kconfirm/kconfirm-lib/src/checks.rs | 701 ++++++++++++++++++
scripts/kconfirm/kconfirm-lib/src/curl_ffi.rs | 182 +++++
.../kconfirm/kconfirm-lib/src/dead_links.rs | 138 ++++
scripts/kconfirm/kconfirm-lib/src/lib.rs | 62 ++
scripts/kconfirm/kconfirm-lib/src/output.rs | 111 +++
.../kconfirm/kconfirm-lib/src/symbol_table.rs | 223 ++++++
scripts/kconfirm/kconfirm-linux/Cargo.toml | 10 +
.../kconfirm/kconfirm-linux/src/getopt_ffi.rs | 99 +++
scripts/kconfirm/kconfirm-linux/src/lib.rs | 78 ++
scripts/kconfirm/kconfirm-linux/src/main.rs | 192 +++++
18 files changed, 2552 insertions(+), 5 deletions(-)
create mode 100644 scripts/kconfirm/.gitignore
create mode 100644 scripts/kconfirm/Cargo.lock
create mode 100644 scripts/kconfirm/Cargo.toml
create mode 100644 scripts/kconfirm/Makefile
create mode 100644 scripts/kconfirm/kconfirm-lib/Cargo.toml
create mode 100644 scripts/kconfirm/kconfirm-lib/src/analyze.rs
create mode 100644 scripts/kconfirm/kconfirm-lib/src/checks.rs
create mode 100644 scripts/kconfirm/kconfirm-lib/src/curl_ffi.rs
create mode 100644 scripts/kconfirm/kconfirm-lib/src/dead_links.rs
create mode 100644 scripts/kconfirm/kconfirm-lib/src/lib.rs
create mode 100644 scripts/kconfirm/kconfirm-lib/src/output.rs
create mode 100644 scripts/kconfirm/kconfirm-lib/src/symbol_table.rs
create mode 100644 scripts/kconfirm/kconfirm-linux/Cargo.toml
create mode 100644 scripts/kconfirm/kconfirm-linux/src/getopt_ffi.rs
create mode 100644 scripts/kconfirm/kconfirm-linux/src/lib.rs
create mode 100644 scripts/kconfirm/kconfirm-linux/src/main.rs
diff --git a/Makefile b/Makefile
index b7b80e84e1eb..99aaed5bdbc5 100644
--- a/Makefile
+++ b/Makefile
@@ -296,7 +296,7 @@ no-dot-config-targets := $(clean-targets) \
$(version_h) headers headers_% archheaders archscripts \
%asm-generic kernelversion %src-pkg dt_binding_check \
outputmakefile rustavailable rustfmt rustfmtcheck \
- run-command
+ run-command kconfirm
no-sync-config-targets := $(no-dot-config-targets) %install modules_sign kernelrelease \
image_name
single-targets := %.a %.i %.ko %.lds %.ll %.lst %.mod %.o %.rsi %.s %/
@@ -536,6 +536,7 @@ OBJDUMP = $(CROSS_COMPILE)objdump
READELF = $(CROSS_COMPILE)readelf
STRIP = $(CROSS_COMPILE)strip
endif
+CARGO = cargo
RUSTC = rustc
RUSTDOC = rustdoc
RUSTFMT = rustfmt
@@ -633,7 +634,7 @@ export RUSTC_BOOTSTRAP := 1
export CLIPPY_CONF_DIR := $(srctree)
export ARCH SRCARCH CONFIG_SHELL BASH HOSTCC KBUILD_HOSTCFLAGS CROSS_COMPILE LD CC HOSTPKG_CONFIG
-export RUSTC RUSTDOC RUSTFMT RUSTC_OR_CLIPPY_QUIET RUSTC_OR_CLIPPY BINDGEN LLVM_LINK
+export CARGO RUSTC RUSTDOC RUSTFMT RUSTC_OR_CLIPPY_QUIET RUSTC_OR_CLIPPY BINDGEN LLVM_LINK
export HOSTRUSTC KBUILD_HOSTRUSTFLAGS
export CPP AR NM STRIP OBJCOPY OBJDUMP READELF PAHOLE RESOLVE_BTFIDS LEX YACC AWK INSTALLKERNEL
export PERL PYTHON3 CHECK CHECKFLAGS MAKE UTS_MACHINE HOSTCXX
@@ -1705,7 +1706,7 @@ MRPROPER_FILES += include/config include/generated \
vmlinux-gdb.py \
rpmbuild \
rust/libmacros.so rust/libmacros.dylib \
- rust/libpin_init_internal.so rust/libpin_init_internal.dylib
+ rust/libpin_init_internal.so rust/libpin_init_internal.dylib \
# clean - Delete most, but leave enough to build external modules
#
@@ -2227,7 +2228,7 @@ endif
# Scripts to check various things for consistency
# ---------------------------------------------------------------------------
-PHONY += includecheck versioncheck coccicheck
+PHONY += includecheck versioncheck coccicheck kconfirm
includecheck:
find $(srctree)/* $(RCS_FIND_IGNORE) \
@@ -2242,6 +2243,12 @@ versioncheck:
coccicheck:
$(Q)$(BASH) $(srctree)/scripts/$@
+
+kconfirm:
+ $(Q)$(MAKE) -C $(srctree)/scripts/kconfirm srctree=$(abs_srctree) SRCARCH=$(SRCARCH) || \
+ (printf "\n kconfirm failed to compile and run. Have you set up its dependencies yet?\n See Documentation/dev-tools/kconfirm.rst\n\n" && false)
+clean-dirs += scripts/kconfirm
+
PHONY += checkstack kernelrelease kernelversion image_name
# UML needs a little special treatment here. It wants to use the host
diff --git a/scripts/Makefile b/scripts/Makefile
index 3434a82a119f..460655bd2de1 100644
--- a/scripts/Makefile
+++ b/scripts/Makefile
@@ -66,4 +66,4 @@ subdir-$(CONFIG_SECURITY_SELINUX) += selinux
subdir-$(CONFIG_SECURITY_IPE) += ipe
# Let clean descend into subdirs
-subdir- += basic dtc gdb kconfig mod
+subdir- += basic dtc gdb kconfig kconfirm mod
diff --git a/scripts/kconfirm/.gitignore b/scripts/kconfirm/.gitignore
new file mode 100644
index 000000000000..f63ee0251591
--- /dev/null
+++ b/scripts/kconfirm/.gitignore
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-only
+/target
+/vendor
diff --git a/scripts/kconfirm/Cargo.lock b/scripts/kconfirm/Cargo.lock
new file mode 100644
index 000000000000..d90bc7d2e2a3
--- /dev/null
+++ b/scripts/kconfirm/Cargo.lock
@@ -0,0 +1,60 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "bytecount"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
+
+[[package]]
+name = "kconfirm-lib"
+version = "0.10.0"
+dependencies = [
+ "nom-kconfig",
+]
+
+[[package]]
+name = "kconfirm-linux"
+version = "0.10.0"
+dependencies = [
+ "kconfirm-lib",
+ "nom-kconfig",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "nom"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "nom-kconfig"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a0220bb2c8e5ad29b06fe0f75a276affeb10c9504726bf46d81fef78d69b1e3"
+dependencies = [
+ "nom",
+ "nom_locate",
+]
+
+[[package]]
+name = "nom_locate"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d"
+dependencies = [
+ "bytecount",
+ "memchr",
+ "nom",
+]
diff --git a/scripts/kconfirm/Cargo.toml b/scripts/kconfirm/Cargo.toml
new file mode 100644
index 000000000000..5880b06c4116
--- /dev/null
+++ b/scripts/kconfirm/Cargo.toml
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0
+[workspace]
+members = ["kconfirm-lib", "kconfirm-linux"]
+resolver = "3"
+
+[workspace.package]
+rust-version = "1.85.0"
+
+[workspace.dependencies]
+nom-kconfig = { version = "0.11", default-features = false, features = [
+ "display",
+] }
diff --git a/scripts/kconfirm/Makefile b/scripts/kconfirm/Makefile
new file mode 100644
index 000000000000..6a0b7389103e
--- /dev/null
+++ b/scripts/kconfirm/Makefile
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: GPL-2.0
+# kconfirm makefile
+
+TARGET := kconfirm
+
+# Extra arguments forwarded to kconfirm.
+# Example: make kconfirm KCONFIRM_ARGS="--enable-check dead_links"
+KCONFIRM_ARGS ?=
+
+$(TARGET):
+ $(CARGO) run --release --offline -p kconfirm-linux -- --linux-path $(srctree) --enable-arch $(SRCARCH) $(KCONFIRM_ARGS)
+
+
+clean-files += target vendor
diff --git a/scripts/kconfirm/kconfirm-lib/Cargo.toml b/scripts/kconfirm/kconfirm-lib/Cargo.toml
new file mode 100644
index 000000000000..dd3d7cb1aa1d
--- /dev/null
+++ b/scripts/kconfirm/kconfirm-lib/Cargo.toml
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0
+[package]
+name = "kconfirm-lib"
+version = "0.10.0"
+edition = "2024"
+rust-version.workspace = true
+
+[dependencies]
+nom-kconfig = { workspace = true }
+
+[features]
+default = []
diff --git a/scripts/kconfirm/kconfirm-lib/src/analyze.rs b/scripts/kconfirm/kconfirm-lib/src/analyze.rs
new file mode 100644
index 000000000000..24798581dc3d
--- /dev/null
+++ b/scripts/kconfirm/kconfirm-lib/src/analyze.rs
@@ -0,0 +1,643 @@
+// SPDX-License-Identifier: GPL-2.0-only
+use crate::AnalysisArgs;
+use crate::Check;
+use crate::SymbolTable;
+use crate::dead_links;
+use crate::dead_links::LinkStatus;
+use crate::dead_links::check_link;
+use crate::output::Finding;
+use crate::output::Severity;
+use crate::symbol_table::ChoiceData;
+use nom_kconfig::Attribute::*;
+use nom_kconfig::Entry;
+use nom_kconfig::attribute::DefaultAttribute;
+use nom_kconfig::attribute::Expression;
+use nom_kconfig::attribute::Imply;
+use nom_kconfig::attribute::Select;
+use nom_kconfig::attribute::r#type::Type;
+use nom_kconfig::entry::Choice;
+use nom_kconfig::entry::Config;
+use nom_kconfig::entry::If;
+use nom_kconfig::entry::Menu;
+use nom_kconfig::entry::Source;
+use std::collections::HashSet;
+use std::option::Option;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+enum FunctionalAttributes {
+ // only tracking the attributes that affect the semantics, e.g. not help texts
+ Dependencies,
+ Selects,
+ Implies,
+ Ranges,
+ Defaults,
+}
+
+struct AttributeGroupingChecker {
+ current_group: Option<FunctionalAttributes>,
+ finished_groups: HashSet<FunctionalAttributes>,
+}
+
+impl AttributeGroupingChecker {
+ fn new() -> Self {
+ Self {
+ current_group: None,
+ finished_groups: HashSet::new(),
+ }
+ }
+
+ // doesn't modify `findings` if the style check is disabled
+ fn check(
+ &mut self,
+ group: FunctionalAttributes,
+ args: &AnalysisArgs,
+ findings: &mut Vec<Finding>,
+ symbol: &str,
+ arch: &String,
+ message: String,
+ ) {
+ if !args.is_enabled(Check::UngroupedAttribute) {
+ return;
+ }
+
+ match self.current_group {
+ // still contiguous
+ Some(current) if current == group => {}
+
+ // start of group
+ None => {
+ self.current_group = Some(group);
+ }
+
+ Some(current) => {
+ // the previous group finished
+ self.finished_groups.insert(current);
+
+ // we've already finished this group, it's ungrouped
+ if self.finished_groups.contains(&group) {
+ findings.push(Finding {
+ severity: Severity::Style,
+ check: Check::UngroupedAttribute,
+ symbol: Some(symbol.to_string()),
+ message,
+ arch: arch.to_owned(),
+ });
+ }
+
+ // switch to the new group
+ self.current_group = Some(group);
+ }
+ }
+ }
+}
+
+struct DeadLinkChecker {
+ visited_links: HashSet<String>,
+}
+
+impl DeadLinkChecker {
+ fn new() -> Self {
+ Self {
+ visited_links: HashSet::new(),
+ }
+ }
+
+ fn check_text(
+ &mut self,
+ text: &str,
+ args: &AnalysisArgs,
+ findings: &mut Vec<Finding>,
+ symbol: Option<&str>,
+ arch: &String,
+ context: &str,
+ ) {
+ if !args.is_enabled(Check::DeadLink) {
+ return;
+ }
+
+ let links = dead_links::find_links(text);
+
+ if links.is_empty() {
+ return;
+ }
+
+ for link in links {
+ // avoid rechecking identical links
+ if !self.visited_links.insert(link.clone()) {
+ continue;
+ }
+
+ let status = check_link(&link);
+ if status != LinkStatus::Ok && status != LinkStatus::ProbablyBlocked {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadLink,
+ symbol: symbol.map(|s| s.to_string()),
+ message: format!(
+ "{} contains link {} with status {:?}",
+ context, link, status
+ ),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct Context {
+ pub arch: String,
+ pub definition_condition: Vec<Expression>,
+ pub visibility: Vec<Option<Expression>>,
+ pub dependencies: Vec<Expression>,
+ pub in_choice: bool,
+}
+
+impl Context {
+ fn with_arch(arch: String) -> Context {
+ Context {
+ arch,
+ definition_condition: vec![],
+ visibility: vec![],
+ dependencies: vec![],
+ in_choice: false,
+ }
+ }
+
+ fn child(&self) -> Self {
+ self.clone()
+ }
+
+ fn with_dep(mut self, dep: Expression) -> Self {
+ self.dependencies.push(dep);
+ self
+ }
+
+ fn with_visibility(mut self, cond: Option<Expression>) -> Self {
+ self.visibility.push(cond);
+ self
+ }
+
+ fn with_definition(mut self, cond: Expression) -> Self {
+ self.definition_condition.push(cond);
+ self
+ }
+
+ fn in_choice(mut self) -> Self {
+ self.in_choice = true;
+ self
+ }
+}
+
+fn recurse_entries(
+ args: &AnalysisArgs,
+ symtab: &mut SymbolTable,
+ entries: Vec<Entry>,
+ ctx: Context,
+ findings: &mut Vec<Finding>,
+) {
+ for entry in entries {
+ process_entry(args, symtab, entry, ctx.clone(), findings);
+ }
+}
+
+pub fn analyze(
+ args: &AnalysisArgs,
+ symtab: &mut SymbolTable,
+ arch: String,
+ entries: Vec<Entry>,
+) -> Vec<Finding> {
+ let mut findings = Vec::new();
+
+ let ctx = Context::with_arch(arch);
+
+ recurse_entries(args, symtab, entries, ctx, &mut findings);
+
+ findings
+}
+
+fn handle_config(
+ args: &AnalysisArgs,
+ symtab: &mut SymbolTable,
+ entry: Config,
+ ctx: &Context,
+ findings: &mut Vec<Finding>,
+) {
+ let config_symbol = entry.symbol;
+
+ let mut child_ctx = ctx.child();
+
+ let mut config_type = None;
+ let mut kconfig_dependencies = Vec::new();
+ let mut kconfig_selects: Vec<Select> = Vec::new();
+ let mut kconfig_implies: Vec<Imply> = Vec::new();
+ let mut kconfig_ranges = Vec::new();
+ let mut kconfig_defaults = Vec::new();
+ let mut found_prompt = false;
+
+ /*
+ * style check: ungrouped attributes
+ * - need to check that dependencies, selects, ranges, and defaults are each kept together.
+ */
+ let mut attribute_grouping_checker = AttributeGroupingChecker::new();
+ let mut dead_link_checker = DeadLinkChecker::new();
+ for attribute in entry.attributes {
+ match attribute {
+ Type(kconfig_type) => match kconfig_type.r#type.clone() {
+ // hybrid type definition and default
+ Type::DefBool(db) => {
+ let default_attribute: DefaultAttribute = DefaultAttribute {
+ expression: db.clone(),
+ r#if: kconfig_type.clone().r#if,
+ };
+
+ kconfig_defaults.push(default_attribute);
+ config_type = Some(kconfig_type);
+
+ // NOTE: as a style, we prefer to keep the hybrid default-typedef with the standalone defaults
+ attribute_grouping_checker.check(
+ FunctionalAttributes::Defaults,
+ args,
+ findings,
+ &config_symbol,
+ &ctx.arch,
+ format!("ungrouped default {}", db),
+ );
+ }
+ Type::Bool(unconditional_prompt) => {
+ if unconditional_prompt.is_some() {
+ found_prompt = true;
+ }
+ config_type = Some(kconfig_type);
+ }
+
+ // hybrid type definition and default
+ Type::DefTristate(dt) => {
+ // NOTE: as a style, we prefer to keep the hybrid default-typedef with the standalone defaults
+ attribute_grouping_checker.check(
+ FunctionalAttributes::Defaults,
+ args,
+ findings,
+ &config_symbol,
+ &ctx.arch,
+ format!("ungrouped default {}", &dt),
+ );
+
+ let default_attribute: DefaultAttribute = DefaultAttribute {
+ expression: dt,
+ r#if: kconfig_type.clone().r#if,
+ };
+
+ kconfig_defaults.push(default_attribute);
+ config_type = Some(kconfig_type);
+ }
+ Type::Tristate(unconditional_prompt) => {
+ if unconditional_prompt.is_some() {
+ found_prompt = true;
+ }
+
+ config_type = Some(kconfig_type.clone())
+ }
+ Type::Hex(unconditional_prompt) => {
+ if unconditional_prompt.is_some() {
+ found_prompt = true;
+ }
+
+ config_type = Some(kconfig_type);
+ }
+ Type::Int(unconditional_prompt) => {
+ if unconditional_prompt.is_some() {
+ found_prompt = true;
+ }
+ config_type = Some(kconfig_type);
+ }
+ Type::String(unconditional_prompt) => {
+ if unconditional_prompt.is_some() {
+ found_prompt = true;
+ }
+ config_type = Some(kconfig_type);
+ }
+ },
+ Default(default) => {
+ attribute_grouping_checker.check(
+ FunctionalAttributes::Defaults,
+ args,
+ findings,
+ &config_symbol,
+ &ctx.arch,
+ format!("ungrouped default {}", &default),
+ );
+
+ kconfig_defaults.push(default);
+ }
+
+ DependsOn(depends_on) => {
+ attribute_grouping_checker.check(
+ FunctionalAttributes::Dependencies,
+ args,
+ findings,
+ &config_symbol,
+ &ctx.arch,
+ format!("ungrouped dependency {}", &depends_on),
+ );
+
+ kconfig_dependencies.push(depends_on);
+ }
+ Select(select) => {
+ attribute_grouping_checker.check(
+ FunctionalAttributes::Selects,
+ args,
+ findings,
+ &config_symbol,
+ &ctx.arch,
+ format!("ungrouped select {}", &select),
+ );
+
+ kconfig_selects.push(select);
+ }
+ Imply(imply) => {
+ attribute_grouping_checker.check(
+ FunctionalAttributes::Implies,
+ args,
+ findings,
+ &config_symbol,
+ &ctx.arch,
+ format!("ungrouped imply {}", imply),
+ );
+
+ kconfig_implies.push(imply);
+
+ // TODO: may be relevant for nonvisible config options when building an SMT model...
+ }
+ // NOTE: range bounds are inclusive
+ Range(r) => {
+ attribute_grouping_checker.check(
+ FunctionalAttributes::Ranges,
+ args,
+ findings,
+ &config_symbol,
+ &ctx.arch,
+ format!("ungrouped range {}", r),
+ );
+
+ kconfig_ranges.push(r);
+ }
+ Help(h) => {
+ // doing nothing for menu help right now
+
+ dead_link_checker.check_text(
+ &h,
+ args,
+ findings,
+ Some(&config_symbol),
+ &ctx.arch,
+ "help text",
+ );
+ }
+
+ Modules => {
+ // the modules attribute designates this config option as the one that determines if the `m` state is available for tristates options.
+
+ // just making a special note of this in the symtab for now...
+ symtab.modules_option = Some(config_symbol.clone());
+ }
+
+ // the prompt's option `if` determines "visibility"
+ Prompt(prompt) => {
+ // TODO: once we have SMT solving, we can also check if the prompt condition is always true or never true (and therefore, effectively unconditional)
+
+ found_prompt = true;
+ if let Some(c) = prompt.r#if {
+ child_ctx = child_ctx.with_visibility(Some(c));
+ }
+ }
+ Transitional => {
+ // doing nothing for transitional right now
+ }
+ Optional | Visible(_) | Requires(_) | Option(_) => {
+ eprintln!("Error: unexpected attribute encountered: {:?}", attribute);
+
+ if cfg!(debug_assertions) {
+ panic!();
+ }
+ }
+ }
+ }
+
+ if !found_prompt {
+ child_ctx = child_ctx.with_visibility(None);
+ }
+
+ // there can be multiple entries that get merged. so we need to do the same for our symtab.
+ let kconfig_type = config_type.clone().map(|c| c.r#type);
+
+ // at the time of writing this, linux's kconfig only uses Bool inside Choice.
+ // however, the kconfig documentation doesn't specify whether or not this is guaranteed to be the case.
+ // we add this check to ensure that we don't cause undefined behavior in future linux versions if something changes...
+ if child_ctx.in_choice {
+ if let Some(kt) = &kconfig_type {
+ match kt {
+ Type::Bool(_) | Type::DefBool(_) => {
+ // expected in a choice...
+ }
+
+ _ => {
+ // TODO: old versions of linux (like 5.4.4) have tristates in the choice
+ // - u-boot also currently has hex options in the choice!
+ eprintln!(
+ "Error: found something unexpected in a choice-statement: {:?}",
+ kt
+ );
+ }
+ }
+ }
+ }
+
+ // at the end, add the file's cur_dependencies to this var's invididual dependencies.
+ kconfig_dependencies.extend(child_ctx.dependencies.clone());
+ symtab.merge_insert_new_solved(
+ config_symbol.clone(),
+ kconfig_type,
+ kconfig_dependencies,
+ //z3_dependency,
+ kconfig_ranges,
+ kconfig_defaults,
+ child_ctx.visibility.clone(),
+ child_ctx.arch.clone(),
+ child_ctx.definition_condition.clone(),
+ None,
+ kconfig_selects
+ .clone()
+ .into_iter()
+ .map(|sel| (sel.symbol, sel.r#if))
+ .collect(),
+ kconfig_implies
+ .into_iter()
+ .map(|imply| (imply.symbol.to_string(), imply.r#if))
+ .collect(),
+ );
+ // TODO: file a github issue, imply can never imply a constant (this is technically parsing incorrectly)
+
+ // TODO: when SMT solving, we may need to keep track of the implies the same way we keep track of selects,
+ // in cases when the implied config option is non-visible
+
+ // need to add the select condition to the definedness condition if it exists
+ for select in kconfig_selects {
+ match select.r#if {
+ None => symtab.merge_insert_new_solved(
+ select.symbol,
+ None,
+ Vec::new(),
+ Vec::new(),
+ Vec::new(),
+ Vec::new(),
+ child_ctx.arch.clone(),
+ child_ctx.definition_condition.clone(),
+ Some((config_symbol.clone(), None)),
+ Vec::new(),
+ Vec::new(),
+ ),
+ Some(select_condition) => {
+ symtab.merge_insert_new_solved(
+ select.symbol,
+ None,
+ Vec::new(),
+ Vec::new(),
+ Vec::new(),
+ Vec::new(),
+ child_ctx.arch.clone(),
+ child_ctx.definition_condition.clone(),
+ Some((config_symbol.clone(), Some(select_condition))),
+ Vec::new(),
+ Vec::new(),
+ );
+ }
+ }
+ }
+}
+
+fn handle_menu(
+ args: &AnalysisArgs,
+ symtab: &mut SymbolTable,
+ entry: Menu,
+ ctx: &Context,
+ findings: &mut Vec<Finding>,
+) {
+ // menus can set the visibility of their menu items
+
+ let mut child_ctx = ctx.child();
+
+ for dep in entry.depends_on {
+ child_ctx = child_ctx.with_dep(dep.clone());
+ child_ctx = child_ctx.with_visibility(Some(dep)); // not a typo, the config options inside of a menu are only visible if the menu's dependencies are satisfied
+ }
+
+ let nested_entries = entry.entries;
+
+ recurse_entries(args, symtab, nested_entries, child_ctx.clone(), findings);
+}
+
+fn handle_choice(
+ args: &AnalysisArgs,
+ symtab: &mut SymbolTable,
+ entry: Choice,
+ ctx: &Context,
+ findings: &mut Vec<Finding>,
+) {
+ let mut child_ctx = ctx.child();
+ child_ctx = child_ctx.in_choice();
+
+ // we are going to add the dependencies of the choice to the dependencies of the entries.
+ // we start with the dependencies inherited from the file
+ let mut choice_visibility_condition = None;
+ let mut defaults = Vec::new();
+ for attribute in entry.options {
+ match attribute {
+ DependsOn(depends_on) => {
+ child_ctx = child_ctx.with_dep(depends_on);
+ }
+
+ Default(default) => {
+ defaults.push(default);
+ }
+
+ // the prompt's `if` determines visibility
+ Prompt(prompt) => {
+ choice_visibility_condition = prompt.r#if;
+ if let Some(i) = choice_visibility_condition.clone() {
+ child_ctx = child_ctx.with_visibility(Some(i));
+ }
+ }
+ _ => {
+ // skip
+ }
+ }
+ }
+
+ // all of the variables in the choice menu
+ //let mut contained_vars = Vec::with_capacity(c.entries.len());
+ let nested_entries = entry.entries;
+
+ recurse_entries(args, symtab, nested_entries, child_ctx.clone(), findings);
+
+ let choice_data = ChoiceData {
+ //inner_vars: contained_vars,
+ arch: child_ctx.arch.clone(),
+ visibility: choice_visibility_condition,
+ dependencies: child_ctx.dependencies,
+ defaults,
+ };
+ symtab.choices.push(choice_data);
+}
+
+fn handle_if(
+ args: &AnalysisArgs,
+ symtab: &mut SymbolTable,
+ entry: If,
+ ctx: &Context,
+ findings: &mut Vec<Finding>,
+) {
+ let mut child_ctx = ctx.child();
+ child_ctx = child_ctx.with_definition(entry.condition.clone());
+ child_ctx = child_ctx.with_dep(entry.condition);
+ let nested_entries = entry.entries;
+
+ recurse_entries(args, symtab, nested_entries, child_ctx, findings);
+}
+
+fn handle_source(
+ args: &AnalysisArgs,
+ symtab: &mut SymbolTable,
+ entry: Source,
+ ctx: &Context,
+ findings: &mut Vec<Finding>,
+) {
+ let sourced_kconfig = entry.kconfigs;
+
+ for sourced_kconfig in sourced_kconfig {
+ recurse_entries(args, symtab, sourced_kconfig.entries, ctx.clone(), findings);
+ }
+}
+
+pub fn process_entry(
+ args: &AnalysisArgs,
+ symtab: &mut SymbolTable,
+ entry: Entry,
+ ctx: Context,
+ findings: &mut Vec<Finding>,
+) {
+ // NOTE: in general, each handler should update the context as it encounters that construct.
+ // e.g. Context.in_choice() should be called at the start of handle_choice(), not right before call to process_entry() when a choice is found and process_entry is called
+ match entry {
+ Entry::Config(c) | Entry::MenuConfig(c) => {
+ handle_config(args, symtab, c, &ctx, findings);
+ }
+ Entry::Menu(m) => handle_menu(args, symtab, m, &ctx, findings),
+ Entry::Choice(c) => handle_choice(args, symtab, c, &ctx, findings),
+ Entry::If(i) => handle_if(args, symtab, i, &ctx, findings),
+ Entry::Source(s) => handle_source(args, symtab, s, &ctx, findings),
+ Entry::Comment(_) => {}
+ Entry::MainMenu(_) => {}
+ _ => {}
+ }
+}
diff --git a/scripts/kconfirm/kconfirm-lib/src/checks.rs b/scripts/kconfirm/kconfirm-lib/src/checks.rs
new file mode 100644
index 000000000000..2ad67f4390ea
--- /dev/null
+++ b/scripts/kconfirm/kconfirm-lib/src/checks.rs
@@ -0,0 +1,701 @@
+// SPDX-License-Identifier: GPL-2.0-only
+use crate::output::Finding;
+use crate::output::Severity;
+use crate::symbol_table::AttributeDef;
+use crate::symbol_table::TypeInfo;
+use nom_kconfig::attribute::Expression;
+use nom_kconfig::attribute::range::RangeBound;
+use std::collections::HashSet;
+use std::num::ParseIntError;
+use std::str::FromStr;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum Check {
+ FailedParse,
+ UngroupedAttribute, // check for duplicate default values, and ungrouped attributes
+ DeadLink, // check for dead links in the help texts
+ SelectVisible,
+ // need SMT solving before we can detect select-undefineds
+ //SelectUndefined,
+ DuplicateDependency,
+ DuplicateRange,
+ DeadRange,
+ DuplicateSelect,
+ DeadSelect,
+ DeadDefault,
+ ConstantCondition,
+ DuplicateDefault,
+ DuplicateDefaultValue,
+ DuplicateImply,
+ DeadImply,
+ ReverseRange,
+}
+
+impl Check {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Check::FailedParse => "failed_parse",
+ Check::UngroupedAttribute => "ungrouped_attribute",
+ Check::DeadLink => "dead_link",
+ Check::SelectVisible => "select_visible",
+ Check::DuplicateDependency => "duplicate_dependency",
+ Check::DuplicateRange => "duplicate_range",
+ Check::DeadRange => "dead_range",
+ Check::DuplicateSelect => "duplicate_select",
+ Check::DeadSelect => "dead_select",
+ Check::DeadDefault => "dead_default",
+ Check::ConstantCondition => "constant_condition",
+ Check::DuplicateDefault => "duplicate_default",
+ Check::DuplicateDefaultValue => "duplicate_default_value",
+ Check::DuplicateImply => "duplicate_imply",
+ Check::DeadImply => "dead_imply",
+ Check::ReverseRange => "reverse_range",
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct ParseCheckError {
+ pub input: String,
+}
+
+impl std::fmt::Display for ParseCheckError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "unknown check '{}'", self.input)
+ }
+}
+
+impl std::error::Error for ParseCheckError {}
+
+impl FromStr for Check {
+ type Err = ParseCheckError;
+
+ fn from_str(name: &str) -> Result<Self, Self::Err> {
+ match name {
+ "failed_parse" => Ok(Check::FailedParse),
+ "ungrouped_attribute" => Ok(Check::UngroupedAttribute),
+ "dead_link" => Ok(Check::DeadLink),
+ "select_visible" => Ok(Check::SelectVisible),
+ "duplicate_dependency" => Ok(Check::DuplicateDependency),
+ "duplicate_range" => Ok(Check::DuplicateRange),
+ "dead_range" => Ok(Check::DeadRange),
+ "duplicate_select" => Ok(Check::DuplicateSelect),
+ "dead_select" => Ok(Check::DeadSelect),
+ "dead_default" => Ok(Check::DeadDefault),
+ "constant_condition" => Ok(Check::ConstantCondition),
+ "duplicate_default" => Ok(Check::DuplicateDefault),
+ "duplicate_default_value" => Ok(Check::DuplicateDefaultValue),
+ "duplicate_imply" => Ok(Check::DuplicateImply),
+ "dead_imply" => Ok(Check::DeadImply),
+ "reverse_range" => Ok(Check::ReverseRange),
+ _ => Err(ParseCheckError {
+ input: name.to_string(),
+ }),
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct AnalysisArgs {
+ // check for duplicate default values
+ pub enabled_checks: HashSet<Check>,
+}
+
+impl AnalysisArgs {
+ pub fn is_enabled(&self, check: Check) -> bool {
+ self.enabled_checks.contains(&check)
+ }
+}
+
+// returns an Error if a hex range bound cannot be parsed as an u64
+pub fn check_reverse_ranges(arch: &String, var_symbol: &str, info: &AttributeDef) -> Vec<Finding> {
+ let mut findings = Vec::new();
+
+ for range in &info.kconfig_ranges {
+ // returns an Error if a hex range bound cannot be parsed as an u64
+ fn range_bound_to_int(range_bound: &RangeBound) -> Result<i128, ParseIntError> {
+ match range_bound {
+ RangeBound::Number(b) => {
+ return Ok(b.to_owned() as i128);
+ }
+ RangeBound::Hex(b_str) => {
+ let trimmed = b_str.trim_start_matches("0x").trim_start_matches("0X");
+
+ return i128::from_str_radix(trimmed, 16);
+ }
+ RangeBound::Variable(_) => {
+ // for now, the caller is expected not to pass these cases.
+ unreachable!("not handling variable ranges until SMT solving");
+ }
+ RangeBound::Symbol(_) => {
+ // TODO: need SMT solving for this case
+ // for now, the caller is expected not to pass these cases.
+ unreachable!("not handling CONFIG ranges until SMT solving");
+ }
+ }
+ }
+
+ if matches!(range.lower_bound, RangeBound::Symbol(_))
+ || matches!(range.upper_bound, RangeBound::Symbol(_))
+ {
+ // not handling these cases until SMT solving.
+ // don't return though, because we stil want to check the other ranges.
+ continue;
+ }
+
+ let maybe_lower_bound = range_bound_to_int(&range.lower_bound);
+ let maybe_upper_bound = range_bound_to_int(&range.upper_bound);
+
+ match (maybe_lower_bound, maybe_upper_bound) {
+ (Ok(lower_bound), Ok(upper_bound)) => {
+ if lower_bound > upper_bound {
+ let message = format!(
+ "reverse range {} for config option: {}, no value is valid",
+ range.to_string(),
+ var_symbol,
+ );
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::ReverseRange,
+ symbol: Some(var_symbol.to_owned()),
+ arch: arch.to_owned(),
+ message,
+ });
+ }
+ }
+ (Result::Err(_), _) | (_, Result::Err(_)) => {
+ eprintln!(
+ "Error: couldn't parse hex range bound as i128 for config option: {}",
+ var_symbol
+ );
+ // still want to check the other range bounds
+ continue;
+ }
+ }
+ }
+
+ findings
+}
+
+pub fn check_constant_conditions(
+ arch: &String,
+ var_symbol: &str,
+ info: &AttributeDef,
+) -> Vec<Finding> {
+ let mut findings = Vec::new();
+ let default_conditions: Vec<&Expression> = info
+ .kconfig_defaults
+ .iter()
+ .filter_map(|conditional_default| conditional_default.r#if.as_ref())
+ .collect();
+
+ check_conditions(
+ arch,
+ &mut findings,
+ &var_symbol,
+ &info.kconfig_dependencies,
+ default_conditions,
+ "default",
+ );
+
+ let select_conditions: Vec<&Expression> = info
+ .selects
+ .iter()
+ .filter_map(|conditional_select| conditional_select.1.as_ref())
+ .collect();
+
+ check_conditions(
+ arch,
+ &mut findings,
+ var_symbol,
+ &info.kconfig_dependencies,
+ select_conditions,
+ "select",
+ );
+
+ let imply_conditions: Vec<&Expression> = info
+ .implies
+ .iter()
+ .filter_map(|imp| imp.1.as_ref())
+ .collect();
+
+ check_conditions(
+ arch,
+ &mut findings,
+ var_symbol,
+ &info.kconfig_dependencies,
+ imply_conditions,
+ "imply",
+ );
+
+ let range_conditions: Vec<&Expression> = info
+ .kconfig_ranges
+ .iter()
+ .filter_map(|conditional_range| conditional_range.r#if.as_ref())
+ .collect();
+
+ check_conditions(
+ arch,
+ &mut findings,
+ var_symbol,
+ &info.kconfig_dependencies,
+ range_conditions,
+ "range",
+ );
+
+ fn check_conditions(
+ arch: &String,
+ findings: &mut Vec<Finding>,
+ symbol: &str,
+ kconfig_dependencies: &[Expression],
+ attribute_conditions: Vec<&Expression>,
+ context: &str,
+ ) {
+ for attribute_condition in attribute_conditions.into_iter() {
+ if kconfig_dependencies.contains(attribute_condition) {
+ let message = format!(
+ "constant {} condition 'if {}' for config option: {}, this condition is a dependency and will always be true",
+ context,
+ attribute_condition.to_string(),
+ symbol,
+ );
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::ConstantCondition,
+ symbol: Some(symbol.to_owned()),
+ arch: arch.to_owned(),
+ message,
+ });
+ }
+ }
+ }
+ findings
+}
+
+pub fn check_variable_info(
+ args: &AnalysisArgs,
+ var_symbol: &str,
+ arch_specific: &String,
+ info: &AttributeDef,
+) -> Vec<Finding> {
+ let mut findings = Vec::new();
+
+ if args.is_enabled(Check::DuplicateDependency) {
+ findings.extend(check_duplicate_dependencies(
+ arch_specific,
+ var_symbol,
+ info,
+ ));
+ }
+
+ if args.is_enabled(Check::DuplicateImply) {
+ findings.extend(check_duplicate_implies(arch_specific, var_symbol, info));
+ }
+
+ if args.is_enabled(Check::DuplicateRange) {
+ findings.extend(check_duplicate_ranges(arch_specific, var_symbol, info));
+ }
+
+ if args.is_enabled(Check::DuplicateSelect) {
+ findings.extend(check_duplicate_selects(arch_specific, var_symbol, info));
+ }
+
+ if args.is_enabled(Check::ConstantCondition) {
+ findings.extend(check_constant_conditions(arch_specific, var_symbol, info));
+ }
+
+ if args.is_enabled(Check::DeadDefault)
+ || args.is_enabled(Check::DuplicateDefault)
+ || args.is_enabled(Check::DuplicateDefaultValue)
+ {
+ findings.extend(check_defaults(arch_specific, var_symbol, info, args));
+ }
+
+ if args.is_enabled(Check::ReverseRange) {
+ findings.extend(check_reverse_ranges(arch_specific, var_symbol, info));
+ }
+
+ findings
+}
+
+// TODO: also check if a config option in one arch unconditionally references a config option that only exists in another arch (need SMT for this first)
+pub fn check_select_visible(var_symbol: &str, info: &TypeInfo) -> Vec<Finding> {
+ let mut findings = Vec::new();
+
+ // only interested in the options that are selected
+ if info.selected_by.is_empty() {
+ return Vec::new();
+ }
+
+ for (selector, select_info) in &info.selected_by {
+ for (arch, _cond) in select_info {
+ // NOTE: we don't care if the select is conditional or unconditional, just the selectee's visibility
+
+ // at this point, we know that `selector` unconditionally selects `var_symbol`
+ // now, we need to check if `var_symbol` is unconditionally visible
+
+ let message = format!(
+ "selects the visible {}; consider using 'depends on' or 'imply' instead",
+ var_symbol
+ );
+
+ // match the architecture that the select happens under with the architecture of the unconditional visibility
+ match info.attribute_defs.get(arch) {
+ None => {
+ // not selected in this architecture
+ }
+ Some(cur_arch_attribute_def) => {
+ for (if_conditions, attributes) in cur_arch_attribute_def {
+ if if_conditions.is_empty() && attributes.visibility.is_empty() {
+ // empty visiblity means that it is unconditionally visible, within the current arch (assuming arch is not `None`)
+
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::SelectVisible,
+ symbol: Some(selector.to_owned()),
+ message: message.clone(),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+ }
+ }
+ }
+ }
+
+ findings
+}
+
+fn is_duplicate<T: Eq + std::hash::Hash>(set: &mut HashSet<T>, key: T) -> bool {
+ !set.insert(key)
+}
+
+fn check_duplicate_dependencies(
+ arch_specific: &String,
+ var_symbol: &str,
+ info: &AttributeDef,
+) -> Vec<Finding> {
+ let mut findings = Vec::new();
+ let mut seen = HashSet::new();
+
+ for dep in &info.kconfig_dependencies {
+ if is_duplicate(&mut seen, dep.to_string()) {
+ let message = format!("duplicate dependency on {}", dep.to_string());
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DuplicateDependency,
+ symbol: Some(var_symbol.to_owned()),
+ message,
+ arch: arch_specific.to_owned(),
+ });
+ }
+ }
+
+ findings
+}
+
+fn check_duplicate_implies(arch: &String, var_symbol: &str, info: &AttributeDef) -> Vec<Finding> {
+ let mut findings = Vec::new();
+
+ // symbols implied unconditionally
+ let mut unconditional: HashSet<String> = HashSet::new();
+
+ // (symbol, condition)
+ let mut conditional: HashSet<(String, String)> = HashSet::new();
+
+ for imp in &info.implies {
+ let imply_var = imp.0.clone();
+
+ match &imp.1 {
+ Some(cond) => {
+ let cond_str = cond.to_string();
+
+ // duplicate conditional imply
+ if !conditional.insert((imply_var.clone(), cond_str.clone())) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DuplicateImply,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!(
+ "duplicate imply of {:?} with condition {}",
+ imp.0, cond_str
+ ),
+ arch: arch.to_owned(),
+ });
+ }
+
+ // conditional imply is dead if unconditional exists
+ if unconditional.contains(&imply_var) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadImply,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("dead imply of {:?}", imp),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+
+ None => {
+ // duplicate unconditional imply
+ if !unconditional.insert(imply_var.clone()) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DuplicateImply,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("duplicate imply of {:?}", imp),
+ arch: arch.to_owned(),
+ });
+ }
+
+ // previous conditionals with same symbol are dead
+ for (sym, _) in &conditional {
+ if sym == &imply_var {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadImply,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("dead imply of {:?}", imp),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+ }
+ }
+ }
+
+ findings
+}
+
+fn check_duplicate_ranges(arch: &String, var_symbol: &str, info: &AttributeDef) -> Vec<Finding> {
+ let mut findings = Vec::new();
+
+ // unconditional ranges by bounds
+ let mut unconditional: HashSet<String> = HashSet::new();
+
+ // (bounds, condition)
+ let mut conditional: HashSet<(String, String)> = HashSet::new();
+
+ for range in &info.kconfig_ranges {
+ // uniquely identify the range bounds
+ let range_key = format!("{} {}", range.lower_bound, range.upper_bound);
+
+ match &range.r#if {
+ Some(cond) => {
+ let cond_str = cond.to_string();
+
+ // duplicate conditional range
+ if !conditional.insert((range_key.clone(), cond_str.clone())) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DuplicateRange,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("duplicate range {:?} with condition {}", range, cond_str),
+ arch: arch.to_owned(),
+ });
+ }
+
+ // conditional range is dead if unconditional exists
+ if unconditional.contains(&range_key) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadRange,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("dead range of {:?}", range),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+
+ None => {
+ // duplicate unconditional range
+ if !unconditional.insert(range_key.clone()) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadRange,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("duplicate range {:?}", range),
+ arch: arch.to_owned(),
+ });
+ }
+
+ // previous conditionals with same bounds are dead
+ for (bounds, _) in &conditional {
+ if bounds == &range_key {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadRange,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("dead range of {:?}", range),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+ }
+ }
+ }
+
+ findings
+}
+
+fn check_duplicate_selects(arch: &String, var_symbol: &str, info: &AttributeDef) -> Vec<Finding> {
+ let mut findings = Vec::new();
+
+ // symbols selected unconditionally
+ let mut unconditional: HashSet<String> = HashSet::new();
+
+ // (symbol, condition)
+ let mut conditional: HashSet<(String, String)> = HashSet::new();
+
+ for select in &info.selects {
+ let select_var = select.0.clone();
+
+ match &select.1 {
+ Some(cond) => {
+ let cond_str = cond.to_string();
+
+ // duplicate conditional select
+ if !conditional.insert((select_var.clone(), cond_str.clone())) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DuplicateSelect,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!(
+ "duplicate select of {:?} with condition {}",
+ select.0, cond_str
+ ),
+ arch: arch.to_owned(),
+ });
+ }
+
+ // conditional is dead if unconditional exists
+ if unconditional.contains(&select_var) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadSelect,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("dead select of {:?}", select.0),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+
+ None => {
+ // duplicate unconditional select
+ if !unconditional.insert(select_var.clone()) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DuplicateSelect,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("duplicate select of {:?}", select.0),
+ arch: arch.to_owned(),
+ });
+ }
+
+ // any previous conditional selects are now dead too
+ for (sym, _) in &conditional {
+ if sym == &select_var {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadSelect,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("dead select of {:?}", select.0),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+ }
+ }
+ }
+
+ findings
+}
+
+#[allow(clippy::collapsible_if)]
+fn check_defaults(
+ arch: &String,
+ var_symbol: &str,
+ info: &AttributeDef,
+ args: &AnalysisArgs,
+) -> Vec<Finding> {
+ let mut findings = Vec::new();
+ let mut seen_conditions = HashSet::new();
+ let mut seen_values = HashSet::new();
+ let mut already_unconditional = false;
+
+ for default in &info.kconfig_defaults {
+ let val_str = default.expression.to_string();
+
+ let has_real_condition = match &default.r#if {
+ Some(cond) => {
+ let cond_str = cond.to_string();
+ !cond_str.is_empty()
+ }
+ None => false,
+ };
+
+ let is_value_dup = if has_real_condition {
+ is_duplicate(&mut seen_values, val_str.clone())
+ } else {
+ false
+ };
+
+ if already_unconditional && args.is_enabled(Check::DeadDefault) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadDefault,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("dead default of {}", val_str),
+ arch: arch.to_owned(),
+ });
+ }
+
+ if args.is_enabled(Check::DuplicateDefaultValue) {
+ if default.r#if.is_some() && is_value_dup {
+ findings.push(Finding {
+ severity: Severity::Style,
+ check: Check::DuplicateDefaultValue,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!(
+ "duplicate default value of {}; consider combining the conditions with a logical-or: ||",
+ val_str
+ ),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+
+ match &default.r#if {
+ Some(cond) => {
+ if is_duplicate(&mut seen_conditions, cond.to_string()) {
+ if is_value_dup {
+ if args.is_enabled(Check::DuplicateDefault) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DuplicateDefault,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("duplicate default condition of {:?}", cond),
+ arch: arch.to_owned(),
+ });
+ }
+ } else {
+ if args.is_enabled(Check::DeadDefault) {
+ findings.push(Finding {
+ severity: Severity::Warning,
+ check: Check::DeadDefault,
+ symbol: Some(var_symbol.to_owned()),
+ message: format!("dead default of {}", val_str),
+ arch: arch.to_owned(),
+ });
+ }
+ }
+ }
+ }
+ None => {
+ already_unconditional = true;
+ }
+ }
+ }
+
+ findings
+}
diff --git a/scripts/kconfirm/kconfirm-lib/src/curl_ffi.rs b/scripts/kconfirm/kconfirm-lib/src/curl_ffi.rs
new file mode 100644
index 000000000000..d458010cc3f1
--- /dev/null
+++ b/scripts/kconfirm/kconfirm-lib/src/curl_ffi.rs
@@ -0,0 +1,182 @@
+// SPDX-License-Identifier: GPL-2.0-only
+use core::ffi::c_void;
+use std::ffi::CStr;
+use std::ffi::CString;
+use std::os::raw::c_char;
+use std::os::raw::c_int;
+use std::os::raw::c_long;
+use std::sync::OnceLock;
+
+static CURL_INIT: OnceLock<()> = OnceLock::new();
+
+#[repr(C)]
+pub struct CURL {
+ _private: [u8; 0],
+}
+
+type CURLcode = c_int;
+type CURLoption = u32;
+type CURLINFO = u32;
+
+const CURLE_OK: CURLcode = 0;
+
+const CURL_GLOBAL_DEFAULT: c_long = 3;
+
+const CURLOPT_URL: CURLoption = 10002;
+const CURLOPT_NOBODY: CURLoption = 44;
+const CURLOPT_TIMEOUT: CURLoption = 13;
+const CURLOPT_FOLLOWLOCATION: CURLoption = 52;
+const CURLOPT_USERAGENT: CURLoption = 10018;
+const CURLOPT_HEADERFUNCTION: CURLoption = 20079;
+const CURLOPT_HEADERDATA: CURLoption = 10029;
+
+const CURLINFO_RESPONSE_CODE: CURLINFO = 0x200002;
+
+#[link(name = "curl")]
+unsafe extern "C" {}
+
+unsafe extern "C" {
+ fn curl_global_init(flags: c_long) -> CURLcode;
+
+ fn curl_easy_init() -> *mut CURL;
+
+ fn curl_easy_cleanup(handle: *mut CURL);
+
+ fn curl_easy_perform(handle: *mut CURL) -> CURLcode;
+
+ fn curl_easy_strerror(code: CURLcode) -> *const c_char;
+
+ fn curl_easy_setopt(handle: *mut CURL, option: CURLoption, ...) -> CURLcode;
+
+ fn curl_easy_getinfo(handle: *mut CURL, info: CURLINFO, ...) -> CURLcode;
+}
+
+fn init_curl() {
+ CURL_INIT.get_or_init(|| unsafe {
+ curl_global_init(CURL_GLOBAL_DEFAULT);
+ });
+}
+
+fn curl_error(code: CURLcode) -> String {
+ unsafe {
+ let ptr = curl_easy_strerror(code);
+
+ if ptr.is_null() {
+ return format!("curl error {}", code);
+ }
+
+ CStr::from_ptr(ptr).to_string_lossy().into_owned()
+ }
+}
+
+struct HeaderCapture {
+ location: Option<String>,
+}
+
+extern "C" fn header_callback(
+ buffer: *mut c_char,
+ size: usize,
+ nitems: usize,
+ userdata: *mut c_void,
+) -> usize {
+ let total = size * nitems;
+
+ unsafe {
+ let bytes = std::slice::from_raw_parts(buffer as *const u8, total);
+
+ if let Ok(header) = std::str::from_utf8(bytes) {
+ let lower = header.to_ascii_lowercase();
+
+ if lower.starts_with("location:") {
+ if let Some((_, value)) = header.split_once(':') {
+ let capture = &mut *(userdata as *mut HeaderCapture);
+
+ capture.location = Some(value.trim().to_string());
+ }
+ }
+ }
+ }
+
+ total
+}
+
+#[derive(Debug)]
+pub struct HttpResponse {
+ pub response_code: u16,
+ pub location: Option<String>,
+}
+
+pub fn head_request(url: &str) -> Result<HttpResponse, String> {
+ init_curl();
+
+ unsafe {
+ let curl = curl_easy_init();
+
+ if curl.is_null() {
+ return Err("curl_easy_init failed".into());
+ }
+
+ let url_c = match CString::new(url) {
+ Ok(v) => v,
+ Err(_) => {
+ curl_easy_cleanup(curl);
+
+ return Err("invalid URL".into());
+ }
+ };
+
+ let ua_c = CString::new("link-checker/1.0").unwrap();
+
+ let mut headers = HeaderCapture { location: None };
+
+ macro_rules! setopt {
+ ($opt:expr, $val:expr) => {{
+ let rc = curl_easy_setopt(curl, $opt, $val);
+
+ if rc != CURLE_OK {
+ curl_easy_cleanup(curl);
+
+ return Err(curl_error(rc));
+ }
+ }};
+ }
+
+ setopt!(CURLOPT_URL, url_c.as_ptr());
+ setopt!(CURLOPT_NOBODY, 1 as c_long);
+ setopt!(CURLOPT_TIMEOUT, 10 as c_long);
+ setopt!(CURLOPT_FOLLOWLOCATION, 0 as c_long);
+ setopt!(CURLOPT_USERAGENT, ua_c.as_ptr());
+
+ setopt!(
+ CURLOPT_HEADERFUNCTION,
+ header_callback as extern "C" fn(_, _, _, _) -> _
+ );
+
+ setopt!(CURLOPT_HEADERDATA, &mut headers as *mut _ as *mut c_void);
+
+ let rc = curl_easy_perform(curl);
+
+ if rc != CURLE_OK {
+ curl_easy_cleanup(curl);
+
+ return Err(curl_error(rc));
+ }
+
+ let mut response_code: c_long = 0;
+
+ let rc = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &mut response_code);
+
+ if rc != CURLE_OK {
+ curl_easy_cleanup(curl);
+
+ return Err(curl_error(rc));
+ }
+
+ curl_easy_cleanup(curl);
+
+ Ok(HttpResponse {
+ response_code: response_code as u16,
+ location: headers.location,
+ })
+ }
+}
diff --git a/scripts/kconfirm/kconfirm-lib/src/dead_links.rs b/scripts/kconfirm/kconfirm-lib/src/dead_links.rs
new file mode 100644
index 000000000000..47bbd5c09114
--- /dev/null
+++ b/scripts/kconfirm/kconfirm-lib/src/dead_links.rs
@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: GPL-2.0-only
+use crate::curl_ffi::head_request;
+use std::collections::HashSet;
+
+#[derive(PartialEq, Debug)]
+pub enum LinkStatus {
+ Ok,
+ ProbablyBlocked,
+ Redirected(String),
+ NotFound,
+ ServerError,
+ Unreachable(String),
+ UnsupportedScheme(String),
+}
+
+pub fn check_link(url: &str) -> LinkStatus {
+ if let Some(scheme) = url.split("://").next() {
+ match scheme {
+ "http" | "https" => return check_http(url),
+
+ "git" | "ftp" => {
+ return LinkStatus::UnsupportedScheme(scheme.into());
+ }
+
+ _ => {
+ return LinkStatus::UnsupportedScheme(scheme.into());
+ }
+ }
+ }
+
+ LinkStatus::Unreachable("invalid URL".into())
+}
+
+fn check_http(url: &str) -> LinkStatus {
+ let response = match head_request(url) {
+ Ok(r) => r,
+ Err(e) => return LinkStatus::Unreachable(e),
+ };
+
+ match response.response_code {
+ 200..=299 => LinkStatus::Ok,
+
+ 301 | 302 => LinkStatus::Redirected(response.location.unwrap_or_else(|| "unknown".into())),
+
+ 403 | 429 => LinkStatus::ProbablyBlocked,
+
+ 404 => LinkStatus::NotFound,
+
+ 500..=599 => LinkStatus::ServerError,
+
+ _ => LinkStatus::ProbablyBlocked,
+ }
+}
+
+pub fn find_links(text: &str) -> Vec<String> {
+ fn is_scheme_char(c: u8) -> bool {
+ c.is_ascii_alphanumeric() || matches!(c, b'+' | b'-' | b'.')
+ }
+
+ fn is_url_terminator(c: u8) -> bool {
+ c.is_ascii_whitespace()
+ || matches!(
+ c,
+ b'"' | b'\'' | b'<' | b'>' | b'(' | b')' | b'[' | b']' | b'{' | b'}'
+ )
+ }
+
+ let bytes = text.as_bytes();
+
+ let mut links = Vec::new();
+ let mut seen = HashSet::new();
+
+ let mut i = 0;
+
+ while i + 3 < bytes.len() {
+ if bytes[i] == b':' && bytes[i + 1] == b'/' && bytes[i + 2] == b'/' {
+ // walk backward to find scheme start
+ let mut start = i;
+
+ while start > 0 && is_scheme_char(bytes[start - 1]) {
+ start -= 1;
+ }
+
+ // require non-empty scheme
+ if start == i {
+ i += 3;
+ continue;
+ }
+
+ // first char must be alphabetic
+ if !bytes[start].is_ascii_alphabetic() {
+ i += 3;
+ continue;
+ }
+
+ // walk forward to url end
+ let mut end = i + 3;
+
+ while end < bytes.len() && !is_url_terminator(bytes[end]) {
+ end += 1;
+ }
+
+ let mut url = &text[start..end];
+
+ // trim trailing punctuation
+ url = url.trim_end_matches(&['.', ',', ';', ':', '!', '?'][..]);
+
+ // trim unmatched markdown
+ while let Some(last) = url.chars().last() {
+ let trim = match last {
+ ')' => url.matches('(').count() < url.matches(')').count(),
+
+ ']' => url.matches('[').count() < url.matches(']').count(),
+
+ '}' => url.matches('{').count() < url.matches('}').count(),
+
+ _ => false,
+ };
+
+ if trim {
+ url = &url[..url.len() - last.len_utf8()];
+ } else {
+ break;
+ }
+ }
+
+ if seen.insert(url) {
+ links.push(url.to_string());
+ }
+
+ i = end;
+ } else {
+ i += 1;
+ }
+ }
+
+ links
+}
diff --git a/scripts/kconfirm/kconfirm-lib/src/lib.rs b/scripts/kconfirm/kconfirm-lib/src/lib.rs
new file mode 100644
index 000000000000..6be0199f0785
--- /dev/null
+++ b/scripts/kconfirm/kconfirm-lib/src/lib.rs
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-2.0-only
+use analyze::analyze;
+pub use checks::AnalysisArgs;
+pub use checks::Check;
+pub use checks::check_select_visible;
+pub use checks::check_variable_info;
+use nom_kconfig::Entry;
+use nom_kconfig::KconfigInput;
+use nom_kconfig::parse_kconfig;
+use output::*;
+use symbol_table::*;
+mod analyze;
+mod checks;
+mod curl_ffi;
+mod dead_links;
+pub mod output;
+pub mod symbol_table;
+
+pub fn check_kconfig(
+ args: AnalysisArgs,
+ kconfig_files: Vec<(String, KconfigInput)>,
+) -> Vec<Finding> {
+ let mut findings = Vec::new();
+ let mut symbol_table = SymbolTable::new();
+
+ for (arch_config_option, kconfig_file) in kconfig_files {
+ match parse_kconfig(kconfig_file) {
+ Ok(parsed) => {
+ let entries: Vec<Entry> = parsed.1.entries;
+ findings.extend(analyze(
+ &args,
+ &mut symbol_table,
+ arch_config_option,
+ entries,
+ ));
+ }
+ Err(e) => {
+ findings.push(Finding {
+ severity: Severity::Fatal,
+ check: Check::FailedParse,
+ symbol: None,
+ message: format!("Failed to parse kconfig, error is: {}", e),
+ arch: arch_config_option,
+ });
+ }
+ }
+ }
+
+ for (var_symbol, type_info) in &symbol_table.raw {
+ for (arch_specific, redefinitions) in &type_info.attribute_defs {
+ for (_definition_condition, info) in redefinitions {
+ findings.extend(check_variable_info(&args, var_symbol, arch_specific, info));
+ }
+ }
+
+ if args.is_enabled(Check::SelectVisible) {
+ findings.extend(check_select_visible(var_symbol, type_info));
+ }
+ }
+
+ findings
+}
diff --git a/scripts/kconfirm/kconfirm-lib/src/output.rs b/scripts/kconfirm/kconfirm-lib/src/output.rs
new file mode 100644
index 000000000000..e0d8bf8342d5
--- /dev/null
+++ b/scripts/kconfirm/kconfirm-lib/src/output.rs
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-2.0-only
+use crate::Check;
+use std::fmt;
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub enum Severity {
+ Fatal,
+ Error, // will be used for known bugs, e.g. unmet dependencies
+ Warning,
+ Style,
+}
+
+#[derive(Debug)]
+pub struct Finding {
+ pub severity: Severity,
+ pub check: Check,
+ pub symbol: Option<String>,
+ pub message: String,
+ pub arch: String,
+}
+
+impl Finding {
+ fn fmt_with_arches(&self, f: &mut fmt::Formatter, arches: &[&str]) -> fmt::Result {
+ let arch_part = if arches.is_empty() {
+ String::new()
+ } else {
+ format!(" [{}]", arches.join(", "))
+ };
+
+ match &self.symbol {
+ Some(s) => write!(
+ f,
+ "{} [{}]{} config {}: {}",
+ self.severity,
+ self.check.as_str(),
+ arch_part,
+ s,
+ self.message
+ ),
+ None => write!(
+ f,
+ "{} [{}]{} {}",
+ self.severity,
+ self.check.as_str(),
+ arch_part,
+ self.message
+ ),
+ }
+ }
+}
+
+impl fmt::Display for Finding {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.fmt_with_arches(f, &[])
+ }
+}
+
+pub fn print_findings(mut findings: Vec<Finding>) {
+ findings.sort_by(|a, b| {
+ (
+ &a.severity,
+ a.check.as_str(),
+ &a.symbol,
+ &a.message,
+ &a.arch,
+ )
+ .cmp(&(
+ &b.severity,
+ b.check.as_str(),
+ &b.symbol,
+ &b.message,
+ &b.arch,
+ ))
+ });
+
+ for group in findings.chunk_by(|a, b| {
+ a.severity == b.severity
+ && a.check.as_str() == b.check.as_str()
+ && a.symbol == b.symbol
+ && a.message == b.message
+ }) {
+ let head = &group[0];
+
+ let mut arches: Vec<&str> = Vec::new();
+ for f in group {
+ if arches.last() != Some(&f.arch.as_str()) {
+ arches.push(&f.arch);
+ }
+ }
+
+ // Use a small wrapper so we can call our custom formatter via println!
+ struct Wrap<'a>(&'a Finding, &'a [&'a str]);
+ impl fmt::Display for Wrap<'_> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.0.fmt_with_arches(f, self.1)
+ }
+ }
+ println!("{}", Wrap(head, &arches));
+ }
+}
+
+impl fmt::Display for Severity {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Severity::Fatal => write!(f, "FATAL "),
+ Severity::Error => write!(f, "ERROR "),
+ Severity::Warning => write!(f, "WARNING"),
+ Severity::Style => write!(f, "STYLE "),
+ }
+ }
+}
diff --git a/scripts/kconfirm/kconfirm-lib/src/symbol_table.rs b/scripts/kconfirm/kconfirm-lib/src/symbol_table.rs
new file mode 100644
index 000000000000..48abb46c1945
--- /dev/null
+++ b/scripts/kconfirm/kconfirm-lib/src/symbol_table.rs
@@ -0,0 +1,223 @@
+// SPDX-License-Identifier: GPL-2.0-only
+use nom_kconfig::attribute::DefaultAttribute;
+use nom_kconfig::attribute::Expression;
+use nom_kconfig::attribute::OrExpression;
+use nom_kconfig::attribute::Range;
+use nom_kconfig::attribute::r#type::Type;
+use std::collections::HashMap;
+use std::collections::hash_map;
+
+type KconfigSymbol = String;
+type Arch = String;
+type Cond = Option<Expression>;
+
+// NOTE: we cannot add these elements to the solver until we've processed all variables,
+// because we need to know all of the selectors.
+#[derive(Debug, Clone)]
+pub struct TypeInfo {
+ pub kconfig_type: Option<Type>, // 'None' when we don't know the type (e.g. if it's a dangling reference)
+
+ // maps the selector to an (ARCH, select_cond)
+ // - if the ARCH is None, then it's not arch-specific
+ // if the select_cond is None, then it's unconditional
+ pub selected_by: HashMap<KconfigSymbol, Vec<(Arch, Cond)>>, // .0 only selects it when .1 is true.
+
+ // there is one of these per entry (each entry expected to have a different definedness condition)
+ // maps architecture option name (or none if not arch-specific) to:
+ // [([condition], config definition)]
+ // - NOTE: there can be multiple partial definitions under the same condition, or mutually-exclusive conditions, or a subset condition.
+ pub attribute_defs: HashMap<Arch, Vec<(Vec<Expression>, AttributeDef)>>, // the innermost `Vec<Expression>` represents each nested condition that was reached (we will eventually need to AND them all)
+}
+
+// everything is a vector because we may encounter multiple over time,
+// so we won't know until the end what the condition is.
+#[derive(Debug, Clone)]
+pub struct AttributeDef {
+ pub kconfig_dependencies: Vec<OrExpression>,
+ pub kconfig_ranges: Vec<Range>,
+ pub kconfig_defaults: Vec<DefaultAttribute>,
+ pub visibility: Vec<Option<OrExpression>>,
+ pub selects: Vec<(KconfigSymbol, Cond)>,
+ pub implies: Vec<(KconfigSymbol, Cond)>,
+}
+
+impl TypeInfo {
+ fn new_empty() -> Self {
+ Self {
+ kconfig_type: None,
+ selected_by: HashMap::new(),
+ attribute_defs: HashMap::new(),
+ }
+ }
+
+ // TODO: we should consider having separate functions for:
+ // 1. merge-inserting a redef of attributes (NOTE: the type definition is actually part of the redef, but we aren't handling type-redefinitions for now)
+ // 2. selectors
+ fn insert(
+ &mut self,
+ kconfig_type: Option<Type>,
+ raw_constraints: Vec<OrExpression>,
+ kconfig_ranges: Vec<Range>,
+ kconfig_defaults: Vec<DefaultAttribute>,
+ visibility: Vec<Option<OrExpression>>,
+ arch: String,
+ definition_condition: Vec<OrExpression>,
+ selected_by: Option<(KconfigSymbol, Cond)>,
+ selects: Vec<(KconfigSymbol, Cond)>,
+ implies: Vec<(KconfigSymbol, Cond)>,
+ ) {
+ // type merge
+ match (&self.kconfig_type, &kconfig_type) {
+ (None, Some(_)) => self.kconfig_type = kconfig_type.clone(),
+ (Some(_), Some(new)) if Some(new) != self.kconfig_type.as_ref() => {
+ // TODO: not doing anything with redefined types yet.
+ // later, we will want to consider e.g. bool/def_bool the same type (and possibly int/hex?) but not bool/tristate, so we need to build out typechecking.
+ }
+ _ => {}
+ }
+
+ // selected_by merge
+ if let Some(sb) = selected_by {
+ merge_selected_by(&mut self.selected_by, arch.clone(), sb);
+ }
+
+ // variable_info merge:
+ // we only want to add an attribute redefinition if the things in the attribute def aren't empty
+ // (the visibility is just additional info to capture)
+ if (&kconfig_type).is_some() // we need to ensure that we have an empty definition here if the config option had a type definition
+ || !raw_constraints.is_empty()
+ || !kconfig_ranges.is_empty()
+ || !kconfig_defaults.is_empty()
+ || !selects.is_empty()
+ || !implies.is_empty()
+ {
+ insert_variable_info(
+ &mut self.attribute_defs,
+ arch,
+ definition_condition,
+ AttributeDef {
+ kconfig_dependencies: raw_constraints,
+ kconfig_ranges,
+ kconfig_defaults,
+ visibility,
+ selects,
+ implies,
+ },
+ );
+ }
+ }
+}
+
+// the visibility and the dependencies will each need to be AND'd (separately)
+// the defaults should each be handled separately.
+pub struct ChoiceData {
+ //pub inner_vars: Vec<String>,
+ pub arch: Arch,
+ pub visibility: Cond,
+ pub dependencies: Vec<OrExpression>, // this is the menu's dependencies (and inherited dependencies from the file)
+ pub defaults: Vec<DefaultAttribute>, // these are each of the conditional defaults for the choice
+}
+
+// NOTE: it might be better if TypeInfo is an enum with a single value,
+// e.g. Unsolved(kconfig_raw) and Solved(z3_ast)
+pub struct SymbolTable {
+ pub raw: HashMap<KconfigSymbol, TypeInfo>,
+ pub choices: Vec<ChoiceData>,
+ pub modules_option: Option<KconfigSymbol>, // None until we find the modules attribute in exactly 1 config option
+}
+
+impl SymbolTable {
+ pub fn new() -> Self {
+ SymbolTable {
+ raw: HashMap::new(),
+ choices: Vec::new(),
+ modules_option: None,
+ }
+ }
+
+ pub fn from_parts(
+ raw: HashMap<KconfigSymbol, TypeInfo>,
+ choices: Vec<ChoiceData>,
+ modules_option: Option<KconfigSymbol>,
+ ) -> Self {
+ SymbolTable {
+ raw,
+ choices,
+ modules_option,
+ }
+ }
+
+ pub fn merge_insert_new_solved(
+ &mut self,
+ var: KconfigSymbol,
+ kconfig_type: Option<Type>,
+ raw_constraints: Vec<OrExpression>,
+ kconfig_ranges: Vec<Range>,
+ kconfig_defaults: Vec<DefaultAttribute>,
+ visibility: Vec<Option<OrExpression>>,
+ arch: Arch,
+ definition_condition: Vec<OrExpression>,
+ selected_by: Option<(KconfigSymbol, Cond)>,
+ selects: Vec<(KconfigSymbol, Cond)>,
+ implies: Vec<(KconfigSymbol, Cond)>,
+ ) {
+ let entry = self.raw.entry(var.clone());
+
+ match entry {
+ hash_map::Entry::Vacant(v) => {
+ let mut t = TypeInfo::new_empty();
+ t.insert(
+ kconfig_type,
+ raw_constraints,
+ kconfig_ranges,
+ kconfig_defaults,
+ visibility,
+ arch,
+ definition_condition,
+ selected_by,
+ selects,
+ implies,
+ );
+ v.insert(t);
+ }
+
+ hash_map::Entry::Occupied(mut o) => {
+ let t = o.get_mut();
+
+ t.insert(
+ kconfig_type,
+ raw_constraints,
+ kconfig_ranges,
+ kconfig_defaults,
+ visibility,
+ arch,
+ definition_condition,
+ selected_by,
+ selects,
+ implies,
+ );
+ }
+ }
+ }
+}
+
+fn merge_selected_by(
+ map: &mut HashMap<String, Vec<(Arch, Cond)>>,
+ arch: Arch,
+ selected_by: (KconfigSymbol, Cond),
+) {
+ map.entry(selected_by.0)
+ .or_default() // empty vec
+ .push((arch, selected_by.1));
+}
+
+fn insert_variable_info(
+ map: &mut HashMap<Arch, Vec<(Vec<Expression>, AttributeDef)>>,
+ arch: Arch,
+ definition_condition: Vec<Expression>,
+ info: AttributeDef,
+) {
+ map.entry(arch)
+ .or_default() // empty vec
+ .push((definition_condition, info));
+}
diff --git a/scripts/kconfirm/kconfirm-linux/Cargo.toml b/scripts/kconfirm/kconfirm-linux/Cargo.toml
new file mode 100644
index 000000000000..9516399e1dae
--- /dev/null
+++ b/scripts/kconfirm/kconfirm-linux/Cargo.toml
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: GPL-2.0
+[package]
+name = "kconfirm-linux"
+version = "0.10.0"
+edition = "2024"
+rust-version.workspace = true
+
+[dependencies]
+kconfirm-lib = { path = "../kconfirm-lib" }
+nom-kconfig = { workspace = true }
diff --git a/scripts/kconfirm/kconfirm-linux/src/getopt_ffi.rs b/scripts/kconfirm/kconfirm-linux/src/getopt_ffi.rs
new file mode 100644
index 000000000000..227faa17b962
--- /dev/null
+++ b/scripts/kconfirm/kconfirm-linux/src/getopt_ffi.rs
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-2.0-only
+use std::env;
+use std::ffi::CStr;
+use std::ffi::CString;
+use std::os::raw::c_char;
+use std::os::raw::c_int;
+use std::ptr;
+
+pub const REQUIRED_ARGUMENT: c_int = 1;
+
+#[repr(C)]
+pub struct option {
+ pub name: *const c_char,
+ pub has_arg: c_int,
+ pub flag: *mut c_int,
+ pub val: c_int,
+}
+
+#[link(name = "c")]
+unsafe extern "C" {
+ fn getopt_long(
+ argc: c_int,
+ argv: *mut *mut c_char,
+ optstring: *const c_char,
+ longopts: *const option,
+ longindex: *mut c_int,
+ ) -> c_int;
+
+ static mut optarg: *mut c_char;
+ static mut optind: c_int;
+}
+
+pub struct Getopt {
+ _cstrings: Vec<CString>,
+ argv: Vec<*mut c_char>,
+ argc: c_int,
+}
+
+impl Getopt {
+ pub fn new() -> Self {
+ let raw_args: Vec<String> = env::args().collect();
+
+ let cstrings: Vec<CString> = raw_args
+ .iter()
+ .map(|s| CString::new(s.as_str()).unwrap())
+ .collect();
+
+ let mut argv: Vec<*mut c_char> =
+ cstrings.iter().map(|s| s.as_ptr() as *mut c_char).collect();
+
+ argv.push(ptr::null_mut());
+
+ let argc = (argv.len() - 1) as c_int;
+
+ Self {
+ _cstrings: cstrings,
+ argv,
+ argc,
+ }
+ }
+
+ pub fn reset(&mut self) {
+ unsafe {
+ optind = 1;
+ }
+ }
+
+ pub fn next(
+ &mut self,
+ optstring: &CStr,
+ longopts: &[option],
+ ) -> Option<Result<(char, Option<String>), String>> {
+ unsafe {
+ let c = getopt_long(
+ self.argc,
+ self.argv.as_mut_ptr(),
+ optstring.as_ptr(),
+ longopts.as_ptr(),
+ ptr::null_mut(),
+ );
+
+ if c == -1 {
+ return None;
+ }
+
+ if c == '?' as c_int {
+ return Some(Err("invalid argument".into()));
+ }
+
+ let arg = if optarg.is_null() {
+ None
+ } else {
+ Some(CStr::from_ptr(optarg).to_string_lossy().into_owned())
+ };
+
+ Some(Ok((c as u8 as char, arg)))
+ }
+ }
+}
diff --git a/scripts/kconfirm/kconfirm-linux/src/lib.rs b/scripts/kconfirm/kconfirm-linux/src/lib.rs
new file mode 100644
index 000000000000..f52399d2c9e5
--- /dev/null
+++ b/scripts/kconfirm/kconfirm-linux/src/lib.rs
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: GPL-2.0-only
+use nom_kconfig::KconfigFile;
+use std::io;
+use std::path::PathBuf;
+
+pub const ALL_ARCHITECTURES: [&str; 21] = [
+ "arm",
+ "arm64",
+ "x86",
+ "riscv",
+ "mips",
+ "xtensa",
+ "sparc",
+ "alpha",
+ "arc",
+ "csky",
+ "hexagon",
+ "loongarch",
+ "m68k",
+ "microblaze",
+ "nios2",
+ "openrisc",
+ "parisc",
+ "powerpc",
+ "s390",
+ "sh",
+ "um",
+];
+
+// each architecture has its own directory, and config option.
+// most are the same, but powerpc / ppc and um / uml are not.
+// this maps the directory to the config option
+pub fn arch_dir_to_config(arch_dir: &str) -> String {
+ match arch_dir {
+ "powerpc" => String::from("PPC"),
+ "um" => String::from("UML"),
+ _ => String::from(arch_dir).to_uppercase(),
+ }
+}
+
+pub struct LinuxKconfig {
+ pub arch_config_option: String,
+ pub kconfig_file: KconfigFile,
+ pub file_contents: String,
+}
+
+// collects the root kconfig file, and all of the arch-specific kconfig files
+pub fn collect_kconfig_root_files(
+ archs: Vec<String>,
+ linux_source: PathBuf,
+) -> io::Result<Vec<LinuxKconfig>> {
+ let mut all_root_kconfig_files = Vec::new();
+
+ // add the root kconfig file
+ let root_kconfig_path = PathBuf::from("Kconfig"); // doesn't include the arch: arch/x86/Kconfig
+ let root_kconfig_file = KconfigFile::new(linux_source.clone(), root_kconfig_path.clone());
+
+ for arch_dir in archs {
+ let mut cur_root_kconfig_file = root_kconfig_file.clone();
+
+ if arch_dir == "um" {
+ // this is only used by the 'um' architecture to include arch/x86/um/Kconfig
+ cur_root_kconfig_file.add_local_var("HEADER_ARCH", "x86");
+ }
+
+ cur_root_kconfig_file.add_local_var("SRCARCH", &arch_dir);
+
+ let linux_kconfig = LinuxKconfig {
+ arch_config_option: arch_dir_to_config(&arch_dir),
+ file_contents: root_kconfig_file.read_to_string()?,
+ kconfig_file: cur_root_kconfig_file,
+ };
+
+ all_root_kconfig_files.push(linux_kconfig);
+ }
+
+ Ok(all_root_kconfig_files)
+}
diff --git a/scripts/kconfirm/kconfirm-linux/src/main.rs b/scripts/kconfirm/kconfirm-linux/src/main.rs
new file mode 100644
index 000000000000..03554a94f57c
--- /dev/null
+++ b/scripts/kconfirm/kconfirm-linux/src/main.rs
@@ -0,0 +1,192 @@
+// SPDX-License-Identifier: GPL-2.0-only
+use crate::getopt_ffi::Getopt;
+use crate::getopt_ffi::REQUIRED_ARGUMENT;
+use crate::getopt_ffi::option;
+use kconfirm_lib::AnalysisArgs;
+use kconfirm_lib::Check;
+use kconfirm_lib::check_kconfig;
+use kconfirm_lib::output::print_findings;
+use kconfirm_linux::ALL_ARCHITECTURES;
+use kconfirm_linux::collect_kconfig_root_files;
+use nom_kconfig::KconfigInput;
+use std::collections::HashSet;
+use std::io;
+use std::path::PathBuf;
+use std::ptr;
+use std::str::FromStr;
+mod getopt_ffi;
+
+fn split_csv_arg(dst: &mut Vec<String>, value: &str) {
+ dst.extend(
+ value
+ .split(',')
+ .filter(|s| !s.is_empty())
+ .map(|s| s.to_string()),
+ );
+}
+
+#[derive(Debug)]
+pub struct Args {
+ pub linux_path: PathBuf,
+ pub enable_arch: Vec<String>,
+ pub disable_arch: Vec<String>,
+ pub enable_check: Vec<String>,
+ pub disable_check: Vec<String>,
+}
+
+pub fn parse_args() -> Result<Args, String> {
+ let mut linux_path: Option<PathBuf> = None;
+ let mut enable_arch = Vec::new();
+ let mut disable_arch = Vec::new();
+ let mut enable_check = Vec::new();
+ let mut disable_check = Vec::new();
+
+ let long_options = [
+ option {
+ name: c"linux-path".as_ptr(),
+ has_arg: REQUIRED_ARGUMENT,
+ flag: ptr::null_mut(),
+ val: 'l' as _,
+ },
+ option {
+ name: c"enable-arch".as_ptr(),
+ has_arg: REQUIRED_ARGUMENT,
+ flag: ptr::null_mut(),
+ val: 'a' as _,
+ },
+ option {
+ name: c"disable-arch".as_ptr(),
+ has_arg: REQUIRED_ARGUMENT,
+ flag: ptr::null_mut(),
+ val: 'x' as _,
+ },
+ option {
+ name: c"enable-check".as_ptr(),
+ has_arg: REQUIRED_ARGUMENT,
+ flag: ptr::null_mut(),
+ val: 'e' as _,
+ },
+ option {
+ name: c"disable-check".as_ptr(),
+ has_arg: REQUIRED_ARGUMENT,
+ flag: ptr::null_mut(),
+ val: 'd' as _,
+ },
+ option {
+ name: ptr::null(),
+ has_arg: 0,
+ flag: ptr::null_mut(),
+ val: 0,
+ },
+ ];
+
+ let mut getopt = Getopt::new();
+
+ getopt.reset();
+
+ while let Some(result) = getopt.next(c"l:a:x:e:d:", &long_options) {
+ let (opt, arg) = result?;
+
+ match opt {
+ 'l' => {
+ linux_path = Some(PathBuf::from(arg.unwrap()));
+ }
+
+ 'a' => {
+ split_csv_arg(&mut enable_arch, &arg.unwrap());
+ }
+
+ 'x' => {
+ split_csv_arg(&mut disable_arch, &arg.unwrap());
+ }
+
+ 'e' => {
+ split_csv_arg(&mut enable_check, &arg.unwrap());
+ }
+
+ 'd' => {
+ split_csv_arg(&mut disable_check, &arg.unwrap());
+ }
+
+ _ => {}
+ }
+ }
+
+ let linux_path = linux_path.ok_or("--linux-path is required")?;
+
+ if enable_arch.is_empty() {
+ return Err("--enable-arch is required".into());
+ }
+
+ Ok(Args {
+ linux_path,
+ enable_arch,
+ disable_arch,
+ enable_check,
+ disable_check,
+ })
+}
+
+fn main() -> io::Result<()> {
+ let cli_args = parse_args().unwrap_or_else(|e| {
+ eprintln!("error: {e}");
+ std::process::exit(1);
+ });
+ let mut enabled_checks: HashSet<Check> = [
+ Check::DuplicateDependency,
+ Check::DuplicateRange,
+ Check::DeadRange,
+ Check::DuplicateSelect,
+ Check::DeadDefault,
+ Check::ConstantCondition,
+ Check::DuplicateDefault,
+ Check::DuplicateImply,
+ Check::ReverseRange,
+ ]
+ .into_iter()
+ .collect(); // apply --enable-check
+ for name in &cli_args.enable_check {
+ if let Ok(c) = Check::from_str(name) {
+ enabled_checks.insert(c);
+ } else {
+ eprintln!("Error: check {} does not exist", name);
+ std::process::exit(1);
+ }
+ } // apply --disable-check
+ for name in &cli_args.disable_check {
+ if let Ok(c) = Check::from_str(name) {
+ enabled_checks.remove(&c);
+ } else {
+ eprintln!("Error: check {} does not exist", name);
+ std::process::exit(1);
+ }
+ }
+ let analysis_args = AnalysisArgs { enabled_checks };
+ let mut selected_arches: HashSet<String> = cli_args.enable_arch.iter().cloned().collect(); // apply --disable-arch
+ for arch in &cli_args.disable_arch {
+ selected_arches.remove(arch);
+ }
+ for desired_arch in &selected_arches {
+ if !ALL_ARCHITECTURES.contains(&desired_arch.as_str()) {
+ eprintln!("Error: unexpected architecture, please pass one of the following:");
+ for available_arch in ALL_ARCHITECTURES {
+ eprint!("{} ", available_arch);
+ }
+ eprintln!("");
+ std::process::exit(1);
+ }
+ }
+ let kconfig_files =
+ collect_kconfig_root_files(selected_arches.into_iter().collect(), cli_args.linux_path)?;
+ let kconfig_inputs = kconfig_files
+ .iter()
+ .map(|kconfig| {
+ let kconfig_input =
+ KconfigInput::new_extra(&kconfig.file_contents, kconfig.kconfig_file.clone());
+ (kconfig.arch_config_option.clone(), kconfig_input)
+ })
+ .collect();
+ let findings = check_kconfig(analysis_args, kconfig_inputs);
+ print_findings(findings);
+ Ok(())
+}
--
2.53.0
^ permalink raw reply related
* [RFC PATCH v3 2/3] Documentation: add kconfirm
From: Julian Braha @ 2026-05-16 21:53 UTC (permalink / raw)
To: nathan, nsc
Cc: jani.nikula, akpm, gary, ljs, arnd, gregkh, masahiroy, ojeda,
corbet, qingfang.deng, yann.prono, demiobenour, ej, linux-kernel,
rust-for-linux, linux-doc, linux-kbuild, Julian Braha
In-Reply-To: <20260516215354.449807-1-julianbraha@gmail.com>
Add usage documentation and a brief description for kconfirm to
Documentation/dev-tools/
---
Documentation/dev-tools/index.rst | 1 +
Documentation/dev-tools/kconfirm.rst | 222 +++++++++++++++++++++++++++
2 files changed, 223 insertions(+)
create mode 100644 Documentation/dev-tools/kconfirm.rst
diff --git a/Documentation/dev-tools/index.rst b/Documentation/dev-tools/index.rst
index 59cbb77b33ff..130ebc0d7282 100644
--- a/Documentation/dev-tools/index.rst
+++ b/Documentation/dev-tools/index.rst
@@ -40,3 +40,4 @@ Documentation/process/debugging/index.rst
autofdo
propeller
container
+ kconfirm
diff --git a/Documentation/dev-tools/kconfirm.rst b/Documentation/dev-tools/kconfirm.rst
new file mode 100644
index 000000000000..8790672c9a87
--- /dev/null
+++ b/Documentation/dev-tools/kconfirm.rst
@@ -0,0 +1,222 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+.. Copyright (C) 2026 Julian Braha <julianbraha@gmail.com>
+
+========
+kconfirm
+========
+
+kconfirm is a static analysis tool for the kernel's Kconfig. It checks
+the entire tree-wide Kconfig, and reports misusage like dead code. In the
+case of dead default statements, these can be a code smell.
+
+kconfirm has some additional, optional checks. The first is for dead links
+in the Kconfig help texts. Since this has a high potential for false
+positives (due to websites blocking bots) and slows down runtime
+significantly, it is disabled by default.
+
+Another optional check is for config options that select visible config
+options. Examples of how to enable the optional checks are included
+below.
+
+kconfirm is written in Rust and lives in ``scripts/kconfirm``. Other
+than the dead link checks, kconfirm aims for zero false positives.
+
+By default, kconfirm checks the same architecture as your kernel build,
+but you can also enable checking more architectures with
+``--enable-arch`` or disable checking your default architecture with
+``--disable-arch``. Alarms are deduplicated across all affected
+architectures; kconfirm displays a tag with the corresponding Kconfig
+architecture config option names. For example, ``[RISCV]`` indicates
+that an alarm is specific to RISC-V, while ``[ARM, X86]`` indicates that
+an alarm affects both arm and x86. Running on each architecture will take
+approximately one minute on modern consumer hardware.
+
+**NOTE**: kconfirm does not modify or compile the source tree; it is
+strictly a static checker.
+
+
+Getting Started
+===============
+
+
+kconfirm's Minimum Supported Rust Version (MSRV) is v1.85.0, because
+it uses Rust edition 2024, and this is the earliest supported version.
+
+kconfirm requires the Cargo package manager and an internet connection
+to download its dependencies from crates.io.
+
+In ``scripts/kconfirm/`` run the following to download the dependencies::
+
+ cargo vendor
+
+Then, kconfirm can be built and run from the top of the
+kernel source tree::
+
+ make kconfirm
+
+The compiled ``kconfirm-linux`` binary will be available in
+``scripts/kconfirm/target/release/``.
+
+The default checks currently cover dead code analysis, as well as invalid
+(reverse) ranges and constant conditions. ``select_visible`` and
+``dead_links`` must be turned on explicitly with ``--enable-check``;
+conversely, any default check can be turned off with ``--disable-check``. Both
+options accept either a comma-separated list or repeated flags, so the
+following two invocations are equivalent::
+
+ kconfirm-linux --linux-path . --enable-check select_visible,dead_link
+ kconfirm-linux --linux-path . --enable-check select_visible --enable-check dead_link
+
+
+Options
+=======
+
+**NOTE**: kconfirm's arguments must be provided in the ``KCONFIRM_ARGS``
+environment variable if running with ``make``. See `Examples`_.
+
+Available options:
+
+``--linux-path PATH``
+ The path to the linux source tree to analyze. ``make`` uses this
+ option to pass the current linux tree, but this option can be used
+ when running the tool directly with another source tree.
+ See `Examples`_.
+
+``--enable-check CHECK[,CHECK...]``
+
+ Enable one or more checks in addition to the default set. May be
+ given multiple times, or as a single comma-separated list. See
+ `Available checks`_ below for valid names.
+
+``--disable-check CHECK[,CHECK...]``
+
+ Disable one or more checks from the default set. May be given
+ multiple times, or as a single comma-separated list.
+
+``--enable-arch ARCH[,ARCH...]``
+
+ Enable one or more architectures in addition to the default
+ architecture. May be given multiple times, or as a single
+ comma-separated list.
+
+``--disable-arch ARCH[,ARCH...]``
+
+ Disable one or more architectures from the default set. May be given
+ multiple times, or as a single comma-separated list.
+
+``-h, --help``
+
+ Show the help message and exit.
+
+``-V, --version``
+
+ Show version information and exit.
+
+
+Available checks
+================
+
+Each check has a string name that is accepted by ``--enable`` and
+``--disable``. Checks marked *(default)* are enabled unless turned off
+explicitly.
+
+``duplicate_dependency`` *(default)*
+
+ Reports duplicated ``depends on`` entries on a single Kconfig symbol.
+
+``duplicate_range`` *(default)*
+
+ Reports duplicated ``range`` entries on a single Kconfig symbol.
+
+``dead_range`` *(default)*
+
+ Reports ``range`` entries that will never be evaluated, due to an
+ unconditional range entry.
+
+``duplicate_select`` *(default)*
+
+ Reports duplicated ``select`` entries on a single Kconfig symbol.
+
+``dead_select`` *(default)*
+
+ Reports dead ``select`` entries that will never be evaluated, due to an
+ unconditional select entry of the same config option.
+
+``duplicate_imply`` *(default)*
+
+ Reports duplicated ``imply`` entries on a single Kconfig symbol.
+
+``dead_imply`` *(default)*
+
+ Reports dead ``imply`` entries that will never be evaluated, due to an
+ unconditional imply entry for the same config option.
+
+``duplicate_default`` *(default)*
+
+ Reports duplicated ``default`` entries on a single Kconfig symbol.
+
+``dead_default`` *(default)*
+
+ Reports ``default`` entries that can never be selected, for example
+ because their condition is unsatisfiable.
+
+``constant_condition`` *(default)*
+
+ Reports conditions for any entries that always evaluate to ``true``.
+
+``reverse_range`` *(default)*
+
+ Reports invalid ranges for int and hex configuration options.
+
+``failed_parse`` *(default)*
+
+ Reports a parsing failure of the Kconfig. Cannot be disabled.
+
+``select_visible``
+
+ Reports configuration options that ``select`` a config option that is
+ visible to users.
+
+``dead_link``
+
+ Reports broken URLs found in Kconfig help text. Because this
+ performs network requests it can be quite slow, and is disabled by
+ default. May also have false positives.
+
+``ungrouped_attribute``
+
+ Reports ungrouped entries, like ``select`` and ``depends on``.
+ This is a style check, and is disabled by default.
+
+``duplicate_default_value``
+
+ Reports duplicate default values that have different conditions.
+ Suggests combining the conditions using a logical-or ``||``.
+ This is a style check, and is disabled by default.
+
+
+Examples
+========
+
+Compile (as needed) and run on the current tree::
+
+ make kconfirm
+
+To additionally enable the dead link and select-visible checks::
+
+ make kconfirm KCONFIRM_ARGS="--enable-check=dead_link,select_visible"
+
+To disable a check (here, ``duplicate_dependency``) while keeping the
+rest of the default set::
+
+ make kconfirm KCONFIRM_ARGS="--disable-check duplicate_dependency"
+
+To enable an architecture (here, ``RISC-V``) while keeping the
+default architecture enabled::
+
+ make kconfirm KCONFIRM_ARGS="--enable-arch riscv"
+
+To run the default checks against a kernel tree separate from the
+current directory, such as ``~/repos/linux``::
+
+ scripts/kconfirm/target/release/kconfirm-linux --linux-path ~/repos/linux
--
2.53.0
^ permalink raw reply related
* [RFC PATCH v3 3/3] MAINTAINERS: create entry for kconfirm
From: Julian Braha @ 2026-05-16 21:53 UTC (permalink / raw)
To: nathan, nsc
Cc: jani.nikula, akpm, gary, ljs, arnd, gregkh, masahiroy, ojeda,
corbet, qingfang.deng, yann.prono, demiobenour, ej, linux-kernel,
rust-for-linux, linux-doc, linux-kbuild, Julian Braha
In-Reply-To: <20260516215354.449807-1-julianbraha@gmail.com>
Add myself as maintainer of kconfirm.
Signed-off-by: Julian Braha <julianbraha@gmail.com>
---
MAINTAINERS | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index b2040011a386..8f4f5a009228 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13824,6 +13824,12 @@ F: Documentation/kbuild/kconfig*
F: scripts/Kconfig.include
F: scripts/kconfig/
+KCONFIRM
+M: Julian Braha <julianbraha@gmail.com>
+S: Maintained
+F: Documentation/dev-tools/kconfirm.rst
+F: scripts/kconfirm/
+
KCORE
M: Omar Sandoval <osandov@osandov.com>
L: linux-debuggers@vger.kernel.org
--
2.53.0
^ permalink raw reply related
* Re: [RFC PATCH v3 00/28] mm/damon: introduce data attributes monitoring
From: SeongJae Park @ 2026-05-16 22:03 UTC (permalink / raw)
To: SeongJae Park
Cc: Liam R. Howlett, Andrew Morton, David Hildenbrand,
Jonathan Corbet, Lorenzo Stoakes, Masami Hiramatsu,
Mathieu Desnoyers, Michal Hocko, Mike Rapoport, Shuah Khan,
Shuah Khan, Steven Rostedt, Suren Baghdasaryan, Vlastimil Babka,
damon, linux-doc, linux-kernel, linux-kselftest, linux-mm,
linux-trace-kernel
In-Reply-To: <20260516183712.81393-1-sj@kernel.org>
On Sat, 16 May 2026 11:36:41 -0700 SeongJae Park <sj@kernel.org> wrote:
> TL; DR
> ======
>
> Extend DAMON for monitoring general data attributes other than accesses.
> The short term motivation is lightweight page type (e.g., belonging
> cgroup) aware monitoring. In long term, this will help extending DAMON
> for multiple access events capture primitives (e.g., page faults and
> PMU) and eventually pivotting DAMON to a "Data Attributes Monitoring and
> Operations eNgine" in long term.
Sashiko found [1] no blocker for this version but a nice document wordsmithing
idea. Unless I get other opinions, I will drop RFC tag from the next version
of this series.
[1] https://lore.kernel.org/damon/20260516185032.82261-1-sj@kernel.org/
Thanks,
SJ
[...]
^ permalink raw reply
* [PATCH RESEND 2] Documentation: hwmon: fix typo in heading for max31730
From: Hassan Maazu @ 2026-05-16 22:09 UTC (permalink / raw)
To: Guenter Roeck
Cc: skhan@linuxfoundation.org, corbet@lwn.net,
linux-doc@vger.kernel.org, linux-hwmon@vger.kernel.org,
linux-kernel@vger.kernel.org, Randy Dunlap
In-Reply-To: <ce11a8ba-8ebc-4c09-b6d0-7e98febeae6b@roeck-us.net>
Generated heading & link to driver doc for max31730 wrongly named
max31790 under hwmon docs. This patch fixes typo so link to max31730
is easily identifiable without confusion with max31790.
Signed-off-by: Hassan Maazu <maazudev@proton.me>
Acked-by: Randy Dunlap <rdunlap@infradead.org>
---
Documentation/hwmon/max31730.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Documentation/hwmon/max31730.rst b/Documentation/hwmon/max31730.rst
index 1c5a32b64187..0936ba2eac24 100644
--- a/Documentation/hwmon/max31730.rst
+++ b/Documentation/hwmon/max31730.rst
@@ -1,4 +1,4 @@
-Kernel driver max31790
+Kernel driver max31730
======================
Supported chips:
--
2.54.0
^ permalink raw reply related
* Re: [PATCH v6 03/11] dt-bindings: mfd: add documentation for S2MU005 PMIC
From: Conor Dooley @ 2026-05-16 22:25 UTC (permalink / raw)
To: Kaustabh Chakraborty
Cc: Lee Jones, Pavel Machek, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, MyungJoo Ham, Chanwoo Choi, Sebastian Reichel,
Krzysztof Kozlowski, André Draszik, Alexandre Belloni,
Jonathan Corbet, Shuah Khan, Nam Tran,
Łukasz Lebiedziński, linux-leds, devicetree,
linux-kernel, linux-pm, linux-samsung-soc, linux-rtc, linux-doc
In-Reply-To: <DIJK5FTQ5KWG.HOKZAOXHTGU7@disroot.org>
[-- Attachment #1: Type: text/plain, Size: 3180 bytes --]
On Sat, May 16, 2026 at 02:41:29AM +0530, Kaustabh Chakraborty wrote:
> On 2026-05-15 18:14 +01:00, Conor Dooley wrote:
> > On Fri, May 15, 2026 at 04:08:59PM +0530, Kaustabh Chakraborty wrote:
> >> Samsung's S2MU005 PMIC includes subdevices for a charger, an MUIC (Micro
> >> USB Interface Controller), and flash and RGB LED controllers.
> >>
> >> Add the compatible and documentation for the S2MU005 PMIC. Also, add an
> >> example for nodes for supported sub-devices, i.e. MUIC, flash LEDs, and
> >> RGB LEDs. Charger sub-device uses the node of the parent.
> >>
> >> Signed-off-by: Kaustabh Chakraborty <kauschluss@disroot.org>
> >> ---
> >> .../bindings/mfd/samsung,s2mu005-pmic.yaml | 120 +++++++++++++++++++++
> >> 1 file changed, 120 insertions(+)
> >>
> >> diff --git a/Documentation/devicetree/bindings/mfd/samsung,s2mu005-pmic.yaml b/Documentation/devicetree/bindings/mfd/samsung,s2mu005-pmic.yaml
> >> new file mode 100644
> >> index 0000000000000..0e6afb7d2017b
> >> --- /dev/null
> >> +++ b/Documentation/devicetree/bindings/mfd/samsung,s2mu005-pmic.yaml
> >> @@ -0,0 +1,120 @@
> >> +# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
> >> +%YAML 1.2
> >> +---
> >> +$id: http://devicetree.org/schemas/mfd/samsung,s2mu005-pmic.yaml#
> >> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> >> +
> >> +title: Samsung S2MU005 Power Management IC
> >> +
> >> +maintainers:
> >> + - Kaustabh Chakraborty <kauschluss@disroot.org>
> >> +
> >> +description: |
> >> + The S2MU005 is a companion power management IC which includes subdevices for
> >> + a charger controller, an MUIC (Micro USB Interface Controller), and flash and
> >> + RGB LED controllers.
> >> +
> >> +allOf:
> >> + - $ref: /schemas/power/supply/power-supply.yaml#
> >> +
> >> +properties:
> >> + compatible:
> >> + const: samsung,s2mu005-pmic
> >> +
> >> + flash:
> >> + $ref: /schemas/leds/samsung,s2mu005-flash.yaml
> >> + description:
> >> + Child node describing flash LEDs.
> >> +
> >> + interrupts:
> >> + maxItems: 1
> >> +
> >> + muic:
> >> + $ref: /schemas/extcon/samsung,s2mu005-muic.yaml#
> >> + description:
> >> + Child node describing MUIC device.
> >> +
> >> + multi-led:
> >> + type: object
> >> +
> >> + allOf:
> >> + - $ref: /schemas/leds/leds-class-multicolor.yaml#
> >
> > Does this need to be an allOf when the other refs are not?
>
> It has it's own properties, that's the reason. This used to be it's own
> thing in dt-bindings/leds, but I was asked to move it here in prior
> reviews.
What do you mean by "its own properties"?
>
> >> +
> >> + properties:
> >> + compatible:
> >> + const: samsung,s2mu005-rgb
> >> +
> >> + required:
> >> + - compatible
> >> +
> >> + unevaluatedProperties: false
> >> +
> >> + reg:
> >> + maxItems: 1
> >
> > Move this above the child nodes please.
>
> But properties are sorted in lex order?
Typically the binding is sorted in the same order as properties go in
nodes. Common stuff like reg/clocks/interrupts therefore send up above
child nodes.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply
* [RFC PATCH 0/7] mm/damon: hardware-sampled access reports + AMD IBS Op example
From: Ravi Jonnalagadda @ 2026-05-16 22:34 UTC (permalink / raw)
To: sj, damon, linux-mm, linux-kernel, linux-doc
Cc: akpm, corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun,
ravis.opensrc, bharata
Hi all,
This is an RFC, not for merge. The series exercises and validates
damon_report_access() -- the consumer API SeongJae introduced in [1]
-- as a substrate for ingesting access reports from hardware-sampling
sources. The series includes one worked-example backend, an AMD IBS
Op module (damon_ibs.ko), that runs on Zen 3+ silicon via the
existing perf event subsystem.
Combined with node_eligible_mem_bp [2], the recently-merged DAMOS goal
metric, the same DAMON interface composes naturally for two
operational regimes from one set of primitives:
1. Traditional tiering -- promote hot pages to DRAM up to a target
cap.
2. System-wide bandwidth interleaving -- split hot pages between
DRAM and CXL at an operator-chosen ratio, for workloads where
placing some hot pages on CXL improves aggregate throughput.
Either regime composes with a separately-configured migrate_cold
scheme to pair bandwidth shaping with capacity expansion: the
hot-page schemes drive placement to meet the bandwidth target while
migrate_cold reclaims DRAM by demoting cold pages.
The demonstration in this RFC exercises different
target ratios of the same PULL+PUSH setup.
Why a hardware-source primitive complements existing primitives
===============================================================
DAMON's existing access-check primitives observe access through
software paths:
- PTE-Accessed bit scanning samples Accessed bits and clears them
periodically. The hardware sets PTE-A on TLB miss, so already-
resident TLB entries do not re-set the bit until they're evicted.
For pages whose translations stay TLB-resident across DAMON's
aggregation interval, nr_accesses reflects fewer accesses than
the page actually serviced. This is correct behaviour for the
primitive -- it observes what the TLB-miss path observes.
- Page-fault sampling (NUMA hint faults) requires unmapping pages
to provoke the fault, then samples access on the fault path.
For closed-loop schemes that drive migrate_hot from the same
observations, the unmap and the migrate action interact.
Both primitives produce a view of hotness that converges to the
true distribution over the aggregation interval. For systems where
the address space is small relative to the aggregation rate, this is
the right tool. On large heterogeneous-memory systems with goal-
driven schemes asking the closed-loop tuner to converge on a target
distribution, a complementary lower-latency view of accesses can
tighten the loop -- reducing the time DAMON's nr_accesses takes to
reflect the workload's actual access distribution, which in turn
reduces ramp duration and oscillation amplitude during convergence
of goal-driven schemes.
A hardware-sampling primitive provides this complementary view:
hardware retirement records each access at its natural event rate,
with a physical address per sample, independent of TLB state and
independent of the unmap/fault path.
This RFC adds the substrate (damon_report_access) so any hardware
sampler -- IBS, PEBS, future CXL hotness monitoring units --
can feed access reports into the kdamond drain path and existing
DAMOS schemes. The substrate is the contribution; the IBS backend
is one worked example proving it on broadly-available silicon today.
Demonstration
=============
The two-scheme PULL+PUSH setup from the node_eligible_mem_bp
introduction holds a target hot-memory ratio across DRAM and CXL.
With damon_ibs.ko feeding damon_report_access, we observe two
operational regimes:
Cold-start convergence -- workload starts at an even DRAM/CXL
distribution (numactl --interleave=DRAM,CXL), DAMON context starts
with the target ratio set at kdamond launch, schemes converge from
the initial distribution to the target distribution.
+-----------+--------+----------+---------+
| Target | Mean | Offset | Stddev |
+-----------+--------+----------+---------+
| 70% DRAM | 69.73% | -0.27pp | 0.70pp |
| 30% DRAM | 31.00% | +1.00pp | 1.28pp |
+-----------+--------+----------+---------+
Live target changes from a converged state -- kdamond context runs
continuously, target ratio updated via DAMOS commit_schemes_quota_goals
without kdamond teardown.
+-----------+--------+----------+---------+
| Target | Mean | Offset | Stddev |
+-----------+--------+----------+---------+
| 90% DRAM | 89.74% | -0.26pp | 0.64pp |
| 85% DRAM | 84.61% | -0.39pp | 0.60pp |
+-----------+--------+----------+---------+
In both regimes, convergence to target is quick, and the workload's
measured DRAM share then holds within 1.3 percentage points of
target with standard deviation under 1.3 percentage points, sustained
over runs of 15-30 minutes per target.
Hardware envelope: AMD EPYC dual-socket, CXL.mem on a separate NUMA
node, 32GB hot working set, two migrate_hot schemes with complementary
address filters, temporal quota tuner, 256-entry per-CPU report ring,
512 MiB per-scheme quota, 1s reset interval.
What's in this series
=====================
Patch 1. mm/damon/core: refcount ops owner module to prevent
rmmod UAF
Patch 2. mm/damon/paddr: export damon_pa_* ops for IBS module
Patch 3. mm/damon/core: replace mutex-protected report buffer
with per-CPU lockless ring
Patch 4. mm/damon/core: flat-array snapshot + bsearch in ring-
drain loop
Patch 5. mm/damon: add sysfs binding and dispatch hookup for
paddr_ibs operations
Patch 6. mm/damon/core: accept paddr_ibs in node_eligible_mem_bp
ops check
Patch 7. mm/damon/damon_ibs: add AMD IBS-based access sampling
backend
Patches 1, 3, and 4 are general infrastructure that benefits any
consumer of damon_report_access(). Patches 2, 5, 6, and 7 are the
worked-example backend (paddr_ibs ops, sysfs binding, IBS module).
Patches worth folding into damon/next
=====================================
Patches 1, 3, and 4 are not specific to IBS or to this RFC's
backend. Each is preparatory infrastructure that any consumer of
damon_report_access() will need:
- Patch 1 (refcount ops owner) -- any modular ops set, including
out-of-tree backends, needs clean module unload to avoid UAF
on damon_unregister_ops.
- Patch 3 (per-CPU lockless ring) -- damon_report_access() cannot
be called from NMI context with the current mutex-protected
buffer. Hardware samplers all need NMI-safe submission.
- Patch 4 (flat-array snapshot + bsearch drain) -- the linear-
scan drain is O(reports x regions) and exceeds the sample
interval at high-CPU x large-region products. Bsearch brings
it to O(reports x log regions).
If these belong directly on damon/next as preparatory patches for
damon_report_access() rather than living inside an IBS-specific
track, we are happy to rebase and resend them that way.
Relation to prior and ongoing work
==================================
The IBS sampling pattern in patch 7 -- attr.config=0 to use IBS Op
default config, dc_phy_addr_valid filter, NMI-safe sample submission
-- is derived from concepts in Bharata B Rao's pghot RFC v5 [3].
The attribution header is in mm/damon/damon_ibs.c and the patch
carries a Suggested-by: trailer.
Bharata's pghot v7 [4] introduces a different IBS driver targeting
the new IBS Memory Profiler (IBS-MProf) facility, which Bharata
describes as a facility "that will be present in future AMD
processors" -- a separate IBS instance from the one this RFC's
backend uses. This version of driver based out of v5 [3] is an
example of how DAMON can be benefited from AMD IBS Hardware
source and validates importance of IBS information indepedently.
It is not meant to be merged in the current form.
@Bharata if you see a path where IBS samples can be consumed
by DAMON at some point, will be happy to collaborate.
Akinobu Mita's perf-event-based access-check RFC [5] explores a
configurable perf-event-driven access source for DAMON. IBS has
vendor-specific MSR setup beyond what perf_event_attr alone
expresses (e.g. dc_phy_addr_valid filtering on the produced sample,
not on the perf attr), so the IBS path here appears complementary
to [5] -- operators choose based on whether their hardware sampler
fits stock perf or needs additional kernel-side setup.
Specific asks
=============
To SeongJae:
1. Patches 1, 3, and 4 are infrastructure that benefits any consumer
of damon_report_access(), not just the IBS backend in this RFC.
Would these belong directly on damon/next as preparatory patches
for damon_report_access(), rather than living inside an
IBS-specific track? Happy to rebase and resend them that way if
you'd prefer that shape. Tested-by: tags can come along.
Future work
===========
- Longer-duration stability and broader workload coverage.
Test branch
===========
A single fetch reproduces the cover-letter measurements on top of
both this RFC and the companion DAMOS quota controller and paddr
migration walk fixes posted separately at [6]:
git fetch https://github.com/ravis-opensrc/linux.git \
damon/hw-hotness-rfc-v1-testing
The companion fixes are not required for this RFC to function, but
the closed-loop measurements above were collected on the testing
branch which has both applied. The standalone series-only branches
are also available:
git fetch https://github.com/ravis-opensrc/linux.git \
damon/hw-hotness-rfc-v1
git fetch https://github.com/ravis-opensrc/linux.git \
damon/closed-loop-fixes-v1
Links
=====
[1] [RFC PATCH v3 00/37] mm/damon: introduce per-CPUs/threads/
write/read monitoring (SeongJae Park)
https://lore.kernel.org/linux-mm/20251208062943.68824-1-sj@kernel.org/
Patch 01 introduces damon_report_access(), the consumer API
this RFC builds on.
[2] mm/damon: add node_eligible_mem_bp goal metric
https://lore.kernel.org/linux-mm/20260428030520.701-1-ravis.opensrc@gmail.com/
[3] [RFC PATCH v5 00/10] mm: Hot page tracking and promotion
infrastructure (Bharata B Rao)
https://lore.kernel.org/linux-mm/20260129144043.231636-1-bharata@amd.com/
[4] [PATCH v7 0/7] mm: Hot page tracking and promotion
infrastructure (Bharata B Rao)
https://lore.kernel.org/linux-mm/20260504060924.344313-1-bharata@amd.com/
[5] [RFC PATCH v3 0/4] mm/damon: introduce perf event based access
check (Akinobu Mita)
https://lore.kernel.org/linux-mm/20260423004211.7037-1-akinobu.mita@gmail.com/
[6] [PATCH 0/5] mm/damon: DAMOS quota controller and paddr
migration walk fixes (Ravi Jonnalagadda)
https://lore.kernel.org/linux-mm/20260516210357.2247-1-ravis.opensrc@gmail.com/
Ravi Jonnalagadda (7):
mm/damon/core: refcount ops owner module to prevent rmmod UAF
mm/damon/paddr: export damon_pa_* ops for IBS module
mm/damon/core: replace mutex-protected report buffer with per-CPU
lockless ring
mm/damon/core: flat-array snapshot + bsearch in ring-drain loop
mm/damon: add sysfs binding and dispatch hookup for paddr_ibs
operations
mm/damon/core: accept paddr_ibs in node_eligible_mem_bp ops check
mm/damon/damon_ibs: add AMD IBS-based access sampling backend
include/linux/damon.h | 13 ++
mm/damon/Kconfig | 10 +
mm/damon/Makefile | 1 +
mm/damon/core.c | 341 +++++++++++++++++++++++++++------
mm/damon/damon_ibs.c | 369 ++++++++++++++++++++++++++++++++++++
mm/damon/ops-common.h | 13 ++
mm/damon/paddr.c | 15 +-
mm/damon/sysfs.c | 12 +-
mm/damon/tests/core-kunit.h | 2 +-
9 files changed, 707 insertions(+), 69 deletions(-)
create mode 100644 mm/damon/damon_ibs.c
base-commit: 606bfbf72120df4f406ef46971d48053706f6f75
--
2.43.0
^ permalink raw reply
* [RFC PATCH 1/7] mm/damon/core: refcount ops owner module to prevent rmmod UAF
From: Ravi Jonnalagadda @ 2026-05-16 22:34 UTC (permalink / raw)
To: sj, damon, linux-mm, linux-kernel, linux-doc
Cc: akpm, corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun,
ravis.opensrc, bharata
In-Reply-To: <20260516223439.4033-1-ravis.opensrc@gmail.com>
damon_select_ops() copies the registered damon_operations struct into
ctx->ops by value. After damon_unregister_ops() is called from a
backend module's exit path, the registry slot is cleared but any
surviving ctx still holds function pointers that resolve into the
unloaded module's text. Restarting kdamond on such a ctx, or invoking
any ops callback, jumps into freed code.
Add a struct module *owner field to damon_operations. In
damon_select_ops(), take a reference to ops->owner via try_module_get()
after locating the registry entry; on failure return -EBUSY without
binding the ctx. If the ctx already had an ops bound (re-select
case), drop the previous owner's reference before installing the new
one to keep the refcount balanced. In damon_destroy_ctx(), release
the reference via module_put(ctx->ops.owner).
In damon_commit_ctx(), the live ops field is overwritten by a value
copy from src. Balance the refcount when the owner changes: take a
ref on the new owner (return -EBUSY on failure) and put the ref on the
old owner before the assignment.
Built-in ops sets (vaddr, paddr) leave owner = NULL; try_module_get(NULL)
returns true and module_put(NULL) is a no-op. Loadable backends set
owner = THIS_MODULE in their registration.
Also add damon_unregister_ops() so loadable backends have a clean exit
path.
Signed-off-by: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
---
include/linux/damon.h | 4 ++++
mm/damon/core.c | 46 ++++++++++++++++++++++++++++++++++---
mm/damon/tests/core-kunit.h | 2 +-
3 files changed, 48 insertions(+), 4 deletions(-)
diff --git a/include/linux/damon.h b/include/linux/damon.h
index df7910a39b407..8e6e1cd89e551 100644
--- a/include/linux/damon.h
+++ b/include/linux/damon.h
@@ -682,6 +682,8 @@ enum damon_ops_id {
* struct damon_operations - Monitoring operations for given use cases.
*
* @id: Identifier of this operations set.
+ * @owner: Module that provides this operations set, or NULL
+ * for built-in ops.
* @init: Initialize operations-related data structures.
* @update: Update operations-related data structures.
* @prepare_access_checks: Prepare next access check of target regions.
@@ -728,6 +730,7 @@ enum damon_ops_id {
*/
struct damon_operations {
enum damon_ops_id id;
+ struct module *owner;
void (*init)(struct damon_ctx *context);
void (*update)(struct damon_ctx *context);
void (*prepare_access_checks)(struct damon_ctx *context);
@@ -1206,6 +1209,7 @@ int damon_commit_ctx(struct damon_ctx *old_ctx, struct damon_ctx *new_ctx);
int damon_nr_running_ctxs(void);
bool damon_is_registered_ops(enum damon_ops_id id);
int damon_register_ops(struct damon_operations *ops);
+int damon_unregister_ops(enum damon_ops_id id);
int damon_select_ops(struct damon_ctx *ctx, enum damon_ops_id id);
static inline bool damon_target_has_pid(const struct damon_ctx *ctx)
diff --git a/mm/damon/core.c b/mm/damon/core.c
index e4b9adc0a64dd..b605d36b29b1a 100644
--- a/mm/damon/core.c
+++ b/mm/damon/core.c
@@ -12,6 +12,7 @@
#include <linux/kthread.h>
#include <linux/memcontrol.h>
#include <linux/mm.h>
+#include <linux/module.h>
#include <linux/psi.h>
#include <linux/sched.h>
#include <linux/slab.h>
@@ -93,6 +94,31 @@ int damon_register_ops(struct damon_operations *ops)
mutex_unlock(&damon_ops_lock);
return err;
}
+EXPORT_SYMBOL_GPL(damon_register_ops);
+
+/**
+ * damon_unregister_ops() - Unregister a monitoring operations set.
+ * @id: ID of the operations set to unregister.
+ *
+ * Return: 0 on success, negative error code otherwise.
+ */
+int damon_unregister_ops(enum damon_ops_id id)
+{
+ if (id >= NR_DAMON_OPS)
+ return -EINVAL;
+
+ /*
+ * Callers (typically the owning module exit path) hold a
+ * module ref via try_module_get() in damon_select_ops(); the
+ * unregister cannot race with active ctxs because module_exit
+ * runs only at owner refcount 0.
+ */
+ mutex_lock(&damon_ops_lock);
+ memset(&damon_registered_ops[id], 0, sizeof(damon_registered_ops[id]));
+ mutex_unlock(&damon_ops_lock);
+ return 0;
+}
+EXPORT_SYMBOL_GPL(damon_unregister_ops);
/**
* damon_select_ops() - Select a monitoring operations to use with the context.
@@ -112,10 +138,18 @@ int damon_select_ops(struct damon_ctx *ctx, enum damon_ops_id id)
return -EINVAL;
mutex_lock(&damon_ops_lock);
- if (!__damon_is_registered_ops(id))
+ if (!__damon_is_registered_ops(id)) {
err = -EINVAL;
- else
- ctx->ops = damon_registered_ops[id];
+ goto out;
+ }
+ if (!try_module_get(damon_registered_ops[id].owner)) {
+ err = -EBUSY;
+ goto out;
+ }
+ /* Drop previous owner ref if this ctx had ops selected before. */
+ module_put(ctx->ops.owner);
+ ctx->ops = damon_registered_ops[id];
+out:
mutex_unlock(&damon_ops_lock);
return err;
}
@@ -835,6 +869,7 @@ void damon_destroy_ctx(struct damon_ctx *ctx)
damon_for_each_sample_filter_safe(f, next_f, &ctx->sample_control)
damon_destroy_sample_filter(f, &ctx->sample_control);
+ module_put(ctx->ops.owner);
kfree(ctx);
}
@@ -1749,6 +1784,11 @@ int damon_commit_ctx(struct damon_ctx *dst, struct damon_ctx *src)
return err;
}
dst->pause = src->pause;
+ if (src->ops.owner != dst->ops.owner) {
+ if (!try_module_get(src->ops.owner))
+ return -EBUSY;
+ module_put(dst->ops.owner);
+ }
dst->ops = src->ops;
err = damon_commit_probes(dst, src);
if (err)
diff --git a/mm/damon/tests/core-kunit.h b/mm/damon/tests/core-kunit.h
index 0369c717b93db..300659b115602 100644
--- a/mm/damon/tests/core-kunit.h
+++ b/mm/damon/tests/core-kunit.h
@@ -342,7 +342,7 @@ static void damon_test_split_regions_of(struct kunit *test)
static void damon_test_ops_registration(struct kunit *test)
{
struct damon_ctx *c = damon_new_ctx();
- struct damon_operations ops = {.id = DAMON_OPS_VADDR}, bak;
+ struct damon_operations ops = {.id = DAMON_OPS_VADDR}, bak = {};
bool need_cleanup = false;
if (!c)
--
2.43.0
^ permalink raw reply related
* [RFC PATCH 2/7] mm/damon/paddr: export damon_pa_* ops for IBS module
From: Ravi Jonnalagadda @ 2026-05-16 22:34 UTC (permalink / raw)
To: sj, damon, linux-mm, linux-kernel, linux-doc
Cc: akpm, corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun,
ravis.opensrc, bharata
In-Reply-To: <20260516223439.4033-1-ravis.opensrc@gmail.com>
Remove static qualifier from damon_pa_prepare_access_checks,
damon_pa_check_accesses, damon_pa_apply_probes, damon_pa_apply_scheme,
and damon_pa_scheme_score. Add EXPORT_SYMBOL_GPL for each.
These functions are used as ops callbacks by the IBS backend module (damon_ibs.ko)
which registers paddr_ibs operations.
Signed-off-by: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
---
mm/damon/ops-common.h | 13 +++++++++++++
mm/damon/paddr.c | 15 ++++++++++-----
2 files changed, 23 insertions(+), 5 deletions(-)
diff --git a/mm/damon/ops-common.h b/mm/damon/ops-common.h
index 5efa5b5970def..0ec75276d985a 100644
--- a/mm/damon/ops-common.h
+++ b/mm/damon/ops-common.h
@@ -23,3 +23,16 @@ bool damos_folio_filter_match(struct damos_filter *filter, struct folio *folio);
unsigned long damon_migrate_pages(struct list_head *folio_list, int target_nid);
bool damos_ops_has_filter(struct damos *s);
+
+/*
+ * paddr ops callbacks, declared here so paddr-family backends
+ * (e.g. paddr_ibs) can reuse the paddr operation implementations.
+ */
+void damon_pa_prepare_access_checks(struct damon_ctx *ctx);
+unsigned int damon_pa_check_accesses(struct damon_ctx *ctx);
+void damon_pa_apply_probes(struct damon_ctx *ctx);
+unsigned long damon_pa_apply_scheme(struct damon_ctx *ctx,
+ struct damon_target *t, struct damon_region *r,
+ struct damos *scheme, unsigned long *sz_filter_passed);
+int damon_pa_scheme_score(struct damon_ctx *context,
+ struct damon_region *r, struct damos *scheme);
diff --git a/mm/damon/paddr.c b/mm/damon/paddr.c
index fc2154b6221fb..5af4ac2a7ed4d 100644
--- a/mm/damon/paddr.c
+++ b/mm/damon/paddr.c
@@ -124,13 +124,14 @@ static void damon_pa_prepare_access_checks_faults(struct damon_ctx *ctx)
}
}
-static void damon_pa_prepare_access_checks(struct damon_ctx *ctx)
+void damon_pa_prepare_access_checks(struct damon_ctx *ctx)
{
if (ctx->sample_control.primitives_enabled.page_table)
damon_pa_prepare_access_checks_abit(ctx);
if (ctx->sample_control.primitives_enabled.page_fault)
damon_pa_prepare_access_checks_faults(ctx);
}
+EXPORT_SYMBOL_GPL(damon_pa_prepare_access_checks);
static bool damon_pa_young(phys_addr_t paddr, unsigned long *folio_sz)
{
@@ -168,7 +169,7 @@ static void __damon_pa_check_access(struct damon_region *r,
last_addr = sampling_addr;
}
-static unsigned int damon_pa_check_accesses(struct damon_ctx *ctx)
+unsigned int damon_pa_check_accesses(struct damon_ctx *ctx)
{
struct damon_target *t;
struct damon_region *r;
@@ -184,6 +185,7 @@ static unsigned int damon_pa_check_accesses(struct damon_ctx *ctx)
return max_nr_accesses;
}
+EXPORT_SYMBOL_GPL(damon_pa_check_accesses);
static bool damon_pa_filter_match(struct damon_filter *filter,
struct folio *folio)
@@ -234,7 +236,7 @@ static bool damon_pa_filter_pass(phys_addr_t pa, struct folio *folio,
return pass;
}
-static void damon_pa_apply_probes(struct damon_ctx *ctx)
+void damon_pa_apply_probes(struct damon_ctx *ctx)
{
struct damon_target *t;
struct damon_region *r;
@@ -259,6 +261,7 @@ static void damon_pa_apply_probes(struct damon_ctx *ctx)
}
}
}
+EXPORT_SYMBOL_GPL(damon_pa_apply_probes);
/*
* damos_pa_filter_out - Return true if the page should be filtered out.
@@ -542,7 +545,7 @@ static unsigned long damon_pa_alloc_or_free(
#endif
-static unsigned long damon_pa_apply_scheme(struct damon_ctx *ctx,
+unsigned long damon_pa_apply_scheme(struct damon_ctx *ctx,
struct damon_target *t, struct damon_region *r,
struct damos *scheme, unsigned long *sz_filter_passed)
{
@@ -574,8 +577,9 @@ static unsigned long damon_pa_apply_scheme(struct damon_ctx *ctx,
}
return 0;
}
+EXPORT_SYMBOL_GPL(damon_pa_apply_scheme);
-static int damon_pa_scheme_score(struct damon_ctx *context,
+int damon_pa_scheme_score(struct damon_ctx *context,
struct damon_region *r, struct damos *scheme)
{
switch (scheme->action) {
@@ -595,6 +599,7 @@ static int damon_pa_scheme_score(struct damon_ctx *context,
return DAMOS_MAX_SCORE;
}
+EXPORT_SYMBOL_GPL(damon_pa_scheme_score);
static int __init damon_pa_initcall(void)
{
--
2.43.0
^ permalink raw reply related
* [RFC PATCH 3/7] mm/damon/core: replace mutex-protected report buffer with per-CPU lockless ring
From: Ravi Jonnalagadda @ 2026-05-16 22:34 UTC (permalink / raw)
To: sj, damon, linux-mm, linux-kernel, linux-doc
Cc: akpm, corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun,
ravis.opensrc, bharata
In-Reply-To: <20260516223439.4033-1-ravis.opensrc@gmail.com>
Replace the mutex-protected fixed-size array (DAMON_ACCESS_REPORTS_CAP=1000)
with a per-CPU lockless ring buffer. This enables damon_report_access()
to be called from NMI context.
Ring design:
- Producer is serialized per CPU: only one in-flight producer per CPU
is allowed. A per-CPU damon_report_ring_busy counter detects
NMI-on-process nesting and drops the nested attempt, preserving the
single-writer invariant on the slot.
- head is advanced by the producer with smp_wmb() before publish.
- tail is advanced by the consumer (kdamond) after the entries[] reads.
- Overflow: sample silently dropped. NMI context is allocation-free
and access reports are best-effort.
To keep the producer/consumer pattern scalable on systems with many
CPUs and a high NMI rate, the ring layout follows three rules:
- head, tail and entries[] live on separate cache lines via
____cacheline_aligned_in_smp, so producer and consumer do not
invalidate each other's working set on every advance.
- DAMON_REPORT_RING_SIZE is bounded so the per-CPU footprint stays
small (256 entries x sizeof(struct damon_access_report) plus head
and tail cache lines), keeping draining all rings during one
kdamond tick from evicting unrelated data on contemporary server
parts.
- A cpumask, damon_rings_pending, is set by the producer after
publishing and cleared by the consumer per ring drained, so the
consumer iterates only CPUs with pending entries instead of
walking every online CPU. An smp_mb__before_atomic() between the
head publish and the cpumask_set_cpu() ensures observers of the
pending bit also observe the published head; without it, weakly-
ordered architectures could let the consumer drain stale head and
delay the report. The consumer pairs this with an
smp_mb__after_atomic() between cpumask_clear_cpu() and reading
head, so a producer that publishes between the consumer's clear
and head-read is observed via the bit it re-sets rather than
silently stranded.
Consumer (kdamond_check_reported_accesses) drains the rings of CPUs
in damon_rings_pending, applying reports to targets.
Signed-off-by: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
---
mm/damon/core.c | 143 ++++++++++++++++++++++++++++++++++--------------
1 file changed, 101 insertions(+), 42 deletions(-)
diff --git a/mm/damon/core.c b/mm/damon/core.c
index b605d36b29b1a..9ed789e932ebd 100644
--- a/mm/damon/core.c
+++ b/mm/damon/core.c
@@ -25,7 +25,26 @@
#define CREATE_TRACE_POINTS
#include <trace/events/damon.h>
-#define DAMON_ACCESS_REPORTS_CAP 1000
+/* Sized so the per-CPU ring set fits in L3 on typical multi-socket boxes. */
+#define DAMON_REPORT_RING_SIZE 256
+#define DAMON_REPORT_RING_MASK (DAMON_REPORT_RING_SIZE - 1)
+
+struct damon_report_ring {
+ unsigned int head; /* written by producer (NMI) */
+ unsigned int tail /* written by consumer (kdamond) */
+ ____cacheline_aligned_in_smp;
+ struct damon_access_report entries[DAMON_REPORT_RING_SIZE]
+ ____cacheline_aligned_in_smp;
+};
+
+static DEFINE_PER_CPU(struct damon_report_ring, damon_report_rings);
+static DEFINE_PER_CPU(int, damon_report_ring_busy);
+/*
+ * Per-CPU bitmap: producer (NMI) sets after publishing a report;
+ * consumer (kdamond) clears before draining the corresponding ring.
+ * Hot-write under sampling load - do NOT mark __read_mostly.
+ */
+static cpumask_t damon_rings_pending;
static DEFINE_MUTEX(damon_lock);
static int nr_running_ctxs;
@@ -36,10 +55,6 @@ static struct damon_operations damon_registered_ops[NR_DAMON_OPS];
static struct kmem_cache *damon_region_cache __ro_after_init;
-static DEFINE_MUTEX(damon_access_reports_lock);
-static struct damon_access_report damon_access_reports[
- DAMON_ACCESS_REPORTS_CAP];
-static int damon_access_reports_len;
/* Should be called under damon_ops_lock with id smaller than NR_DAMON_OPS */
static bool __damon_is_registered_ops(enum damon_ops_id id)
@@ -2127,33 +2142,56 @@ int damos_walk(struct damon_ctx *ctx, struct damos_walk_control *control)
}
/**
- * damon_report_access() - Report identified access events to DAMON.
- * @report: The reporting access information.
+ * damon_report_access() - Report a hardware-observed memory access.
+ * @report: pointer to a filled damon_access_report struct.
*
- * Report access events to DAMON.
- *
- * Context: May sleep.
- *
- * NOTE: we may be able to implement this as a lockless queue, and allow any
- * context. As the overhead is unknown, and region-based DAMON logics would
- * guarantee the reports would be not made that frequently, let's start with
- * this simple implementation.
+ * Context: NMI-safe. No sleeping, no allocation, no locks.
*/
void damon_report_access(struct damon_access_report *report)
{
- struct damon_access_report *dst;
+ struct damon_report_ring *ring;
+ unsigned int head, next;
- /* silently fail for races */
- if (!mutex_trylock(&damon_access_reports_lock))
- return;
- dst = &damon_access_reports[damon_access_reports_len++];
- /* just drop all existing reports in favor of simplicity. */
- if (damon_access_reports_len == DAMON_ACCESS_REPORTS_CAP)
- damon_access_reports_len = 0;
- *dst = *report;
- dst->report_jiffies = jiffies;
- mutex_unlock(&damon_access_reports_lock);
+ /* Pin to a CPU so the SPSC invariant holds for preemptible callers. */
+ preempt_disable();
+ /*
+ * NMI nesting on the same CPU as a process-context producer would
+ * stomp the same entries[head] slot. Detect and drop instead.
+ */
+ if (this_cpu_inc_return(damon_report_ring_busy) != 1) {
+ /* NMI nested on a process-context producer; drop. */
+ goto out;
+ }
+
+ ring = this_cpu_ptr(&damon_report_rings);
+ head = ring->head;
+ next = (head + 1) & DAMON_REPORT_RING_MASK;
+
+ if (next == READ_ONCE(ring->tail)) {
+ /* Ring full; consumer is behind, drop the report. */
+ goto out;
+ }
+
+ ring->entries[head] = *report;
+ ring->entries[head].report_jiffies = jiffies;
+ smp_wmb(); /* ensure entry visible before head advance */
+ WRITE_ONCE(ring->head, next);
+ /*
+ * Order the head advance before publishing the pending bit
+ * so that the consumer, on observing the bit, is also
+ * guaranteed to observe the new head. set_bit/cpumask_set_cpu
+ * are documented as unordered RMW (atomic_bitops.txt), hence
+ * the explicit barrier; without it, a weakly-ordered arch
+ * could let the consumer drain stale head, clear the bit, and
+ * delay the report until the next producer sets the bit again.
+ */
+ smp_mb__before_atomic();
+ cpumask_set_cpu(smp_processor_id(), &damon_rings_pending);
+out:
+ this_cpu_dec(damon_report_ring_busy);
+ preempt_enable();
}
+EXPORT_SYMBOL_GPL(damon_report_access);
#ifdef CONFIG_MMU
void damon_report_page_fault(struct vm_fault *vmf, bool huge_pmd)
@@ -3814,26 +3852,47 @@ static unsigned int kdamond_apply_zero_access_report(struct damon_ctx *ctx)
static unsigned int kdamond_check_reported_accesses(struct damon_ctx *ctx)
{
- int i;
- struct damon_access_report *report;
+ int cpu;
struct damon_target *t;
- /* currently damon_access_report supports only physical address */
- if (damon_target_has_pid(ctx))
- return 0;
+ for_each_cpu(cpu, &damon_rings_pending) {
+ struct damon_report_ring *ring =
+ per_cpu_ptr(&damon_report_rings, cpu);
+ unsigned int head, tail;
- mutex_lock(&damon_access_reports_lock);
- for (i = 0; i < damon_access_reports_len; i++) {
- report = &damon_access_reports[i];
- if (time_before(report->report_jiffies,
- jiffies -
- usecs_to_jiffies(
- ctx->attrs.sample_interval)))
- continue;
- damon_for_each_target(t, ctx)
- kdamond_apply_access_report(report, t, ctx);
+ cpumask_clear_cpu(cpu, &damon_rings_pending);
+ /*
+ * Pair with the producer's smp_mb__before_atomic() between
+ * the head publish and cpumask_set_cpu(): order the bit
+ * clear before the head read so that a producer publishing
+ * between our clear and our READ_ONCE(head) is observed via
+ * the bit it re-sets, not lost as a stale-head drain.
+ */
+ smp_mb__after_atomic();
+ head = READ_ONCE(ring->head);
+ /*
+ * Pair with smp_wmb in damon_report_access(): the entry
+ * data published before the producer advanced head must be
+ * visible to the entries[] reads inside the loop below.
+ */
+ smp_rmb();
+ tail = ring->tail;
+
+ while (tail != head) {
+ struct damon_access_report *report =
+ &ring->entries[tail];
+
+ if (!time_before(report->report_jiffies,
+ jiffies - usecs_to_jiffies(
+ ctx->attrs.sample_interval))) {
+ damon_for_each_target(t, ctx)
+ kdamond_apply_access_report(
+ report, t, ctx);
+ }
+ tail = (tail + 1) & DAMON_REPORT_RING_MASK;
+ }
+ WRITE_ONCE(ring->tail, tail);
}
- mutex_unlock(&damon_access_reports_lock);
/* For nr_accesses_bp, absence of access should also be reported. */
return kdamond_apply_zero_access_report(ctx);
}
--
2.43.0
^ permalink raw reply related
* [RFC PATCH 5/7] mm/damon: add sysfs binding and dispatch hookup for paddr_ibs operations
From: Ravi Jonnalagadda @ 2026-05-16 22:34 UTC (permalink / raw)
To: sj, damon, linux-mm, linux-kernel, linux-doc
Cc: akpm, corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun,
ravis.opensrc, bharata
In-Reply-To: <20260516223439.4033-1-ravis.opensrc@gmail.com>
Extend damon_ops_id enum to include DAMON_OPS_PADDR_IBS and add the
corresponding 'paddr_ibs' name to the sysfs ops_names array so users
can select AMD IBS-based PA-mode monitoring via
/sys/kernel/mm/damon/admin/kdamonds/<N>/contexts/<N>/operations.
Route ops that report accesses through the hardware-sampling ring
(currently only DAMON_OPS_PADDR_IBS) through the existing
kdamond_check_reported_accesses() drain path used for page-fault
reports. A small helper damon_ops_is_hw_hotness() centralises the
classification so any future paddr-family backend that also reports
through the ring just adds a case here.
This routing is bound to ops.id rather than to a separate per-context
flag. A flag in damon_sample_control would have to be set by the ops
.init callback after damon_select_ops() and would then need to be
preserved by damon_commit_sample_control() across sysfs commits;
deriving from ops.id avoids both pitfalls.
Signed-off-by: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
---
include/linux/damon.h | 2 ++
mm/damon/core.c | 13 ++++++++++++-
mm/damon/sysfs.c | 12 +++++++++---
3 files changed, 23 insertions(+), 4 deletions(-)
diff --git a/include/linux/damon.h b/include/linux/damon.h
index 35cc3d42fcba8..16da528845d03 100644
--- a/include/linux/damon.h
+++ b/include/linux/damon.h
@@ -669,12 +669,14 @@ struct damos {
* @DAMON_OPS_FVADDR: Monitoring operations for only fixed ranges of virtual
* address spaces
* @DAMON_OPS_PADDR: Monitoring operations for the physical address space
+ * @DAMON_OPS_PADDR_IBS: AMD IBS-based PA-mode monitoring
* @NR_DAMON_OPS: Number of monitoring operations implementations
*/
enum damon_ops_id {
DAMON_OPS_VADDR,
DAMON_OPS_FVADDR,
DAMON_OPS_PADDR,
+ DAMON_OPS_PADDR_IBS,
NR_DAMON_OPS,
};
diff --git a/mm/damon/core.c b/mm/damon/core.c
index 03f9c671e8bc9..2aa031cbc70b7 100644
--- a/mm/damon/core.c
+++ b/mm/damon/core.c
@@ -73,6 +73,16 @@ static bool __damon_is_registered_ops(enum damon_ops_id id)
return true;
}
+/*
+ * Returns true if the given ops id reports access samples through the
+ * hardware-sampling ring-buffer drain path (rather than its own
+ * .check_accesses callback).
+ */
+static bool damon_ops_is_hw_hotness(enum damon_ops_id id)
+{
+ return id == DAMON_OPS_PADDR_IBS;
+}
+
/**
* damon_is_registered_ops() - Check if a given damon_operations is registered.
* @id: Id of the damon_operations to check if registered.
@@ -4048,7 +4058,8 @@ static int kdamond_fn(void *data)
ctx->passed_sample_intervals++;
/* todo: make these non-exclusive */
- if (ctx->sample_control.primitives_enabled.page_fault)
+ if (ctx->sample_control.primitives_enabled.page_fault ||
+ damon_ops_is_hw_hotness(ctx->ops.id))
max_nr_accesses = kdamond_check_reported_accesses(ctx);
else if (ctx->ops.check_accesses)
max_nr_accesses = ctx->ops.check_accesses(ctx);
diff --git a/mm/damon/sysfs.c b/mm/damon/sysfs.c
index fc7256e522a69..261ccf0c61846 100644
--- a/mm/damon/sysfs.c
+++ b/mm/damon/sysfs.c
@@ -1388,6 +1388,10 @@ static const struct damon_sysfs_ops_name damon_sysfs_ops_names[] = {
.ops_id = DAMON_OPS_PADDR,
.name = "paddr",
},
+ {
+ .ops_id = DAMON_OPS_PADDR_IBS,
+ .name = "paddr_ibs",
+ },
};
struct damon_sysfs_context {
@@ -2023,7 +2027,8 @@ static int damon_sysfs_add_targets(struct damon_ctx *ctx,
int i, err;
/* Multiple physical address space monitoring targets makes no sense */
- if (ctx->ops.id == DAMON_OPS_PADDR && sysfs_targets->nr > 1)
+ if ((ctx->ops.id == DAMON_OPS_PADDR ||
+ ctx->ops.id == DAMON_OPS_PADDR_IBS) && sysfs_targets->nr > 1)
return -EINVAL;
for (i = 0; i < sysfs_targets->nr; i++) {
@@ -2072,8 +2077,9 @@ static int damon_sysfs_apply_inputs(struct damon_ctx *ctx,
if (err)
return err;
ctx->addr_unit = sys_ctx->addr_unit;
- /* addr_unit is respected by only DAMON_OPS_PADDR */
- if (sys_ctx->ops_id == DAMON_OPS_PADDR)
+ /* addr_unit is respected by only paddr-family ops */
+ if (sys_ctx->ops_id == DAMON_OPS_PADDR ||
+ sys_ctx->ops_id == DAMON_OPS_PADDR_IBS)
ctx->min_region_sz = max(
DAMON_MIN_REGION_SZ / sys_ctx->addr_unit, 1);
ctx->pause = sys_ctx->pause;
--
2.43.0
^ permalink raw reply related
* [RFC PATCH 4/7] mm/damon/core: flat-array snapshot + bsearch in ring-drain loop
From: Ravi Jonnalagadda @ 2026-05-16 22:34 UTC (permalink / raw)
To: sj, damon, linux-mm, linux-kernel, linux-doc
Cc: akpm, corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun,
ravis.opensrc, bharata
In-Reply-To: <20260516223439.4033-1-ravis.opensrc@gmail.com>
The drain loop is O(reports x regions) when matching each ring entry
back to a region. At sufficiently large CPU x region products the
linear scan exceeds the sample interval, producing unbounded backlog.
At drain start, snapshot each target's regions into a flat array
(struct damon_target_lookup), already sorted by ascending r->ar.start.
Replace the linear lookup with binary search over the snapshot,
reducing the drain to O(reports x log2(regions)).
Reject reports that straddle a region boundary
(addr + report->size > r->ar.end) so partial-region accesses are
not credited to the lower region.
If the snapshot allocation fails under memory pressure, log
ratelimited and fall through to zero-access reporting; the next
tick retries.
Hoist the per-report damon_sample_filter_out() check out of the
per-target loop so it runs once per ring entry rather than N times.
Signed-off-by: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
---
include/linux/damon.h | 7 +++
mm/damon/core.c | 137 ++++++++++++++++++++++++++++++++++++------
2 files changed, 126 insertions(+), 18 deletions(-)
diff --git a/include/linux/damon.h b/include/linux/damon.h
index 8e6e1cd89e551..35cc3d42fcba8 100644
--- a/include/linux/damon.h
+++ b/include/linux/damon.h
@@ -1045,6 +1045,13 @@ struct damon_ctx {
/* Per-ctx PRNG state for damon_rand(); kdamond is the sole consumer. */
struct rnd_state rnd_state;
+ /* Reusable drain-loop snapshot buffer (avoids per-tick kmalloc) */
+ struct {
+ struct damon_target_lookup *lookups;
+ unsigned int nr_lookups;
+ struct damon_region **region_buf;
+ unsigned int region_buf_cap;
+ } drain_snapshot;
};
/* Get a random number in [@l, @r) using @ctx's lockless PRNG. */
diff --git a/mm/damon/core.c b/mm/damon/core.c
index 9ed789e932ebd..03f9c671e8bc9 100644
--- a/mm/damon/core.c
+++ b/mm/damon/core.c
@@ -29,6 +29,13 @@
#define DAMON_REPORT_RING_SIZE 256
#define DAMON_REPORT_RING_MASK (DAMON_REPORT_RING_SIZE - 1)
+/* Per-target region lookup for drain loop */
+struct damon_target_lookup {
+ struct damon_region **regions;
+ unsigned int nr_regions;
+};
+
+
struct damon_report_ring {
unsigned int head; /* written by producer (NMI) */
unsigned int tail /* written by consumer (kdamond) */
@@ -855,6 +862,7 @@ struct damon_ctx *damon_new_ctx(void)
INIT_LIST_HEAD(&ctx->schemes);
prandom_seed_state(&ctx->rnd_state, get_random_u64());
+ /* drain_snapshot zero-initialized by kzalloc — no explicit init */
return ctx;
}
@@ -884,6 +892,8 @@ void damon_destroy_ctx(struct damon_ctx *ctx)
damon_for_each_sample_filter_safe(f, next_f, &ctx->sample_control)
damon_destroy_sample_filter(f, &ctx->sample_control);
+ kfree(ctx->drain_snapshot.lookups);
+ kfree(ctx->drain_snapshot.region_buf);
module_put(ctx->ops.owner);
kfree(ctx);
}
@@ -3806,27 +3816,44 @@ static bool damon_sample_filter_out(struct damon_access_report *report,
return !filter->allow;
}
+
static void kdamond_apply_access_report(struct damon_access_report *report,
- struct damon_target *t, struct damon_ctx *ctx)
+ struct damon_region **regions, unsigned int nr_regions,
+ struct damon_ctx *ctx)
{
struct damon_region *r;
unsigned long addr;
+ int left, right, mid;
- if (damon_sample_filter_out(report, &ctx->sample_control))
- return;
if (damon_target_has_pid(ctx))
addr = report->vaddr;
else
addr = report->paddr;
- /* todo: make search faster, e.g., binary search? */
- damon_for_each_region(r, t) {
- if (addr < r->ar.start)
- continue;
- if (r->ar.end < addr + report->size)
- continue;
- if (!r->access_reported)
- damon_update_region_access_rate(r, true, &ctx->attrs);
+ /* Binary search the snapshot for the region containing addr. */
+ left = 0;
+ right = nr_regions - 1;
+ r = NULL;
+ while (left <= right) {
+ /* Avoid (left + right) overflow at large nr_regions. */
+ mid = left + (right - left) / 2;
+ if (addr < regions[mid]->ar.start)
+ right = mid - 1;
+ else if (addr >= regions[mid]->ar.end)
+ left = mid + 1;
+ else {
+ r = regions[mid];
+ break;
+ }
+ }
+
+ if (!r)
+ return;
+ /* Reject reports straddling a region boundary. */
+ if (addr + report->size > r->ar.end)
+ return;
+ if (!r->access_reported) {
+ damon_update_region_access_rate(r, true, &ctx->attrs);
r->access_reported = true;
}
}
@@ -3850,10 +3877,79 @@ static unsigned int kdamond_apply_zero_access_report(struct damon_ctx *ctx)
return max_nr_accesses;
}
+/*
+ * Build a snapshot of the ctx's targets and their region arrays for
+ * use by the ring drain loop.
+ *
+ * The two-pass walk over adaptive_targets is safe even though
+ * krealloc_array() may sleep: target list mutation is funneled
+ * through damon_call onto the kdamond itself, so no other thread
+ * can mutate the list while kdamond is running this function.
+ */
+static struct damon_target_lookup *damon_build_target_lookup(
+ struct damon_ctx *ctx, unsigned int *nr_targets_out)
+{
+ struct damon_target *t;
+ struct damon_target_lookup *tbl;
+ unsigned int nr_targets = 0, total_regions = 0, ti = 0, ri = 0;
+
+ damon_for_each_target(t, ctx) {
+ nr_targets++;
+ total_regions += damon_nr_regions(t);
+ }
+
+ /* Realloc lookups array if needed */
+ if (nr_targets > ctx->drain_snapshot.nr_lookups) {
+ tbl = krealloc_array(ctx->drain_snapshot.lookups,
+ nr_targets, sizeof(*tbl), GFP_KERNEL);
+ if (!tbl)
+ return NULL;
+ ctx->drain_snapshot.lookups = tbl;
+ ctx->drain_snapshot.nr_lookups = nr_targets;
+ }
+ tbl = ctx->drain_snapshot.lookups;
+
+ /* Realloc contiguous region_buf if needed */
+ if (total_regions > ctx->drain_snapshot.region_buf_cap) {
+ struct damon_region **buf;
+
+ buf = krealloc_array(ctx->drain_snapshot.region_buf,
+ total_regions, sizeof(*buf), GFP_KERNEL);
+ if (!buf)
+ return NULL;
+ ctx->drain_snapshot.region_buf = buf;
+ ctx->drain_snapshot.region_buf_cap = total_regions;
+ }
+
+ /* Fill lookup table, slicing region_buf across targets */
+ ri = 0;
+ damon_for_each_target(t, ctx) {
+ struct damon_region *r;
+
+ tbl[ti].regions = &ctx->drain_snapshot.region_buf[ri];
+ tbl[ti].nr_regions = damon_nr_regions(t);
+ damon_for_each_region(r, t)
+ ctx->drain_snapshot.region_buf[ri++] = r;
+ ti++;
+ }
+
+ *nr_targets_out = nr_targets;
+ return tbl;
+}
+
static unsigned int kdamond_check_reported_accesses(struct damon_ctx *ctx)
{
int cpu;
- struct damon_target *t;
+ struct damon_target_lookup *tbl;
+ unsigned int nr_targets = 0;
+ unsigned int i;
+
+ tbl = damon_build_target_lookup(ctx, &nr_targets);
+ if (!tbl) {
+ pr_warn_ratelimited(
+ "damon: target-lookup alloc failed; ring drain skipped this tick\n");
+ return kdamond_apply_zero_access_report(ctx);
+ }
for_each_cpu(cpu, &damon_rings_pending) {
struct damon_report_ring *ring =
@@ -3881,14 +3977,19 @@ static unsigned int kdamond_check_reported_accesses(struct damon_ctx *ctx)
while (tail != head) {
struct damon_access_report *report =
&ring->entries[tail];
-
- if (!time_before(report->report_jiffies,
+ if (time_before(report->report_jiffies,
jiffies - usecs_to_jiffies(
- ctx->attrs.sample_interval))) {
- damon_for_each_target(t, ctx)
- kdamond_apply_access_report(
- report, t, ctx);
+ ctx->attrs.sample_interval)))
+ goto next;
+ if (damon_sample_filter_out(report,
+ &ctx->sample_control))
+ goto next;
+ for (i = 0; i < nr_targets; i++) {
+ kdamond_apply_access_report(report,
+ tbl[i].regions,
+ tbl[i].nr_regions, ctx);
}
+next:
tail = (tail + 1) & DAMON_REPORT_RING_MASK;
}
WRITE_ONCE(ring->tail, tail);
--
2.43.0
^ permalink raw reply related
* [RFC PATCH 6/7] mm/damon/core: accept paddr_ibs in node_eligible_mem_bp ops check
From: Ravi Jonnalagadda @ 2026-05-16 22:34 UTC (permalink / raw)
To: sj, damon, linux-mm, linux-kernel, linux-doc
Cc: akpm, corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun,
ravis.opensrc, bharata
In-Reply-To: <20260516223439.4033-1-ravis.opensrc@gmail.com>
damos_get_node_eligible_mem_bp() and the damon_commit_ctx() validation
path reject any ops.id != DAMON_OPS_PADDR, which caused paddr_ibs to
always get 0 from the node-eligible helper. This caused the quota
control loop to run open-loop (esz doubles every tick) when using the
paddr_ibs backend with a node_eligible_mem_bp goal.
Introduce damon_ops_id_is_paddr_family() and use it at both sites so
DAMON_OPS_PADDR_IBS is accepted alongside DAMON_OPS_PADDR. The helper
also gives any future paddr-family backend a single line to extend.
Signed-off-by: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
---
mm/damon/core.c | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/mm/damon/core.c b/mm/damon/core.c
index 2aa031cbc70b7..1e52161f4c015 100644
--- a/mm/damon/core.c
+++ b/mm/damon/core.c
@@ -83,6 +83,16 @@ static bool damon_ops_is_hw_hotness(enum damon_ops_id id)
return id == DAMON_OPS_PADDR_IBS;
}
+/*
+ * Returns true if the ops id treats the monitoring target as a
+ * physical-address region (no per-task PID). Used by paddr-only
+ * gates such as node_eligible_mem_bp.
+ */
+static bool damon_ops_id_is_paddr_family(enum damon_ops_id id)
+{
+ return id == DAMON_OPS_PADDR || id == DAMON_OPS_PADDR_IBS;
+}
+
/**
* damon_is_registered_ops() - Check if a given damon_operations is registered.
* @id: Id of the damon_operations to check if registered.
@@ -1787,8 +1797,8 @@ int damon_commit_ctx(struct damon_ctx *dst, struct damon_ctx *src)
if (!is_power_of_2(src->min_region_sz))
return -EINVAL;
- /* node_eligible_mem_bp metric requires PADDR ops */
- if (src->ops.id != DAMON_OPS_PADDR) {
+ /* node_eligible_mem_bp metric requires PADDR-family ops */
+ if (!damon_ops_id_is_paddr_family(src->ops.id)) {
damon_for_each_scheme(scheme, src) {
struct damos_quota *quota = &scheme->quota;
@@ -3041,7 +3051,7 @@ static unsigned long damos_get_node_eligible_mem_bp(struct damon_ctx *c,
phys_addr_t total_eligible = 0;
phys_addr_t node_eligible;
- if (c->ops.id != DAMON_OPS_PADDR)
+ if (!damon_ops_id_is_paddr_family(c->ops.id))
return 0;
if (nid < 0 || nid >= MAX_NUMNODES || !node_online(nid))
--
2.43.0
^ permalink raw reply related
* [RFC PATCH 7/7] mm/damon/damon_ibs: add AMD IBS-based access sampling backend
From: Ravi Jonnalagadda @ 2026-05-16 22:34 UTC (permalink / raw)
To: sj, damon, linux-mm, linux-kernel, linux-doc
Cc: akpm, corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun,
ravis.opensrc, bharata
In-Reply-To: <20260516223439.4033-1-ravis.opensrc@gmail.com>
Add paddr_ibs operations using AMD IBS Op sampling via
perf_event_create_kernel_counter(). IBS delivers physical-address-
keyed access reports to DAMON's shared-layer ring buffer
(damon_report_access()), without dependency on PTE Accessed-bit
scanning or page faults.
Per-CPU IBS events are created and torn down via cpuhp notifiers
(CPUHP_AP_ONLINE_DYN). Routing of access reports through the ring-
drain path is bound to ops.id == DAMON_OPS_PADDR_IBS at the dispatch
site (see "mm/damon: add sysfs binding and dispatch hookup for
paddr_ibs operations"), so .init does not need to flip a per-context
flag.
Sample-time discipline:
- PERF_SAMPLE_PHYS_ADDR is requested in attr.sample_type, but the
IBS perf driver only fills data->phys_addr when
IBS_OP_DATA3.dc_phy_addr_valid is set. Skip stale-PA samples by
inspecting data->sample_flags rather than testing phys_addr for
zero (which would also drop legitimate page 0).
- PERF_SAMPLE_DATA_SRC is requested so the perf driver decodes
IBS_OP_DATA3.{ld_op,st_op} into data->data_src.mem_op; the
backend reports load vs store accordingly via
damon_access_report.is_write.
Module parameters:
- max_cnt: IBS Op MaxCnt (writable; writes call
damon_ibs_set_sample_rate() to restart sampling at the new rate)
- samples_total / samples_filtered: per-CPU-aggregated counters
(read-only)
Source file is mm/damon/damon_ibs.c (renamed from mm/damon/ibs.c) so
the resulting module is loaded as damon_ibs.ko, avoiding the generic
"ibs" namespace.
The IBS sampling approach is derived from Bharata B Rao's pghot RFC v5
series; the attribution header is in damon_ibs.c.
Suggested-by: Bharata B Rao <bharata@amd.com>
Link: https://lore.kernel.org/linux-mm/20260129144043.231636-1-bharata@amd.com/
Signed-off-by: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
---
mm/damon/Kconfig | 10 ++
mm/damon/Makefile | 1 +
mm/damon/damon_ibs.c | 369 +++++++++++++++++++++++++++++++++++++++++++
3 files changed, 380 insertions(+)
create mode 100644 mm/damon/damon_ibs.c
diff --git a/mm/damon/Kconfig b/mm/damon/Kconfig
index ad629f0f31d8d..bb698d2717f34 100644
--- a/mm/damon/Kconfig
+++ b/mm/damon/Kconfig
@@ -131,4 +131,14 @@ config DAMON_ACMA
min/max memory for the system and maximum memory pressure stall time
ratio.
+config DAMON_IBS
+ tristate "AMD IBS-based access sampling for DAMON"
+ depends on DAMON_PADDR && CPU_SUP_AMD && X86_64 && PERF_EVENTS
+ help
+ Uses AMD IBS (Instruction-Based Sampling) hardware to deliver
+ physical-address-keyed access reports to DAMON's shared-layer
+ ring buffer, without relying on PTE Accessed-bit scanning or
+ page faults. Registers as the "paddr_ibs" operations set.
+ Requires AMD processors with IBS Op support.
+
endmenu
diff --git a/mm/damon/Makefile b/mm/damon/Makefile
index 22494754f41e8..109d0fb1db97d 100644
--- a/mm/damon/Makefile
+++ b/mm/damon/Makefile
@@ -9,3 +9,4 @@ obj-$(CONFIG_DAMON_RECLAIM) += modules-common.o reclaim.o
obj-$(CONFIG_DAMON_LRU_SORT) += modules-common.o lru_sort.o
obj-$(CONFIG_DAMON_STAT) += modules-common.o stat.o
obj-$(CONFIG_DAMON_ACMA) += modules-common.o acma.o
+obj-$(CONFIG_DAMON_IBS) += damon_ibs.o
diff --git a/mm/damon/damon_ibs.c b/mm/damon/damon_ibs.c
new file mode 100644
index 0000000000000..1dd99e91c3928
--- /dev/null
+++ b/mm/damon/damon_ibs.c
@@ -0,0 +1,369 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * DAMON IBS (Instruction-Based Sampling) backend for AMD processors.
+ *
+ * Uses AMD IBS Op sampling via the perf kernel counter infrastructure to
+ * deliver PA-keyed access reports to DAMON's shared-layer ring buffer
+ * (see damon_report_access()). This enables physical-address hot-page
+ * detection without relying on page-table Accessed bits or page faults.
+ *
+ * The IBS sampling approach in this file derives from concepts in
+ * Bharata B Rao's pghot RFC v5 series for hot page tracking.
+ * See: https://lore.kernel.org/linux-mm/20260129144043.231636-1-bharata@amd.com/
+ *
+ * Author: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
+ */
+
+#include <linux/cpu.h>
+#include <linux/fs.h>
+#include <linux/module.h>
+#include <linux/percpu.h>
+#include <linux/perf_event.h>
+#include <linux/slab.h>
+#include <linux/smp.h>
+
+#include <linux/damon.h>
+#include "ops-common.h"
+
+#define DAMON_IBS_DEFAULT_MAX_CNT 262144 /* ~4K samples/sec/core */
+#define IBS_OP_PMU_TYPE_PATH "/sys/bus/event_source/devices/ibs_op/type"
+
+static unsigned int damon_ibs_max_cnt = DAMON_IBS_DEFAULT_MAX_CNT;
+
+static int damon_ibs_set_sample_rate(unsigned int max_cnt);
+
+static int max_cnt_set(const char *val, const struct kernel_param *kp)
+{
+ unsigned int new_cnt;
+ int ret;
+
+ ret = kstrtouint(val, 0, &new_cnt);
+ if (ret)
+ return ret;
+ if (!new_cnt)
+ return -EINVAL;
+ return damon_ibs_set_sample_rate(new_cnt);
+}
+static const struct kernel_param_ops max_cnt_ops = {
+ .set = max_cnt_set,
+ .get = param_get_uint,
+};
+module_param_cb(max_cnt, &max_cnt_ops, &damon_ibs_max_cnt, 0644);
+MODULE_PARM_DESC(max_cnt,
+ "IBS MaxCnt (ops between samples). Writes restart sampling.");
+
+static DEFINE_MUTEX(damon_ibs_lock);
+static bool damon_ibs_enabled;
+static enum cpuhp_state damon_ibs_cpuhp_state;
+static unsigned int ibs_pmu_type; /* discovered at init */
+
+static DEFINE_PER_CPU(struct perf_event *, damon_ibs_event);
+
+/*
+ * Diagnostic counters. Incremented from NMI context, so use per-CPU
+ * counters and sum them on read.
+ */
+static DEFINE_PER_CPU(unsigned long, ibs_samples_total_pcpu);
+static DEFINE_PER_CPU(unsigned long, ibs_samples_filtered_pcpu);
+
+static unsigned long damon_ibs_sum_pcpu(unsigned long __percpu *var)
+{
+ unsigned long sum = 0;
+ int cpu;
+
+ for_each_possible_cpu(cpu)
+ sum += per_cpu(*var, cpu);
+ return sum;
+}
+
+static int samples_total_get(char *buffer, const struct kernel_param *kp)
+{
+ return sysfs_emit(buffer, "%lu\n",
+ damon_ibs_sum_pcpu(&ibs_samples_total_pcpu));
+}
+
+static int samples_filtered_get(char *buffer, const struct kernel_param *kp)
+{
+ return sysfs_emit(buffer, "%lu\n",
+ damon_ibs_sum_pcpu(&ibs_samples_filtered_pcpu));
+}
+
+static const struct kernel_param_ops samples_total_ops = {
+ .get = samples_total_get,
+};
+static const struct kernel_param_ops samples_filtered_ops = {
+ .get = samples_filtered_get,
+};
+
+module_param_cb(samples_total, &samples_total_ops, NULL, 0444);
+MODULE_PARM_DESC(samples_total, "Total IBS samples delivered (read-only)");
+module_param_cb(samples_filtered, &samples_filtered_ops, NULL, 0444);
+MODULE_PARM_DESC(samples_filtered, "IBS samples filtered out (read-only)");
+
+/**
+ * damon_ibs_overflow_handler() - IBS overflow callback.
+ *
+ * Called when an IBS Op counter overflows. The IBS perf driver fills
+ * data->phys_addr from IBSDCPHYSAD when dc_phy_addr_valid is set.
+ *
+ * Context: NMI — no sleeping, no mutex, no kmalloc.
+ */
+static void damon_ibs_overflow_handler(struct perf_event *event,
+ struct perf_sample_data *data,
+ struct pt_regs *regs)
+{
+ struct damon_access_report report;
+ unsigned long phys_addr;
+
+ if (!data)
+ return;
+
+ /*
+ * PERF_SAMPLE_PHYS_ADDR was requested in attr.sample_type, but
+ * the IBS perf driver only populates data->phys_addr when
+ * IBS_OP_DATA3.dc_phy_addr_valid is set. Skip stale-PA samples
+ * by checking the sample_flags rather than testing phys_addr
+ * for zero (which would also drop legitimate page 0).
+ */
+ if (!(data->sample_flags & PERF_SAMPLE_PHYS_ADDR)) {
+ this_cpu_inc(ibs_samples_filtered_pcpu);
+ return;
+ }
+ phys_addr = data->phys_addr;
+
+ report = (struct damon_access_report){
+ .paddr = phys_addr & PAGE_MASK,
+ .size = PAGE_SIZE,
+ .cpu = smp_processor_id(),
+ .is_write = !!(data->data_src.mem_op & PERF_MEM_OP_STORE),
+ };
+ damon_report_access(&report);
+ this_cpu_inc(ibs_samples_total_pcpu);
+}
+
+static int damon_ibs_create_event(int cpu)
+{
+ struct perf_event_attr attr = {
+ .type = ibs_pmu_type,
+ .size = sizeof(attr),
+ /* config=0: IBS perf driver uses sample_period as MaxCnt. */
+ .config = 0,
+ .sample_period = damon_ibs_max_cnt,
+ .sample_type = PERF_SAMPLE_PHYS_ADDR | PERF_SAMPLE_DATA_SRC,
+ .pinned = 1,
+ };
+ struct perf_event *event;
+
+ event = perf_event_create_kernel_counter(&attr, cpu, NULL,
+ damon_ibs_overflow_handler,
+ NULL);
+ if (IS_ERR(event))
+ return PTR_ERR(event);
+
+ /*
+ * perf_event_create_kernel_counter() returns the event already
+ * enabled; no perf_event_enable() needed here.
+ */
+ per_cpu(damon_ibs_event, cpu) = event;
+ return 0;
+}
+
+static void damon_ibs_destroy_event(int cpu)
+{
+ struct perf_event *event = per_cpu(damon_ibs_event, cpu);
+
+ if (!event)
+ return;
+
+ perf_event_disable(event);
+ perf_event_release_kernel(event);
+ per_cpu(damon_ibs_event, cpu) = NULL;
+}
+
+static int damon_ibs_cpu_online(unsigned int cpu)
+{
+ int ret = damon_ibs_create_event(cpu);
+
+ if (ret)
+ pr_warn_ratelimited(
+ "damon-ibs: failed to create perf_event on cpu %u (err %d); "
+ "this cpu will not contribute samples\n", cpu, ret);
+ return 0; /* never block CPU online */
+}
+
+static int damon_ibs_cpu_offline(unsigned int cpu)
+{
+ damon_ibs_destroy_event(cpu);
+ return 0;
+}
+
+/* Caller must hold damon_ibs_lock. */
+static int __damon_ibs_start(void)
+{
+ int ret;
+
+ if (damon_ibs_enabled)
+ return -EBUSY;
+
+ ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "damon/ibs:online",
+ damon_ibs_cpu_online, damon_ibs_cpu_offline);
+ if (ret < 0)
+ return ret;
+ damon_ibs_cpuhp_state = ret;
+
+ damon_ibs_enabled = true;
+ pr_info_once("damon-ibs: first start (max_cnt=%u, pmu_type=%u)\n",
+ damon_ibs_max_cnt, ibs_pmu_type);
+ return 0;
+}
+
+/* Caller must hold damon_ibs_lock. */
+static void __damon_ibs_stop(void)
+{
+ if (!damon_ibs_enabled)
+ return;
+
+ cpuhp_remove_state(damon_ibs_cpuhp_state);
+ damon_ibs_enabled = false;
+}
+
+static int damon_ibs_start(void)
+{
+ int ret;
+
+ mutex_lock(&damon_ibs_lock);
+ ret = __damon_ibs_start();
+ mutex_unlock(&damon_ibs_lock);
+ return ret;
+}
+
+static void damon_ibs_stop(void)
+{
+ mutex_lock(&damon_ibs_lock);
+ __damon_ibs_stop();
+ mutex_unlock(&damon_ibs_lock);
+}
+
+/**
+ * damon_ibs_set_sample_rate() - Set IBS sampling interval.
+ * @max_cnt: IBS Op MaxCnt value (ops between samples).
+ * Higher = fewer samples/sec.
+ *
+ * If IBS is already running, restart it with the new rate.
+ *
+ * Return: 0 on success; if a restart was required and failed,
+ * propagate the error so callers (e.g. the max_cnt module-param
+ * .set callback) surface it to userspace instead of silently
+ * leaving sampling stopped.
+ */
+static int damon_ibs_set_sample_rate(unsigned int max_cnt)
+{
+ int ret = 0;
+
+ mutex_lock(&damon_ibs_lock);
+ damon_ibs_max_cnt = max_cnt ? max_cnt : DAMON_IBS_DEFAULT_MAX_CNT;
+
+ if (damon_ibs_enabled) {
+ __damon_ibs_stop();
+ ret = __damon_ibs_start();
+ if (ret)
+ pr_warn("damon-ibs: restart failed: %d\n", ret);
+ }
+ mutex_unlock(&damon_ibs_lock);
+ return ret;
+}
+
+
+static void damon_ibs_init_ctx(struct damon_ctx *ctx)
+{
+ int ret;
+
+ /* IBS is the access-detection source for this ctx. */
+ ctx->sample_control.primitives_enabled.page_table = false;
+
+ ret = damon_ibs_start();
+ if (ret && ret != -EBUSY)
+ pr_warn("damon-ibs: failed to start IBS sampling: %d\n", ret);
+}
+
+/**
+ * damon_ibs_discover_pmu_type() - Discover IBS Op PMU type from sysfs.
+ *
+ * Reads /sys/bus/event_source/devices/ibs_op/type to get the PMU type
+ * identifier needed for perf_event_attr.type.
+ *
+ * TODO: replace sysfs-read with a PMU lookup API when one becomes
+ * available.
+ *
+ * Return: 0 on success, negative error code otherwise.
+ */
+static int damon_ibs_discover_pmu_type(void)
+{
+ struct file *f;
+ char buf[16];
+ loff_t pos = 0;
+ ssize_t len;
+ int ret;
+
+ f = filp_open(IBS_OP_PMU_TYPE_PATH, O_RDONLY, 0);
+ if (IS_ERR(f))
+ return PTR_ERR(f);
+
+ len = kernel_read(f, buf, sizeof(buf) - 1, &pos);
+ filp_close(f, NULL);
+ if (len <= 0)
+ return -EIO;
+
+ buf[len] = '\0';
+ ret = kstrtouint(strim(buf), 10, &ibs_pmu_type);
+ if (ret)
+ return ret;
+
+ pr_info("damon-ibs: discovered ibs_op PMU type=%u\n", ibs_pmu_type);
+ return 0;
+}
+
+static int __init damon_ibs_init(void)
+{
+ struct damon_operations ops = {
+ .id = DAMON_OPS_PADDR_IBS,
+ .owner = THIS_MODULE,
+ .init = damon_ibs_init_ctx,
+ .prepare_access_checks = damon_pa_prepare_access_checks,
+ .check_accesses = damon_pa_check_accesses,
+ .apply_probes = damon_pa_apply_probes,
+ .apply_scheme = damon_pa_apply_scheme,
+ .get_scheme_score = damon_pa_scheme_score,
+ };
+ int err;
+
+ if (!boot_cpu_has(X86_FEATURE_IBS))
+ return -ENODEV;
+
+ err = damon_ibs_discover_pmu_type();
+ if (err) {
+ pr_err("damon-ibs: failed to discover IBS PMU type: %d\n", err);
+ return err;
+ }
+
+ err = damon_register_ops(&ops);
+ if (err)
+ return err;
+
+ pr_info("damon-ibs: AMD IBS backend registered (max_cnt=%u, pmu_type=%u)\n",
+ damon_ibs_max_cnt, ibs_pmu_type);
+ return 0;
+}
+
+static void __exit damon_ibs_exit(void)
+{
+ damon_ibs_stop();
+ damon_unregister_ops(DAMON_OPS_PADDR_IBS);
+}
+
+module_init(damon_ibs_init);
+module_exit(damon_ibs_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Ravi Jonnalagadda <ravis.opensrc@gmail.com>");
+MODULE_DESCRIPTION("AMD IBS-based access sampling backend for DAMON");
--
2.43.0
^ permalink raw reply related
* Re: [RFC v3 0/3] add kconfirm
From: Julian Braha @ 2026-05-16 22:36 UTC (permalink / raw)
To: nathan, nsc
Cc: jani.nikula, akpm, gary, ljs, arnd, gregkh, masahiroy, ojeda,
corbet, qingfang.deng, yann.prono, demiobenour, ej, linux-kernel,
rust-for-linux, linux-doc, linux-kbuild
In-Reply-To: <20260516215354.449807-1-julianbraha@gmail.com>
On 5/16/26 22:53, Julian Braha wrote:
> Dependencies will need to first be downloaded from crates.io by running
> the `cargo vendor` command in scripts/kconfirm/
Ugh, sorry all. I didn't realize that the linux gitignore blocks all dot
files.
So, the scripts/kconfirm/.cargo/config.toml wasn't checked in correctly
due to this.
It should have contained the following:
```
[source.crates-io]
replace-with = "vendored-sources"
[source.vendored-sources]
directory = "vendor"
```
And Nicolas, I think this was why v2 didn't compile for you.
- Julian Braha
^ permalink raw reply
* Re: [PATCH RESEND 2] Documentation: hwmon: fix typo in heading for max31730
From: Guenter Roeck @ 2026-05-16 22:49 UTC (permalink / raw)
To: Hassan Maazu
Cc: skhan@linuxfoundation.org, corbet@lwn.net,
linux-doc@vger.kernel.org, linux-hwmon@vger.kernel.org,
linux-kernel@vger.kernel.org, Randy Dunlap
In-Reply-To: <6-vpIVJnccYzmznZMj4zfXmOKnHhtaXdeyJqyqTm3KJwLIEj3iSiWzBVxHnBhkNZHZ8E3KfHn7pYQSt3xrfQOQeN5RCJNnBVwmgyJcaw_zM=@proton.me>
On Sat, May 16, 2026 at 10:09:26PM +0000, Hassan Maazu wrote:
> Generated heading & link to driver doc for max31730 wrongly named
> max31790 under hwmon docs. This patch fixes typo so link to max31730
> is easily identifiable without confusion with max31790.
>
> Signed-off-by: Hassan Maazu <maazudev@proton.me>
> Acked-by: Randy Dunlap <rdunlap@infradead.org>
Applied.
Thanks,
Guenter
^ permalink raw reply
* Re: [PATCH v2] dcache: add fs.dentry-limit sysctl with negative-first reaper
From: Matthew Wilcox @ 2026-05-16 23:09 UTC (permalink / raw)
To: Horst Birthelmer
Cc: Miklos Szeredi, Jonathan Corbet, Shuah Khan, Alexander Viro,
Christian Brauner, Jan Kara, linux-doc, linux-kernel,
linux-fsdevel, Horst Birthelmer
In-Reply-To: <20260516-limit-dentries-cache-v2-1-c733a78e603b@ddn.com>
On Sat, May 16, 2026 at 04:52:54PM +0200, Horst Birthelmer wrote:
> There was a discussion at LSFMM about servers with too many cached
> negative dentries.
> That gave me the idea to keep the dentries in general limited
> if the system administrator needs it to.
I feel you should link to the dozens of previous attempts at this kind
of thing to show that you're aware that this has been tried before and
you're doing something meaningfully different.
^ permalink raw reply
* Re: [PATCH v2 1/2] soc: bcm2835: raspberrypi-firmware: Add voltage domain IDs
From: Guenter Roeck @ 2026-05-16 23:09 UTC (permalink / raw)
To: Shubham Chakraborty, Florian Fainelli, Jonathan Corbet
Cc: Shuah Khan, Broadcom internal kernel review list, Ray Jui,
Scott Branden, linux-hwmon, linux-doc, linux-rpi-kernel,
linux-arm-kernel, linux-kernel
In-Reply-To: <20260516191555.17978-2-chakrabortyshubham66@gmail.com>
On 5/16/26 12:15, Shubham Chakraborty wrote:
> Add firmware voltage domain identifiers for the Raspberry Pi
> mailbox property interface.
>
> These IDs are used by firmware clients to query voltage rails
> through the RPI_FIRMWARE_GET_VOLTAGE property.
>
> Signed-off-by: Shubham Chakraborty <chakrabortyshubham66@gmail.com>
> ---
> include/soc/bcm2835/raspberrypi-firmware.h | 8 ++++++++
> 1 file changed, 8 insertions(+)
>
> diff --git a/include/soc/bcm2835/raspberrypi-firmware.h b/include/soc/bcm2835/raspberrypi-firmware.h
> index e1f87fbfe554..fd2e051ce05b 100644
> --- a/include/soc/bcm2835/raspberrypi-firmware.h
> +++ b/include/soc/bcm2835/raspberrypi-firmware.h
> @@ -156,6 +156,14 @@ enum rpi_firmware_clk_id {
> RPI_FIRMWARE_NUM_CLK_ID,
> };
>
> +enum rpi_firmware_volt_id {
> + RPI_FIRMWARE_VOLT_ID_RESERVED = 0,
Is that needed ?
> + RPI_FIRMWARE_VOLT_ID_CORE = 1,
> + RPI_FIRMWARE_VOLT_ID_SDRAM_C = 2,
> + RPI_FIRMWARE_VOLT_ID_SDRAM_I = 3,
> + RPI_FIRMWARE_VOLT_ID_SDRAM_P = 4,
Regarding Sashiko's feedback: I don't know where it got the
information from, but a web search suggests that it has a point;
RPI_FIRMWARE_VOLT_ID_SDRAM_I and RPI_FIRMWARE_VOLT_ID_SDRAM_P appear
to be swapped. If that is not the case, please provide evidence.
Thanks,
Guenter
^ permalink raw reply
* Re: [PATCH] docs: fix typo in mpo-overview.rst
From: Cheesecake @ 2026-05-16 23:13 UTC (permalink / raw)
To: Jonathan Corbet, Alex Deucher, Christian König,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter, Shuah Khan
Cc: amd-gfx, dri-devel, linux-doc, linux-kernel
In-Reply-To: <87se7rmont.fsf@trenco.lwn.net>
On 2026/05/16 23:09, Jonathan Corbet wrote:
> Cheesecake <cheesecake2960@icloud.com> writes:
>
>> Replace "transparant" with "transparent"
>>
>> Signed-off-by: Cheesecake <cheesecake2960@icloud.com>
> Patches need a proper signoff with a real name, please.
>
> Thanks,
>
> jo
Thanks for pointing that out.
I accidentally sent this patch using an anonymous address/name.
I'll resend it properly later.
Thank you for your time.
^ permalink raw reply
* Re: [PATCH] docs: fix typos in design.rst
From: Cheesecake @ 2026-05-16 23:14 UTC (permalink / raw)
To: SeongJae Park
Cc: Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Jonathan Corbet, Shuah Khan,
damon, linux-mm, linux-doc, linux-kernel
In-Reply-To: <20260516170847.146524-1-sj@kernel.org>
On 2026/05/17 2:08, SeongJae Park wrote:
> Hello Cheesecake,
>
> Thank you for this patch!
>
> For the consistency, let's use 'Docs/mm/damon/design:' as the prefix of the
> subject. E.g., Docs/mm/damon/design: fix three typos
>
> On Sat, 16 May 2026 18:35:37 +0900 Cheesecake <cheesecake2960@icloud.com> wrote:
>
>> L140: "unsinged" -> "unsigned"
>> L371: "sampleing" -> "sampling"
>> L387: "multipled" -> "multiplied"
> Thank you for finding and fixing these!
>
>> Signed-off-by: Cheesecake <cheesecake2960@icloud.com>
> Is Cheesecake your real name or known identity? We don't allow anonymous
> contributions [1], and mm community prefer to use real names.
>
> [...]
>
> The file changes look good.
>
> Could you please send v2 of this patch with changed subject and the name (if
> Cheesecake is not your real name or known identity)?
>
> [1] https://docs.kernel.org/process/submitting-patches.html#developer-s-certificate-of-origin-1-1
>
>
> Thanks,
> SJ
Thanks for pointing that out.
I accidentally sent this patch using an anonymous address/name.
I'll resend it properly later.
Thank you for your time.
^ permalink raw reply
* Re: [PATCH v2 2/2] hwmon: raspberrypi: Add voltage input support
From: Guenter Roeck @ 2026-05-16 23:20 UTC (permalink / raw)
To: Shubham Chakraborty, Florian Fainelli, Jonathan Corbet
Cc: Shuah Khan, Broadcom internal kernel review list, Ray Jui,
Scott Branden, linux-hwmon, linux-doc, linux-rpi-kernel,
linux-arm-kernel, linux-kernel
In-Reply-To: <20260516191555.17978-3-chakrabortyshubham66@gmail.com>
On 5/16/26 12:15, Shubham Chakraborty wrote:
> Extend the raspberrypi-hwmon driver to expose firmware-provided
> voltage measurements through the hwmon subsystem.
>
> The driver now exports the following voltage inputs:
>
> - in0_input (core)
> - in1_input (sdram_c)
> - in2_input (sdram_i)
> - in3_input (sdram_p)
>
> Voltage values returned by firmware are converted from microvolts
> to millivolts as expected by the hwmon subsystem.
>
> Update the documentation related to it.
>
> The existing undervoltage sticky alarm handling is preserved and
> associated with the first voltage channel.
>
> Tested in -
> - Raspberry Pi 3b+ (Linux raspberrypi 6.12.75+rpt-rpi-v8 #1 SMP PREEMPT
> Debian 1:6.12.75-1+rpt1 (2026-03-11) aarch64 GNU/Linux)
>
> Signed-off-by: Shubham Chakraborty <chakrabortyshubham66@gmail.com>
> ---
> Documentation/hwmon/raspberrypi-hwmon.rst | 15 ++-
> drivers/hwmon/raspberrypi-hwmon.c | 134 +++++++++++++++++++++-
> 2 files changed, 144 insertions(+), 5 deletions(-)
>
> diff --git a/Documentation/hwmon/raspberrypi-hwmon.rst b/Documentation/hwmon/raspberrypi-hwmon.rst
> index 8038ade36490..db315184b861 100644
> --- a/Documentation/hwmon/raspberrypi-hwmon.rst
> +++ b/Documentation/hwmon/raspberrypi-hwmon.rst
> @@ -20,6 +20,17 @@ undervoltage conditions.
> Sysfs entries
> -------------
>
> -======================= ==================
> +======================= ======================================================
> +in0_input Core voltage in millivolts
> +in1_input SDRAM controller voltage in millivolts
> +in2_input SDRAM I/O voltage in millivolts
> +in3_input SDRAM PHY voltage in millivolts
> +in0_label "core"
> +in1_label "sdram_c"
> +in2_label "sdram_i"
> +in3_label "sdram_p"
> in0_lcrit_alarm Undervoltage alarm
> -======================= ==================
> +======================= ======================================================
> +
> +The voltage inputs and labels are only exposed if the firmware reports support
> +for the corresponding voltage ID.
> diff --git a/drivers/hwmon/raspberrypi-hwmon.c b/drivers/hwmon/raspberrypi-hwmon.c
> index a2938881ccd2..4f96f37116f3 100644
> --- a/drivers/hwmon/raspberrypi-hwmon.c
> +++ b/drivers/hwmon/raspberrypi-hwmon.c
> @@ -5,6 +5,7 @@
> * Based on firmware/raspberrypi.c by Noralf Trønnes
> *
> * Copyright (C) 2018 Stefan Wahren <stefan.wahren@i2se.com>
> + * Copyright (C) 2026 Shubham Chakraborty <chakrabortyshubham66@gmail.com>
> */
> #include <linux/device.h>
> #include <linux/devm-helpers.h>
> @@ -18,13 +19,26 @@
>
> #define UNDERVOLTAGE_STICKY_BIT BIT(16)
>
> +struct rpi_firmware_get_value {
> + __le32 id;
> + __le32 val;
> +} __packed;
My earlier comment is still valid: This should be defined in
the include file, and it should be query-specific, just like
struct rpi_firmware_clk_rate_request.
> +
> struct rpi_hwmon_data {
> struct device *hwmon_dev;
> struct rpi_firmware *fw;
> + u32 valid_inputs;
> u32 last_throttled;
> struct delayed_work get_values_poll_work;
> };
>
> +static const char * const rpi_hwmon_labels[] = {
> + "core",
> + "sdram_c",
> + "sdram_i",
> + "sdram_p",
> +};
> +
> static void rpi_firmware_get_throttled(struct rpi_hwmon_data *data)
> {
> u32 new_uv, old_uv, value;
> @@ -56,6 +70,23 @@ static void rpi_firmware_get_throttled(struct rpi_hwmon_data *data)
> hwmon_notify_event(data->hwmon_dev, hwmon_in, hwmon_in_lcrit_alarm, 0);
> }
>
> +static int rpi_firmware_get_voltage(struct rpi_hwmon_data *data, u32 id,
> + long *val)
> +{
> + struct rpi_firmware_get_value packet;
> + int ret;
> +
> + packet.id = cpu_to_le32(id);
> + packet.val = 0;
> + ret = rpi_firmware_property(data->fw, RPI_FIRMWARE_GET_VOLTAGE,
> + &packet, sizeof(packet));
> + if (ret)
> + return ret;
> +
> + *val = le32_to_cpu(packet.val) / 1000;
I would suggest to use DIV_ROUND_CLOSEST().
> + return 0;
> +}
> +
> static void get_values_poll(struct work_struct *work)
> {
> struct rpi_hwmon_data *data;
> @@ -77,19 +108,94 @@ static int rpi_read(struct device *dev, enum hwmon_sensor_types type,
> {
> struct rpi_hwmon_data *data = dev_get_drvdata(dev);
>
> - *val = !!(data->last_throttled & UNDERVOLTAGE_STICKY_BIT);
> + if (type == hwmon_in) {
> + switch (attr) {
> + case hwmon_in_input:
> + switch (channel) {
> + case 0:
> + return rpi_firmware_get_voltage(data,
> + RPI_FIRMWARE_VOLT_ID_CORE,
> + val);
> + case 1:
> + return rpi_firmware_get_voltage(data,
> + RPI_FIRMWARE_VOLT_ID_SDRAM_C,
> + val);
> + case 2:
> + return rpi_firmware_get_voltage(data,
> + RPI_FIRMWARE_VOLT_ID_SDRAM_I,
> + val);
> + case 3:
> + return rpi_firmware_get_voltage(data,
> + RPI_FIRMWARE_VOLT_ID_SDRAM_P,
> + val);
> + default:
> + return -EOPNOTSUPP;
With
static const int voltage_regs[] = {
RPI_FIRMWARE_VOLT_ID_CORE, RPI_FIRMWARE_VOLT_ID_SDRAM_C, RPI_FIRMWARE_VOLT_ID_SDRAM_I,
RPI_FIRMWARE_VOLT_ID_SDRAM_P };
this can be simplified to
return rpi_firmware_get_voltage(data, voltage_regs[channel];
> + }
> + case hwmon_in_lcrit_alarm:
> + if (channel == 0) {
> + *val = !!(data->last_throttled & UNDERVOLTAGE_STICKY_BIT);
> + return 0;
> + }
The channel check is not really necessary.
> + return -EOPNOTSUPP;
> + default:
> + return -EOPNOTSUPP;
> + }
> + }
> +
> + return -EOPNOTSUPP;
> +}
> +
> +static int rpi_read_string(struct device *dev, enum hwmon_sensor_types type,
> + u32 attr, int channel, const char **str)
> +{
> + if (type == hwmon_in && attr == hwmon_in_label) {
> + if (channel >= ARRAY_SIZE(rpi_hwmon_labels))
> + return -EOPNOTSUPP;
Unnecessary check.
> +
> + *str = rpi_hwmon_labels[channel];
> + return 0;
> + }
> +
> + return -EOPNOTSUPP;
> +}
> +
> +static umode_t rpi_is_visible(const void *_data, enum hwmon_sensor_types type,
> + u32 attr, int channel)
> +{
> + const struct rpi_hwmon_data *data = _data;
> +
> + if (type == hwmon_in) {
> + switch (attr) {
> + case hwmon_in_input:
> + case hwmon_in_label:
> + if (!(data->valid_inputs & BIT(channel)))
> + return 0;
> + return 0444;
> + case hwmon_in_lcrit_alarm:
> + if (channel == 0)
> + return 0444;
> + return 0;
> + default:
> + return 0;
> + }
> + }
> +
> return 0;
> }
>
> static const struct hwmon_channel_info * const rpi_info[] = {
> HWMON_CHANNEL_INFO(in,
> - HWMON_I_LCRIT_ALARM),
> + HWMON_I_INPUT | HWMON_I_LABEL | HWMON_I_LCRIT_ALARM,
> + HWMON_I_INPUT | HWMON_I_LABEL,
> + HWMON_I_INPUT | HWMON_I_LABEL,
> + HWMON_I_INPUT | HWMON_I_LABEL),
> NULL
> };
>
> static const struct hwmon_ops rpi_hwmon_ops = {
> - .visible = 0444,
> + .is_visible = rpi_is_visible,
> .read = rpi_read,
> + .read_string = rpi_read_string,
> };
>
> static const struct hwmon_chip_info rpi_chip_info = {
> @@ -101,6 +207,7 @@ static int rpi_hwmon_probe(struct platform_device *pdev)
> {
> struct device *dev = &pdev->dev;
> struct rpi_hwmon_data *data;
> + long voltage;
> int ret;
>
> data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
> @@ -110,6 +217,26 @@ static int rpi_hwmon_probe(struct platform_device *pdev)
> /* Parent driver assure that firmware is correct */
> data->fw = dev_get_drvdata(dev->parent);
>
> + ret = rpi_firmware_get_voltage(data, RPI_FIRMWARE_VOLT_ID_CORE,
> + &voltage);
> + if (!ret)
> + data->valid_inputs |= BIT(0);
> +
> + ret = rpi_firmware_get_voltage(data, RPI_FIRMWARE_VOLT_ID_SDRAM_C,
> + &voltage);
> + if (!ret)
> + data->valid_inputs |= BIT(1);
> +
> + ret = rpi_firmware_get_voltage(data, RPI_FIRMWARE_VOLT_ID_SDRAM_I,
> + &voltage);
> + if (!ret)
> + data->valid_inputs |= BIT(2);
> +
> + ret = rpi_firmware_get_voltage(data, RPI_FIRMWARE_VOLT_ID_SDRAM_P,
> + &voltage);
> + if (!ret)
> + data->valid_inputs |= BIT(3);
> +
This can be implemented in a loop, using the above voltage_regs array.
Thanks,
Guenter
> data->hwmon_dev = devm_hwmon_device_register_with_info(dev, "rpi_volt",
> data,
> &rpi_chip_info,
> @@ -159,6 +286,7 @@ static struct platform_driver rpi_hwmon_driver = {
> module_platform_driver(rpi_hwmon_driver);
>
> MODULE_AUTHOR("Stefan Wahren <wahrenst@gmx.net>");
> +MODULE_AUTHOR("Shubham Chakraborty <chakrabortyshubham66@gmail.com>");
> MODULE_DESCRIPTION("Raspberry Pi voltage sensor driver");
> MODULE_LICENSE("GPL v2");
> MODULE_ALIAS("platform:raspberrypi-hwmon");
^ permalink raw reply
* [PATCH v2] docs: fix typos in design.rst
From: Sakurai Shun @ 2026-05-17 1:02 UTC (permalink / raw)
To: SeongJae Park, Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Jonathan Corbet, Shuah Khan
Cc: Sakurai Shun, damon, linux-mm, linux-doc, linux-kernel
In-Reply-To: <20260516170847.146524-1-sj@kernel.org>
L140: "unsinged" -> "unsigned"
L371: "sampleing" -> "sampling"
L387: "multipled" -> "multiplied"
Signed-off-by: Sakurai Shun <ssh1326@icloud.com>
---
Documentation/mm/damon/design.rst | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/Documentation/mm/damon/design.rst b/Documentation/mm/damon/design.rst
index afc7d52bd..9cc70a296 100644
--- a/Documentation/mm/damon/design.rst
+++ b/Documentation/mm/damon/design.rst
@@ -140,7 +140,7 @@ as Idle page tracking does.
Address Unit
------------
-DAMON core layer uses ``unsinged long`` type for monitoring target address
+DAMON core layer uses ``unsigned long`` type for monitoring target address
ranges. In some cases, the address space for a given operations set could be
too large to be handled with the type. ARM (32-bit) with large physical
address extension is an example. For such cases, a per-operations set
@@ -371,7 +371,7 @@ with theoretical maximum ``nr_accesses``, which can be calculated as
``aggregation interval / sampling interval``.
The mechanism calculates the ratio of access events for ``aggrs`` aggregations,
-and increases or decrease the ``sampleing interval`` and ``aggregation
+and increases or decrease the ``sampling interval`` and ``aggregation
interval`` in same ratio, if the observed access ratio is lower or higher than
the target, respectively. The ratio of the intervals change is decided in
proportion to the distance between current samples ratio and the target ratio.
@@ -387,7 +387,7 @@ The tuning is turned off by default, and need to be set explicitly by the user.
As a rule of thumbs and the Parreto principle, 4% access samples ratio target
is recommended. Note that Parreto principle (80/20 rule) has applied twice.
That is, assumes 4% (20% of 20%) DAMON-observed access events ratio (source)
-to capture 64% (80% multipled by 80%) real access events (outcomes).
+to capture 64% (80% multiplied by 80%) real access events (outcomes).
To know how user-space can use this feature via :ref:`DAMON sysfs interface
<sysfs_interface>`, refer to :ref:`intervals_goal
--
2.54.0
^ permalink raw reply related
* [PATCH v5 0/4] Add MSI Claw HID Configuration Driver
From: Derek J. Clark @ 2026-05-17 1:39 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
Derek J . Clark, linux-input, linux-doc, linux-kernel
This series adds an HID Configuration driver for the MSI Claw line of
Handheld Gaming PC's. The MSI Claw HID interface provides multiple
features, such as the ability to switch between xinput, dinput, and a
desktop mode, RGB control, rumble intensity, and mapping of the rear "M"
keys. There are additional gamepad modes that are not included in this
driver as they appear to be used in assembly line testing or are
incomplete in the firmware. During my testing I found them to be unstable.
The initial version of this driver was written by Denis Benato, which
contained the initial reverse-engineering and implementation for the
gamepad mode switching. This work was later expanded by Zhouwang Huang
to include more gamepad modes and additional features. Finally, I
refactored the entire driver, fixed multiple bugs, and refined the overall
format to conform to kernel driver best practices and style guide.
Claude was used initially by Zhouwang Huang to quickly parse HID captures
during the reverse-engineering of some of the features. Since Claude had
already been used, as a test of its capabilities I had it implement the
rumble intensity attribute after I had already rewritten most of the
driver, which I then manually edited to fix some mistakes. I also used
Claude to review the driver and these patches for any mistakes and bugs.
Assisted-by: Claude:claude-sonnet-4-6
Co-developed-by: Denis Benato <denis.benato@linux.dev>
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v5:
- Swap disabled & combination mkeys_function enum values.
- Fix bug introduced in v5 where claw_buttons_store would return
-EINVAL on all valid key entries.
- Ensure mode_mutex is properly init.
- Ensure claw_remove is calling hid_hw_close and not hid_hw_stop for
all paths.
- Ensure adding "DISABLED" key to valid entries is done in the correct
patch.
- Re-enable sending an empty string to clear button mappings in
addition to setting DISABLED.
- Move adding the RGB device into cfg_setup to prevent led core
attributes from being written to prior to setup completing.
- Ensure frame_lock is properly init.
- Change variable names in RGB functions from frame and zone to f and
z respectively to fit all scoped_guard actions in 100 columns.
v4: https://lore.kernel.org/linux-input/20260516042841.500299-1-derekjohn.clark@gmail.com/
- Add msi_suspend/claw_suspend.
- Reorder claw_remove to cancel all work before removing sysfs.
- Add mutex lock for removing sysfs attributes.
- Add mutex lock for MODE command data read/write.
- Change dev_warn to dev_dbg in claw_profile_event.
- use __free with DEFINE_FREE macro for argv instead of manually
running argv_free, cleaining up scoped_guard goto.
- Fix frame_calc validity check to use >=.
- Use spinlock instead of mutex in raw_event and related attribute
_store function.
- Ensure delayed work is canceled in suspend & canceled before sysfs
attribute removal.
v3: https://lore.kernel.org/linux-input/20260515033622.2095277-1-derekjohn.clark@gmail.com/
- Add mutex for read/write if rgb frame data.
- Ensure claw_hw_output_report is properly guarded.
- Remove setting rgb_frame_count when reading rgb profiles as it always
returns garbage data.
- Ensure rgb_speed is getting drvdata from a valid lookup (not hdev).
- Use scoped_guard where necessary.
- Reoder claw_probe to ensure all mutex, completion, and variable
assignments are in place prior to setting drvdata.
- Ensure gamepad_mode is set to a valid enum value in claw_probe.
v2: https://lore.kernel.org/linux-input/20260513231445.3213501-1-derekjohn.clark@gmail.com/
- Use mutexes to guard SYNC_TO_ROM calls and pending_profile calls.
- Rename driver to hid-msi and add generic entrypoints for
probe/resume/remove that call claw specific functions in order to
future proof the driver for other MSI HID interfaces.
- Fix various bugs and formatting issues.
v1: https://lore.kernel.org/linux-input/20260510043510.442807-1-derekjohn.clark@gmail.com/
Derek J. Clark (4):
HID: hid-msi: Add MSI Claw configuration driver
HID: hid-msi: Add M-key mapping attributes
HID: hid-msi: Add RGB control interface
HID: hid-msi: Add Rumble Intensity Attributes
MAINTAINERS | 6 +
drivers/hid/Kconfig | 12 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 6 +
drivers/hid/hid-msi.c | 1728 +++++++++++++++++++++++++++++++++++++++++
5 files changed, 1753 insertions(+)
create mode 100644 drivers/hid/hid-msi.c
--
2.53.0
^ permalink raw reply
* [PATCH v5 1/4] HID: hid-msi: Add MSI Claw configuration driver
From: Derek J. Clark @ 2026-05-17 1:39 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260517013925.3120314-1-derekjohn.clark@gmail.com>
Adds configuration HID driver for the MSI Claw series of handheld PC's.
In this initial patch add the initial driver outline and attributes for
changing the gamepad mode, M-key behavior, and add a WO reset function.
Sending the SWITCH_MODE and RESET commands causes a USB disconnect in
the device. The completion will therefore never get hit and would trigger
an -EIO. To avoid showing the user an error for every write to these
attrs a bypass for the completion handling is introduced when timeout ==
0.
The initial version of this patch was written by Denis Benato, which
contained the initial reverse-engineering and implementation for the
gamepad mode switching. This work was later expanded by Zhouwang Huang
to include more gamepad modes. Finally, I refactored the drivers data
in/out flow and overall format to conform to kernel driver best
practices and style guides. Claude was used as an initial reviewer of
this patch.
Assisted-by: Claude:claude-sonnet-4-6
Co-developed-by: Denis Benato <denis.benato@linux.dev>
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v5:
- Swap disabled & combination mkeys_function enum values.
- Ensure mode_mutex is properly init.
- Ensure claw_remove is calling hid_hw_close and not hid_hw_stop for
all paths.
v4:
- Add msi_suspend/claw_suspend.
- Reorder claw_remove to cancel all work before removing sysfs.
- Add mutex lock for removing sysfs attributes.
- Add mutex lock for MODE command data read/write.
v3:
- Ensure claw_hw_output_report is properly guarded.
- Reoder claw_probe to ensure all mutex, completion, and variable
assignments are in place prior to setting drvdata.
- Ensure gamepad_mode is set to a valid enum value in claw_probe.
v2:
- Rename driver to hid-msi from hid-msi-claw.
- Rename reusable/generic functions to msi_* from claw_*, retaining
claw specific functions.
- Add generic entrypoints for probe, remove, and raw event that route
to claw specific functions.
---
MAINTAINERS | 6 +
drivers/hid/Kconfig | 12 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 5 +
drivers/hid/hid-msi.c | 630 ++++++++++++++++++++++++++++++++++++++++++
5 files changed, 654 insertions(+)
create mode 100644 drivers/hid/hid-msi.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 6f6517bf4f970..8e2de98b768f7 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -17965,6 +17965,12 @@ S: Odd Fixes
F: Documentation/devicetree/bindings/net/ieee802154/mrf24j40.txt
F: drivers/net/ieee802154/mrf24j40.c
+MSI HID DRIVER
+M: Derek J. Clark <derekjohn.clark@gmail.com>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-msi.c
+
MSI EC DRIVER
M: Nikita Kravets <teackot@gmail.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 10c12d8e65579..af146691bd481 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -492,6 +492,18 @@ config HID_GT683R
Currently the following devices are know to be supported:
- MSI GT683R
+config HID_MSI
+ tristate "MSI Claw Gamepad Support"
+ depends on USB_HID
+ select LEDS_CLASS
+ select LEDS_CLASS_MULTICOLOR
+ help
+ Support for the MSI Claw RGB and controller configuration
+
+ Say Y here to include configuration interface support for the MSI Claw Line
+ of Handheld Console Controllers. Say M here to compile this driver as a
+ module. The module will be called hid-msi.
+
config HID_KEYTOUCH
tristate "Keytouch HID devices"
help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 07dfdb6a49c59..80925a17b059c 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -92,6 +92,7 @@ obj-$(CONFIG_HID_MAYFLASH) += hid-mf.o
obj-$(CONFIG_HID_MEGAWORLD_FF) += hid-megaworld.o
obj-$(CONFIG_HID_MICROSOFT) += hid-microsoft.o
obj-$(CONFIG_HID_MONTEREY) += hid-monterey.o
+obj-$(CONFIG_HID_MSI) += hid-msi.o
obj-$(CONFIG_HID_MULTITOUCH) += hid-multitouch.o
obj-$(CONFIG_HID_NINTENDO) += hid-nintendo.o
obj-$(CONFIG_HID_NTI) += hid-nti.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 933b7943bdb50..94a9b89dc240a 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1047,7 +1047,12 @@
#define USB_DEVICE_ID_MOZA_R16_R21_2 0x0010
#define USB_VENDOR_ID_MSI 0x1770
+#define USB_VENDOR_ID_MSI_2 0x0db0
#define USB_DEVICE_ID_MSI_GT683R_LED_PANEL 0xff00
+#define USB_DEVICE_ID_MSI_CLAW_XINPUT 0x1901
+#define USB_DEVICE_ID_MSI_CLAW_DINPUT 0x1902
+#define USB_DEVICE_ID_MSI_CLAW_DESKTOP 0x1903
+#define USB_DEVICE_ID_MSI_CLAW_BIOS 0x1904
#define USB_VENDOR_ID_NATIONAL_SEMICONDUCTOR 0x0400
#define USB_DEVICE_ID_N_S_HARMONY 0xc359
diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
new file mode 100644
index 0000000000000..3f809dc70a4cc
--- /dev/null
+++ b/drivers/hid/hid-msi.c
@@ -0,0 +1,630 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HID driver for MSI Claw Handheld PC gamepads.
+ *
+ * Provides configuration support for the MSI Claw series of handheld PC
+ * gamepads. Multiple iterations of the device firmware has led to some
+ * quirks for how certain attributes are handled. The original firmware
+ * did not support remapping of the M1 (right) and M2 (left) rear paddles.
+ * Additionally, the MCU RAM address for writing configuration data has
+ * changed twice. Checks are done during probe to enumerate these variances.
+ *
+ * Copyright (c) 2026 Zhouwang Huang <honjow311@gmail.com>
+ * Copyright (c) 2026 Denis Benato <denis.benato@linux.dev>
+ * Copyright (c) 2026 Valve Corporation
+ */
+
+#include <linux/array_size.h>
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/container_of.h>
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/kobject.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pm.h>
+#include <linux/sysfs.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+#include "hid-ids.h"
+
+#define CLAW_OUTPUT_REPORT_ID 0x0f
+#define CLAW_INPUT_REPORT_ID 0x10
+
+#define CLAW_PACKET_SIZE 64
+
+#define CLAW_DINPUT_CFG_INTF_IN 0x82
+#define CLAW_XINPUT_CFG_INTF_IN 0x83
+
+enum claw_command_index {
+ CLAW_COMMAND_TYPE_READ_PROFILE = 0x04,
+ CLAW_COMMAND_TYPE_READ_PROFILE_ACK = 0x05,
+ CLAW_COMMAND_TYPE_ACK = 0x06,
+ CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA = 0x21,
+ CLAW_COMMAND_TYPE_SYNC_TO_ROM = 0x22,
+ CLAW_COMMAND_TYPE_SWITCH_MODE = 0x24,
+ CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE = 0x26,
+ CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK = 0x27,
+ CLAW_COMMAND_TYPE_RESET_DEVICE = 0x28,
+};
+
+enum claw_gamepad_mode_index {
+ CLAW_GAMEPAD_MODE_XINPUT = 0x01,
+ CLAW_GAMEPAD_MODE_DINPUT = 0x02,
+ CLAW_GAMEPAD_MODE_DESKTOP = 0x04,
+};
+
+static const char * const claw_gamepad_mode_text[] = {
+ [CLAW_GAMEPAD_MODE_XINPUT] = "xinput",
+ [CLAW_GAMEPAD_MODE_DINPUT] = "dinput",
+ [CLAW_GAMEPAD_MODE_DESKTOP] = "desktop",
+};
+
+enum claw_mkeys_function_index {
+ CLAW_MKEY_FUNCTION_MACRO,
+ CLAW_MKEY_FUNCTION_DISABLED,
+ CLAW_MKEY_FUNCTION_COMBO,
+};
+
+static const char * const claw_mkeys_function_text[] = {
+ [CLAW_MKEY_FUNCTION_MACRO] = "macro",
+ [CLAW_MKEY_FUNCTION_DISABLED] = "disabled",
+ [CLAW_MKEY_FUNCTION_COMBO] = "combination",
+};
+
+struct claw_command_report {
+ u8 report_id;
+ u8 padding[2];
+ u8 header_tail;
+ u8 cmd;
+ u8 data[59];
+} __packed;
+
+struct claw_drvdata {
+ /* MCU General Variables */
+ struct completion send_cmd_complete;
+ struct delayed_work cfg_resume;
+ struct delayed_work cfg_setup;
+ struct hid_device *hdev;
+ struct mutex mode_mutex; /* mutex for mode calls */
+ struct mutex cfg_mutex; /* mutex for synchronous data */
+ u8 ep;
+
+ /* Gamepad Variables */
+ enum claw_mkeys_function_index mkeys_function;
+ enum claw_gamepad_mode_index gamepad_mode;
+ bool gamepad_registered;
+};
+
+static int get_endpoint_address(struct hid_device *hdev)
+{
+ struct usb_host_endpoint *ep;
+ struct usb_interface *intf;
+
+ intf = to_usb_interface(hdev->dev.parent);
+ ep = intf->cur_altsetting->endpoint;
+ if (ep)
+ return ep->desc.bEndpointAddress;
+
+ return -ENODEV;
+}
+
+static int claw_gamepad_mode_event(struct claw_drvdata *drvdata,
+ struct claw_command_report *cmd_rep)
+{
+ if (cmd_rep->data[0] >= ARRAY_SIZE(claw_gamepad_mode_text) ||
+ !claw_gamepad_mode_text[cmd_rep->data[0]] ||
+ cmd_rep->data[1] >= ARRAY_SIZE(claw_mkeys_function_text))
+ return -EINVAL;
+
+ drvdata->gamepad_mode = cmd_rep->data[0];
+ drvdata->mkeys_function = cmd_rep->data[1];
+
+ return 0;
+}
+
+static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report,
+ u8 *data, int size)
+{
+ struct claw_command_report *cmd_rep;
+ int ret = 0;
+
+ if (size != CLAW_PACKET_SIZE)
+ return 0;
+
+ cmd_rep = (struct claw_command_report *)data;
+
+ if (cmd_rep->report_id != CLAW_INPUT_REPORT_ID || cmd_rep->header_tail != 0x3c)
+ return 0;
+
+ dev_dbg(&drvdata->hdev->dev, "Rx data as raw input report: [%*ph]\n",
+ CLAW_PACKET_SIZE, data);
+
+ switch (cmd_rep->cmd) {
+ case CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK:
+ ret = claw_gamepad_mode_event(drvdata, cmd_rep);
+ break;
+ case CLAW_COMMAND_TYPE_ACK:
+ break;
+ default:
+ dev_dbg(&drvdata->hdev->dev, "Unknown command: %x\n", cmd_rep->cmd);
+ return 0;
+ }
+
+ complete(&drvdata->send_cmd_complete);
+
+ return ret;
+}
+
+static int msi_raw_event(struct hid_device *hdev, struct hid_report *report,
+ u8 *data, int size)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ if (!drvdata || (drvdata->ep != CLAW_XINPUT_CFG_INTF_IN &&
+ drvdata->ep != CLAW_DINPUT_CFG_INTF_IN))
+ return 0;
+
+ return claw_raw_event(drvdata, report, data, size);
+}
+
+static int claw_hw_output_report(struct hid_device *hdev, u8 index, u8 *data,
+ size_t len, unsigned int timeout)
+{
+ unsigned char *dmabuf __free(kfree) = NULL;
+ u8 header[] = { CLAW_OUTPUT_REPORT_ID, 0, 0, 0x3c, index };
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ size_t header_size = ARRAY_SIZE(header);
+ int ret;
+
+ if (header_size + len > CLAW_PACKET_SIZE)
+ return -EINVAL;
+
+ /* We can't use a devm_alloc reusable buffer without side effects during suspend */
+ dmabuf = kzalloc(CLAW_PACKET_SIZE, GFP_KERNEL);
+ if (!dmabuf)
+ return -ENOMEM;
+
+ memcpy(dmabuf, header, header_size);
+ if (data && len)
+ memcpy(dmabuf + header_size, data, len);
+
+ guard(mutex)(&drvdata->cfg_mutex);
+ if (timeout)
+ reinit_completion(&drvdata->send_cmd_complete);
+
+ dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n",
+ CLAW_PACKET_SIZE, dmabuf);
+
+ ret = hid_hw_output_report(hdev, dmabuf, CLAW_PACKET_SIZE);
+ if (ret < 0)
+ return ret;
+
+ ret = ret == CLAW_PACKET_SIZE ? 0 : -EIO;
+ if (ret)
+ return ret;
+
+ if (timeout) {
+ ret = wait_for_completion_interruptible_timeout(&drvdata->send_cmd_complete,
+ msecs_to_jiffies(timeout));
+
+ dev_dbg(&hdev->dev, "Remaining timeout: %u\n", ret);
+ if (ret >= 0) /* preserve errors */
+ ret = ret == 0 ? -EBUSY : 0; /* timeout occurred : time remained */
+ }
+
+ return ret;
+}
+
+static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int i, ret = -EINVAL;
+ u8 data[2];
+
+ for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) {
+ if (claw_gamepad_mode_text[i] && sysfs_streq(buf, claw_gamepad_mode_text[i])) {
+ ret = i;
+ break;
+ }
+ }
+ if (ret < 0)
+ return ret;
+
+ guard(mutex)(&drvdata->mode_mutex);
+ data[0] = ret;
+ data[1] = drvdata->mkeys_function;
+
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, ARRAY_SIZE(data), 0);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t gamepad_mode_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret, i;
+
+ guard(mutex)(&drvdata->mode_mutex);
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 8);
+ if (ret)
+ return ret;
+
+ i = drvdata->gamepad_mode;
+
+ if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0')
+ return sysfs_emit(buf, "unsupported\n");
+
+ return sysfs_emit(buf, "%s\n", claw_gamepad_mode_text[i]);
+}
+static DEVICE_ATTR_RW(gamepad_mode);
+
+static ssize_t gamepad_mode_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ ssize_t count = 0;
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) {
+ if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0')
+ continue;
+ count += sysfs_emit_at(buf, count, "%s ", claw_gamepad_mode_text[i]);
+ }
+
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(gamepad_mode_index);
+
+static ssize_t mkeys_function_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int i, ret = -EINVAL;
+ u8 data[2];
+
+ for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++) {
+ if (claw_mkeys_function_text[i] && sysfs_streq(buf, claw_mkeys_function_text[i])) {
+ ret = i;
+ break;
+ }
+ }
+ if (ret < 0)
+ return ret;
+
+ guard(mutex)(&drvdata->mode_mutex);
+ data[0] = drvdata->gamepad_mode;
+ data[1] = ret;
+
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, ARRAY_SIZE(data), 0);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t mkeys_function_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret, i;
+
+ guard(mutex)(&drvdata->mode_mutex);
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 8);
+ if (ret)
+ return ret;
+
+ i = drvdata->mkeys_function;
+
+ if (i >= ARRAY_SIZE(claw_mkeys_function_text))
+ return sysfs_emit(buf, "unsupported\n");
+
+ return sysfs_emit(buf, "%s\n", claw_mkeys_function_text[i]);
+}
+static DEVICE_ATTR_RW(mkeys_function);
+
+static ssize_t mkeys_function_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int i, count = 0;
+
+ for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", claw_mkeys_function_text[i]);
+
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(mkeys_function_index);
+
+static ssize_t reset_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ bool val;
+ int ret;
+
+ ret = kstrtobool(buf, &val);
+ if (ret)
+ return ret;
+
+ if (!val)
+ return -EINVAL;
+
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_RESET_DEVICE, NULL, 0, 0);
+ if (ret)
+ return ret;
+
+ return count;
+}
+static DEVICE_ATTR_WO(reset);
+
+static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr,
+ int n)
+{
+ struct hid_device *hdev = to_hid_device(kobj_to_dev(kobj));
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ if (!drvdata) {
+ dev_warn(&hdev->dev,
+ "Failed to get drvdata from kobj. Gamepad attributes are not available.\n");
+ return 0;
+ }
+
+ return attr->mode;
+}
+
+static struct attribute *claw_gamepad_attrs[] = {
+ &dev_attr_gamepad_mode.attr,
+ &dev_attr_gamepad_mode_index.attr,
+ &dev_attr_mkeys_function.attr,
+ &dev_attr_mkeys_function_index.attr,
+ &dev_attr_reset.attr,
+ NULL,
+};
+
+static const struct attribute_group claw_gamepad_attr_group = {
+ .attrs = claw_gamepad_attrs,
+ .is_visible = claw_gamepad_attr_is_visible,
+};
+
+static void cfg_setup_fn(struct work_struct *work)
+{
+ struct delayed_work *dwork = container_of(work, struct delayed_work, work);
+ struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_setup);
+ int ret;
+
+ ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 8);
+ if (ret) {
+ dev_err(&drvdata->hdev->dev,
+ "Failed to setup device, can't read gamepad mode: %d\n", ret);
+ return;
+ }
+
+ /* Add sysfs attributes after we get the device state */
+ ret = device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group);
+ if (ret) {
+ dev_err(&drvdata->hdev->dev,
+ "Failed to setup device, can't create gamepad attrs: %d\n", ret);
+ return;
+ }
+ drvdata->gamepad_registered = true;
+
+ kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE);
+}
+
+static void cfg_resume_fn(struct work_struct *work)
+{
+ struct delayed_work *dwork = container_of(work, struct delayed_work, work);
+ struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_resume);
+ u8 data[2] = { drvdata->gamepad_mode, drvdata->mkeys_function };
+ int ret;
+
+ ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data,
+ ARRAY_SIZE(data), 0);
+ if (ret)
+ dev_err(&drvdata->hdev->dev, "Failed to set gamepad mode settings: %d\n", ret);
+}
+
+static int claw_probe(struct hid_device *hdev, u8 ep)
+{
+ struct claw_drvdata *drvdata;
+ int ret;
+
+ drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL);
+ if (!drvdata)
+ return -ENOMEM;
+
+ drvdata->gamepad_mode = CLAW_GAMEPAD_MODE_XINPUT;
+ drvdata->hdev = hdev;
+ drvdata->ep = ep;
+
+ mutex_init(&drvdata->mode_mutex);
+ mutex_init(&drvdata->cfg_mutex);
+ init_completion(&drvdata->send_cmd_complete);
+ INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn);
+ INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn);
+
+ /* For control interface: open the HID transport for sending commands. */
+ ret = hid_hw_open(hdev);
+ if (ret)
+ return ret;
+
+ hid_set_drvdata(hdev, drvdata);
+ schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500));
+
+ return 0;
+}
+
+static int msi_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+ int ret;
+ u8 ep;
+
+ if (!hid_is_usb(hdev)) {
+ ret = -ENODEV;
+ goto err_probe;
+ }
+
+ ret = hid_parse(hdev);
+ if (ret)
+ goto err_probe;
+
+ /* Set quirk to create separate input devices per HID application */
+ hdev->quirks |= HID_QUIRK_INPUT_PER_APP | HID_QUIRK_MULTI_INPUT;
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret)
+ goto err_probe;
+
+ /* For non-control interfaces (keyboard/mouse), allow userspace to grab the devices. */
+ ret = get_endpoint_address(hdev);
+ if (ret < 0)
+ goto err_stop_hw;
+
+ ep = ret;
+ if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN) {
+ ret = claw_probe(hdev, ep);
+ if (ret)
+ goto err_stop_hw;
+ }
+
+ return 0;
+
+err_stop_hw:
+ hid_hw_stop(hdev);
+err_probe:
+ return dev_err_probe(&hdev->dev, ret, "Failed to init device\n");
+}
+
+static void claw_remove(struct hid_device *hdev)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ if (!drvdata) {
+ hid_hw_close(hdev);
+ return;
+ }
+
+ cancel_delayed_work_sync(&drvdata->cfg_setup);
+ cancel_delayed_work_sync(&drvdata->cfg_resume);
+
+ guard(mutex)(&drvdata->cfg_mutex);
+ if (drvdata->gamepad_registered)
+ device_remove_group(&hdev->dev, &claw_gamepad_attr_group);
+
+ hid_hw_close(hdev);
+}
+
+static void msi_remove(struct hid_device *hdev)
+{
+ int ret;
+ u8 ep;
+
+ ret = get_endpoint_address(hdev);
+ if (ret <= 0)
+ goto hw_stop;
+
+ ep = ret;
+ if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
+ claw_remove(hdev);
+
+hw_stop:
+ hid_hw_stop(hdev);
+}
+
+static int claw_resume(struct hid_device *hdev)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ if (!drvdata)
+ return -ENODEV;
+
+ /* MCU can take up to 500ms to be ready after resume */
+ schedule_delayed_work(&drvdata->cfg_resume, msecs_to_jiffies(500));
+ return 0;
+}
+
+static int msi_resume(struct hid_device *hdev)
+{
+ int ret;
+ u8 ep;
+
+ ret = get_endpoint_address(hdev);
+ if (ret <= 0)
+ return 0;
+
+ ep = ret;
+ if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
+ return claw_resume(hdev);
+
+ return 0;
+}
+
+static int claw_suspend(struct hid_device *hdev)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ if (!drvdata)
+ return -ENODEV;
+
+ cancel_delayed_work_sync(&drvdata->cfg_setup);
+ cancel_delayed_work_sync(&drvdata->cfg_resume);
+
+ return 0;
+}
+
+static int msi_suspend(struct hid_device *hdev, pm_message_t msg)
+{
+ int ret;
+ u8 ep;
+
+ ret = get_endpoint_address(hdev);
+ if (ret <= 0)
+ return 0;
+
+ ep = ret;
+ if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
+ return claw_suspend(hdev);
+
+ return 0;
+}
+
+static const struct hid_device_id msi_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_XINPUT) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DINPUT) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DESKTOP) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_BIOS) },
+ { }
+};
+MODULE_DEVICE_TABLE(hid, msi_devices);
+
+static struct hid_driver msi_driver = {
+ .name = "hid-msi",
+ .id_table = msi_devices,
+ .raw_event = msi_raw_event,
+ .probe = msi_probe,
+ .remove = msi_remove,
+ .resume = msi_resume,
+ .suspend = pm_ptr(msi_suspend),
+};
+module_hid_driver(msi_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Denis Benato <denis.benato@linux.dev>");
+MODULE_AUTHOR("Zhouwang Huang <honjow311@gmail.com>");
+MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
+MODULE_DESCRIPTION("HID driver for MSI Claw Handheld PC gamepads");
--
2.53.0
^ permalink raw reply related
* [PATCH v5 2/4] HID: hid-msi: Add M-key mapping attributes
From: Derek J. Clark @ 2026-05-17 1:39 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260517013925.3120314-1-derekjohn.clark@gmail.com>
Adds attributes that allow for remapping the M-keys with up to 5 values
when in macro mode. There are 2 mappable buttons on the rear of the
device, M1 on the right and M2 on the left. When mapped, the events will
fire from one of three event devices: gamepad buttons will fire from the
device handled by xpad, while keyboard and mouse events will fire from
respectively typed evdevs provided by the input core. Names of each
mapping have been kept as close to the event that will fire from the evdev
as possible, with context added to the ABS_ events on the direction of the
movement.
Initial reverse-engineering and implementation of this feature was done
by Zhouwang Huang. I refactored the overall format to conform to kernel
driver best practices and style guides. Claude was used as an initial
reviewer of this patch.
Assisted-by: Claude:claude-sonnet-4-6
Co-developed-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v5:
- Ensure adding "DISABLED" key to valid entries is done in the correct
patch.
- Re-enable sending an empty string to clear button mappings in
addition to setting DISABLED.
v4:
- Change dev_warn to dev_dbg in claw_profile_event.
- use __free with DEFINE_FREE macro for argv instead of manually
running argv_free, cleaining up scoped_guard goto.
v3:
- Use scoped_guard where necessary.
v2:
- Add mutex for SYNC_TO_ROM commands to ensure every SYNC is completed
before more data is written to the MCU volatile memory.
- Add mutex for profile_pending to ensure every profile action
response is serialized to the generating command.
---
drivers/hid/hid-msi.c | 401 +++++++++++++++++++++++++++++++++++++++++-
1 file changed, 400 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
index 3f809dc70a4cc..991d5a25d3de0 100644
--- a/drivers/hid/hid-msi.c
+++ b/drivers/hid/hid-msi.c
@@ -41,6 +41,8 @@
#define CLAW_DINPUT_CFG_INTF_IN 0x82
#define CLAW_XINPUT_CFG_INTF_IN 0x83
+#define CLAW_KEYS_MAX 5
+
enum claw_command_index {
CLAW_COMMAND_TYPE_READ_PROFILE = 0x04,
CLAW_COMMAND_TYPE_READ_PROFILE_ACK = 0x05,
@@ -65,6 +67,17 @@ static const char * const claw_gamepad_mode_text[] = {
[CLAW_GAMEPAD_MODE_DESKTOP] = "desktop",
};
+enum claw_profile_ack_pending {
+ CLAW_NO_PENDING,
+ CLAW_M1_PENDING,
+ CLAW_M2_PENDING,
+};
+
+enum claw_key_index {
+ CLAW_KEY_M1,
+ CLAW_KEY_M2,
+};
+
enum claw_mkeys_function_index {
CLAW_MKEY_FUNCTION_MACRO,
CLAW_MKEY_FUNCTION_DISABLED,
@@ -77,6 +90,155 @@ static const char * const claw_mkeys_function_text[] = {
[CLAW_MKEY_FUNCTION_COMBO] = "combination",
};
+static const struct {
+ u8 code;
+ const char *name;
+} claw_button_mapping_key_map[] = {
+ /* Gamepad buttons */
+ { 0x01, "ABS_HAT0Y_UP" },
+ { 0x02, "ABS_HAT0Y_DOWN" },
+ { 0x03, "ABS_HAT0X_LEFT" },
+ { 0x04, "ABS_HAT0X_RIGHT" },
+ { 0x05, "BTN_TL" },
+ { 0x06, "BTN_TR" },
+ { 0x07, "BTN_THUMBL" },
+ { 0x08, "BTN_THUMBR" },
+ { 0x09, "BTN_SOUTH" },
+ { 0x0a, "BTN_EAST" },
+ { 0x0b, "BTN_NORTH" },
+ { 0x0c, "BTN_WEST" },
+ { 0x0d, "BTN_MODE" },
+ { 0x0e, "BTN_SELECT" },
+ { 0x0f, "BTN_START" },
+ { 0x13, "BTN_TL2"},
+ { 0x14, "BTN_TR2"},
+ { 0x15, "ABS_Y_UP"},
+ { 0x16, "ABS_Y_DOWN"},
+ { 0x17, "ABS_X_LEFT"},
+ { 0x18, "ABS_X_LEFT_RIGHT"},
+ { 0x19, "ABS_RY_UP"},
+ { 0x1a, "ABS_RY_DOWN"},
+ { 0x1b, "ABS_RX_LEFT"},
+ { 0x1c, "ABS_RX_RIGHT"},
+ /* Keyboard keys */
+ { 0x32, "KEY_ESC" },
+ { 0x33, "KEY_F1" },
+ { 0x34, "KEY_F2" },
+ { 0x35, "KEY_F3" },
+ { 0x36, "KEY_F4" },
+ { 0x37, "KEY_F5" },
+ { 0x38, "KEY_F6" },
+ { 0x39, "KEY_F7" },
+ { 0x3a, "KEY_F8" },
+ { 0x3b, "KEY_F9" },
+ { 0x3c, "KEY_F10" },
+ { 0x3d, "KEY_F11" },
+ { 0x3e, "KEY_F12" },
+ { 0x3f, "KEY_GRAVE" },
+ { 0x40, "KEY_1" },
+ { 0x41, "KEY_2" },
+ { 0x42, "KEY_3" },
+ { 0x43, "KEY_4" },
+ { 0x44, "KEY_5" },
+ { 0x45, "KEY_6" },
+ { 0x46, "KEY_7" },
+ { 0x47, "KEY_8" },
+ { 0x48, "KEY_9" },
+ { 0x49, "KEY_0" },
+ { 0x4a, "KEY_MINUS" },
+ { 0x4b, "KEY_EQUAL" },
+ { 0x4c, "KEY_BACKSPACE" },
+ { 0x4d, "KEY_TAB" },
+ { 0x4e, "KEY_Q" },
+ { 0x4f, "KEY_W" },
+ { 0x50, "KEY_E" },
+ { 0x51, "KEY_R" },
+ { 0x52, "KEY_T" },
+ { 0x53, "KEY_Y" },
+ { 0x54, "KEY_U" },
+ { 0x55, "KEY_I" },
+ { 0x56, "KEY_O" },
+ { 0x57, "KEY_P" },
+ { 0x58, "KEY_LEFTBRACE" },
+ { 0x59, "KEY_RIGHTBRACE" },
+ { 0x5a, "KEY_BACKSLASH" },
+ { 0x5b, "KEY_CAPSLOCK" },
+ { 0x5c, "KEY_A" },
+ { 0x5d, "KEY_S" },
+ { 0x5e, "KEY_D" },
+ { 0x5f, "KEY_F" },
+ { 0x60, "KEY_G" },
+ { 0x61, "KEY_H" },
+ { 0x62, "KEY_J" },
+ { 0x63, "KEY_K" },
+ { 0x64, "KEY_L" },
+ { 0x65, "KEY_SEMICOLON" },
+ { 0x66, "KEY_APOSTROPHE" },
+ { 0x67, "KEY_ENTER" },
+ { 0x68, "KEY_LEFTSHIFT" },
+ { 0x69, "KEY_Z" },
+ { 0x6a, "KEY_X" },
+ { 0x6b, "KEY_C" },
+ { 0x6c, "KEY_V" },
+ { 0x6d, "KEY_B" },
+ { 0x6e, "KEY_N" },
+ { 0x6f, "KEY_M" },
+ { 0x70, "KEY_COMMA" },
+ { 0x71, "KEY_DOT" },
+ { 0x72, "KEY_SLASH" },
+ { 0x73, "KEY_RIGHTSHIFT" },
+ { 0x74, "KEY_LEFTCTRL" },
+ { 0x75, "KEY_LEFTMETA" },
+ { 0x76, "KEY_LEFTALT" },
+ { 0x77, "KEY_SPACE" },
+ { 0x78, "KEY_RIGHTALT" },
+ { 0x79, "KEY_RIGHTCTRL" },
+ { 0x7a, "KEY_INSERT" },
+ { 0x7b, "KEY_HOME" },
+ { 0x7c, "KEY_PAGEUP" },
+ { 0x7d, "KEY_DELETE" },
+ { 0x7e, "KEY_END" },
+ { 0x7f, "KEY_PAGEDOWN" },
+ { 0x8a, "KEY_KPENTER" },
+ { 0x8b, "KEY_KP0" },
+ { 0x8c, "KEY_KP1" },
+ { 0x8d, "KEY_KP2" },
+ { 0x8e, "KEY_KP3" },
+ { 0x8f, "KEY_KP4" },
+ { 0x90, "KEY_KP5" },
+ { 0x91, "KEY_KP6" },
+ { 0x92, "KEY_KP7" },
+ { 0x93, "KEY_KP8" },
+ { 0x94, "KEY_KP9" },
+ { 0x95, "MD_PLAY" },
+ { 0x96, "MD_STOP" },
+ { 0x97, "MD_NEXT" },
+ { 0x98, "MD_PREV" },
+ { 0x99, "MD_VOL_UP" },
+ { 0x9a, "MD_VOL_DOWN" },
+ { 0x9b, "MD_VOL_MUTE" },
+ { 0x9c, "KEY_F23" },
+ /* Mouse events */
+ { 0xc8, "BTN_LEFT" },
+ { 0xc9, "BTN_MIDDLE" },
+ { 0xca, "BTN_RIGHT" },
+ { 0xcb, "BTN_SIDE" },
+ { 0xcc, "BTN_EXTRA" },
+ { 0xcd, "REL_WHEEL_UP" },
+ { 0xce, "REL_WHEEL_DOWN" },
+ { 0xff, "DISABLED" },
+};
+
+static const u16 button_mapping_addr_old[] = {
+ 0x007a, /* M1 */
+ 0x011f, /* M2 */
+};
+
+static const u16 button_mapping_addr_new[] = {
+ 0x00bb, /* M1 */
+ 0x0164, /* M2 */
+};
+
struct claw_command_report {
u8 report_id;
u8 padding[2];
@@ -87,18 +249,26 @@ struct claw_command_report {
struct claw_drvdata {
/* MCU General Variables */
+ enum claw_profile_ack_pending profile_pending;
struct completion send_cmd_complete;
struct delayed_work cfg_resume;
struct delayed_work cfg_setup;
+ struct mutex profile_mutex; /* mutex for profile_pending calls */
struct hid_device *hdev;
struct mutex mode_mutex; /* mutex for mode calls */
struct mutex cfg_mutex; /* mutex for synchronous data */
+ struct mutex rom_mutex; /* mutex for SYNC_TO_ROM calls */
+ u16 bcd_device;
u8 ep;
/* Gamepad Variables */
enum claw_mkeys_function_index mkeys_function;
enum claw_gamepad_mode_index gamepad_mode;
+ u8 m1_codes[CLAW_KEYS_MAX];
+ u8 m2_codes[CLAW_KEYS_MAX];
bool gamepad_registered;
+ const u16 *bmap_addr;
+ bool bmap_support;
};
static int get_endpoint_address(struct hid_device *hdev)
@@ -128,6 +298,30 @@ static int claw_gamepad_mode_event(struct claw_drvdata *drvdata,
return 0;
}
+static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_report *cmd_rep)
+{
+ u8 *codes;
+ int i;
+
+ switch (drvdata->profile_pending) {
+ case CLAW_M1_PENDING:
+ case CLAW_M2_PENDING:
+ codes = (drvdata->profile_pending == CLAW_M1_PENDING) ?
+ drvdata->m1_codes : drvdata->m2_codes;
+ for (i = 0; i < CLAW_KEYS_MAX; i++)
+ codes[i] = (cmd_rep->data[6 + i]);
+ break;
+ default:
+ dev_dbg(&drvdata->hdev->dev,
+ "Got profile event without changes pending from command: %x\n",
+ cmd_rep->cmd);
+ return -EINVAL;
+ }
+ drvdata->profile_pending = CLAW_NO_PENDING;
+
+ return 0;
+}
+
static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report,
u8 *data, int size)
{
@@ -149,6 +343,9 @@ static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *repor
case CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK:
ret = claw_gamepad_mode_event(drvdata, cmd_rep);
break;
+ case CLAW_COMMAND_TYPE_READ_PROFILE_ACK:
+ ret = claw_profile_event(drvdata, cmd_rep);
+ break;
case CLAW_COMMAND_TYPE_ACK:
break;
default:
@@ -373,6 +570,164 @@ static ssize_t reset_store(struct device *dev, struct device_attribute *attr,
}
static DEVICE_ATTR_WO(reset);
+static int button_mapping_name_to_code(const char *name)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) {
+ if (!strcmp(name, claw_button_mapping_key_map[i].name))
+ return claw_button_mapping_key_map[i].code;
+ }
+
+ return -EINVAL;
+}
+
+static const char *button_mapping_code_to_name(u8 code)
+{
+ int i;
+
+ if (code == 0xff)
+ return NULL;
+
+ for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) {
+ if (claw_button_mapping_key_map[i].code == code)
+ return claw_button_mapping_key_map[i].name;
+ }
+
+ return NULL;
+}
+
+DEFINE_FREE(argv, char **, if (_T) argv_free(_T))
+
+static int claw_buttons_store(struct device *dev, const char *buf, u8 mkey_idx)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ u8 data[] = { 0x01, (drvdata->bmap_addr[mkey_idx] >> 8) & 0xff,
+ drvdata->bmap_addr[mkey_idx] & 0xff, 0x07,
+ 0x04, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff };
+ char **raw_keys __free(argv) = NULL;
+ size_t len = ARRAY_SIZE(data);
+ int ret, key_count, i;
+
+ raw_keys = argv_split(GFP_KERNEL, buf, &key_count);
+ if (!raw_keys)
+ return -ENOMEM;
+
+ if (key_count > CLAW_KEYS_MAX)
+ return -EINVAL;
+
+ if (key_count == 0)
+ goto set_buttons;
+
+ for (i = 0; i < key_count; i++) {
+ ret = button_mapping_name_to_code(raw_keys[i]);
+ if (ret < 0)
+ return ret;
+
+ data[6 + i] = ret;
+ }
+
+set_buttons:
+ scoped_guard(mutex, &drvdata->rom_mutex) {
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
+ data, len, 8);
+ if (ret)
+ return ret;
+
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 8);
+ }
+
+ return ret;
+}
+
+static int claw_buttons_show(struct device *dev, char *buf, enum claw_key_index m_key)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ u8 data[] = { 0x01, (drvdata->bmap_addr[m_key] >> 8) & 0xff,
+ drvdata->bmap_addr[m_key] & 0xff, 0x07 };
+ size_t len = ARRAY_SIZE(data);
+ int i, ret, count = 0;
+ const char *name;
+ u8 *codes;
+
+ codes = (m_key == CLAW_KEY_M1) ? drvdata->m1_codes : drvdata->m2_codes;
+
+ guard(mutex)(&drvdata->profile_mutex);
+ drvdata->profile_pending = (m_key == CLAW_KEY_M1) ? CLAW_M1_PENDING : CLAW_M2_PENDING;
+
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, len, 8);
+ if (ret) {
+ drvdata->profile_pending = CLAW_NO_PENDING;
+ return ret;
+ }
+ for (i = 0; i < CLAW_KEYS_MAX; i++) {
+ name = button_mapping_code_to_name(codes[i]);
+ if (name)
+ count += sysfs_emit_at(buf, count, "%s ", name);
+ }
+
+ if (!count)
+ return sysfs_emit(buf, "(not set)\n");
+
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
+static ssize_t button_m1_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ int ret;
+
+ ret = claw_buttons_store(dev, buf, CLAW_KEY_M1);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t button_m1_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ return claw_buttons_show(dev, buf, CLAW_KEY_M1);
+}
+static DEVICE_ATTR_RW(button_m1);
+
+static ssize_t button_m2_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ int ret;
+
+ ret = claw_buttons_store(dev, buf, CLAW_KEY_M2);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t button_m2_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ return claw_buttons_show(dev, buf, CLAW_KEY_M2);
+}
+static DEVICE_ATTR_RW(button_m2);
+
+static ssize_t button_mapping_options_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int i, count = 0;
+
+ for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++)
+ count += sysfs_emit_at(buf, count, "%s ", claw_button_mapping_key_map[i].name);
+
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(button_mapping_options);
+
static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr,
int n)
{
@@ -385,10 +740,22 @@ static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribu
return 0;
}
- return attr->mode;
+ /* Always show attrs available on all firmware */
+ if (attr == &dev_attr_gamepad_mode.attr ||
+ attr == &dev_attr_gamepad_mode_index.attr ||
+ attr == &dev_attr_mkeys_function.attr ||
+ attr == &dev_attr_mkeys_function_index.attr ||
+ attr == &dev_attr_reset.attr)
+ return attr->mode;
+
+ /* Hide button mapping attrs if it isn't supported */
+ return drvdata->bmap_support ? attr->mode : 0;
}
static struct attribute *claw_gamepad_attrs[] = {
+ &dev_attr_button_m1.attr,
+ &dev_attr_button_m2.attr,
+ &dev_attr_button_mapping_options.attr,
&dev_attr_gamepad_mode.attr,
&dev_attr_gamepad_mode_index.attr,
&dev_attr_mkeys_function.attr,
@@ -440,8 +807,31 @@ static void cfg_resume_fn(struct work_struct *work)
dev_err(&drvdata->hdev->dev, "Failed to set gamepad mode settings: %d\n", ret);
}
+static void claw_features_supported(struct claw_drvdata *drvdata)
+{
+ u8 major = (drvdata->bcd_device >> 8) & 0xff;
+ u8 minor = drvdata->bcd_device & 0xff;
+
+ if (major == 0x01) {
+ drvdata->bmap_support = true;
+ if (minor >= 0x66)
+ drvdata->bmap_addr = button_mapping_addr_new;
+ else
+ drvdata->bmap_addr = button_mapping_addr_old;
+ return;
+ }
+
+ if ((major == 0x02 && minor >= 0x17) || major >= 0x03) {
+ drvdata->bmap_support = true;
+ drvdata->bmap_addr = button_mapping_addr_new;
+ return;
+ }
+}
+
static int claw_probe(struct hid_device *hdev, u8 ep)
{
+ struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+ struct usb_device *udev = interface_to_usbdev(intf);
struct claw_drvdata *drvdata;
int ret;
@@ -453,8 +843,17 @@ static int claw_probe(struct hid_device *hdev, u8 ep)
drvdata->hdev = hdev;
drvdata->ep = ep;
+ /* Determine feature level from firmware version */
+ drvdata->bcd_device = le16_to_cpu(udev->descriptor.bcdDevice);
+ claw_features_supported(drvdata);
+
+ if (!drvdata->bmap_support)
+ dev_dbg(&hdev->dev, "M-Key mapping is not supported. Update firmware to enable.\n");
+
mutex_init(&drvdata->mode_mutex);
mutex_init(&drvdata->cfg_mutex);
+ mutex_init(&drvdata->profile_mutex);
+ mutex_init(&drvdata->rom_mutex);
init_completion(&drvdata->send_cmd_complete);
INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn);
INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn);
--
2.53.0
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox