From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mail-wm1-f45.google.com (mail-wm1-f45.google.com [209.85.128.45]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 0C2F421CC44 for ; Mon, 27 Jul 2026 00:16:34 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.128.45 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1785111400; cv=none; b=pMdzpJb4jCcTkw67CyUk6qUfH/QM5xw+Hib4Nebq1fzb5rPflSJMTf6w1hF48fJfXbx3dI1i6MboWMLqA5VMRXfDnjIh36asYBNjE5LuYk5MhXaXH3hunHwgOu5Fe2wgGSZRKKtA63ap5SeFKKEoDMV2MtUsJEd/sPwHA/zswRI= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1785111400; c=relaxed/simple; bh=4+Oz360zwazfoirGT8giu2mK1dFmnnpEtxH2ch3V8pE=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=QgFWRIlRDnrfYf4XTrj/kTozJhS5re3xYzLQPvXiIXnMDvYMGS/DRVk1lGFGeoRm3AFwar8mn1T6B5wotkvxkKrwzYRaM8sg6eNmNcQXjExqwx2NV+IfwnkoHd6bJzSkw9kM50fdroCBt/W3c2eU1SHcvBzSLjLeG37EremOvSg= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=bMIg7Bc6; arc=none smtp.client-ip=209.85.128.45 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="bMIg7Bc6" Received: by mail-wm1-f45.google.com with SMTP id 5b1f17b1804b1-49557167508so21558345e9.1 for ; Sun, 26 Jul 2026 17:16:34 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1785111393; x=1785716193; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:sender:from:to:cc:subject:date :message-id:reply-to:content-type; bh=bN6ma0rV34kLGFrt0qX3ZHqBrLP/aoHUexBxcJPZ7Qw=; b=bMIg7Bc66DR4qTTXQJ2e2f71uuzyjF6/n4nNwMuM8vNjIZrl7AKhPwJChmU2wvAChM x7mzBv7HN0t1Mo1AKWXa7FUtr7pNQQzBHdQ1xwk6ZdIg9TYGGycd6zd6jG6YFwCzh4NL Xo1U+6fnjJlgHMiPMRySCeKQDlpulcgRVo1BV2olK9QyiYp0X57tFY14JI3peFOUN6i0 U2lDNG3HdoCtYMIFdscSAbsly81FSqxHyHRIRkWaX3c1ga0/dIRj3GmOVy8VB1+04FNS RnbiS/vr/5Jl3jVlUohbKKR/WJjlTF6BvgJsaaQpxxIq2eKFDQYGVdGjaBdTgMUb+Jee hpSw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1785111393; x=1785716193; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:sender:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=bN6ma0rV34kLGFrt0qX3ZHqBrLP/aoHUexBxcJPZ7Qw=; b=Xx+8QHYg+vmgaDMQvh1wXtJ7keQ3lAJXVNo8jsJfKFq4BcMIjC+WA7YerAjrGT6fRj bDQkKRpDKrDpampqBOLNKylH++zraAHJwDrISYZTpI80sqHKmd+ms20oUYh6+MK2Co0N PdKX7Ku0cgAoW2TFFBVV+zy7ji/HGandgwWXggMsy/tfqAnlZoIhY7rMP5ae7Yt9pxKu OaZW2EFdmuTgSfIoGbZozxyVgLyEqkqCvDn9ewTrZbZ7QmWKZNJNh7mwLAx4h29ZBfSc vTt+RiwLYcQTDQ74VYDs3wsbgqCupS0a2f6L0vL5XjifNDyHY+TF/A9fU9RyC+LctI/g MaLA== X-Forwarded-Encrypted: i=1; AHgh+RpFYz0lnt2eEkc9MdgM5gkQHuXdVlsR1JH1qRbwfx/mcTTcdepfOfXYqsQwuz5s11nI0XhZYtdNA9I=@vger.kernel.org X-Gm-Message-State: AOJu0YzPT7qG+gVWHQn/Xd5phfz8+op9tp1V2uiPI/RSUd0CaiDfzbuD PvMCF7hNDlRHmHl5I06loNOCoP6JOcIXUo1cnwohWD2ORJhyXax/oEAq X-Gm-Gg: AR+sD120qMeHZAzaae08aXEulE0v0uhFnDTEF1H0QysJsOAz/regTV/yNzUT8QICv1k Vuwek6hplrZxhsIyUQfNyQbaSc70o2SlnMnJB1T22961kBDeQTl7E1NjiJ21QkwKJ91VYSIk06M m/UVddum04MXkT96NydR7DX9DF51crq1b8LF62iVix0MrFSsVzEVMjGCSCPFoROz1Az6/UxW3XN w1KKrmCi9AkphJjsJKjJg7TgWa6K/yjuFibrJ95HuWftWDth21uekxYLTDfD72xPGEa1kZvm/KA jPZZFOugcNP9TicyLNL7pm/PsMfN+VzKSgJGbh7u2pGDAgQWyEihy2cRF7pUytVDVDdrqeIMWjY XW7fega8EKBQRHJGBidKTRECyLguyJ4Hg8VsfarLbFRnvnkO7Uqc3Ew5R+Bp8w51REe1kY+iYYr uxhidlVSVhyZZthp3KstoBkGhf7nppnzZHyK1qGuDHe4BZ0znldu21bkMrXd43RRj9rQ== X-Received: by 2002:a05:600c:8b52:b0:493:c2cc:aecb with SMTP id 5b1f17b1804b1-496b570fc3cmr87617415e9.38.1785111392714; Sun, 26 Jul 2026 17:16:32 -0700 (PDT) Received: from nixos-office (195-23-151-163.net.novis.pt. [195.23.151.163]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-496b485f65csm176403555e9.5.2026.07.26.17.16.31 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 26 Jul 2026 17:16:32 -0700 (PDT) Sender: Julian Braha From: Julian Braha To: nathan@kernel.org, nsc@kernel.org Cc: ojeda@kernel.org, akpm@linux-foundation.org, jani.nikula@linux.intel.com, gary@garyguo.net, gregkh@linuxfoundation.org, arnd@arndb.de, ljs@kernel.org, andrew.jones@linux.dev, masahiroy@kernel.org, corbet@lwn.net, qingfang.deng@linux.dev, demiobenour@gmail.com, ej@inai.de, linux-kernel@vger.kernel.org, rust-for-linux@vger.kernel.org, linux-doc@vger.kernel.org, linux-kbuild@vger.kernel.org, Julian Braha Subject: [PATCH 2/5] kconfig: add kconfirm Date: Mon, 27 Jul 2026 01:16:20 +0100 Message-ID: <20260727001623.2794156-3-julianbraha@gmail.com> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260727001623.2794156-1-julianbraha@gmail.com> References: <20260727001623.2794156-1-julianbraha@gmail.com> Precedence: bulk X-Mailing-List: linux-doc@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Add kconfirm into scripts/kconfig/ kconfirm is a static analysis tool with various checks for Kconfig, and intended to have zero false alarms by default, though some are expected for host compiler-related config options due to macro expansion. The 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. kconfirm runs semantic analysis on the unsimplified parse tree that the Kconfig parser provides. Config option definitions are collected into its symbol table, and checks run from there using informed heuristics. The dead link check uses the curl CLI to check the liveness of links, and is optional. Assisted-by: Claude:claude-fable-5 Signed-off-by: Julian Braha --- Makefile | 17 +- scripts/kconfig/.gitignore | 1 + scripts/kconfig/Makefile | 54 ++ scripts/kconfig/kconfig.rs | 445 ++++++++++++++ scripts/kconfig/kconfirm/.gitignore | 2 + scripts/kconfig/kconfirm/analyze.rs | 340 +++++++++++ scripts/kconfig/kconfirm/arch.rs | 53 ++ scripts/kconfig/kconfirm/checks.rs | 748 +++++++++++++++++++++++ scripts/kconfig/kconfirm/dead_links.rs | 230 +++++++ scripts/kconfig/kconfirm/kconfirm-cfg.sh | 57 ++ scripts/kconfig/kconfirm/kconfirm.rs | 278 +++++++++ scripts/kconfig/kconfirm/output.rs | 87 +++ scripts/kconfig/kconfirm/symbol_table.rs | 105 ++++ 13 files changed, 2415 insertions(+), 2 deletions(-) create mode 100644 scripts/kconfig/kconfig.rs create mode 100644 scripts/kconfig/kconfirm/.gitignore create mode 100644 scripts/kconfig/kconfirm/analyze.rs create mode 100644 scripts/kconfig/kconfirm/arch.rs create mode 100644 scripts/kconfig/kconfirm/checks.rs create mode 100644 scripts/kconfig/kconfirm/dead_links.rs create mode 100755 scripts/kconfig/kconfirm/kconfirm-cfg.sh create mode 100644 scripts/kconfig/kconfirm/kconfirm.rs create mode 100644 scripts/kconfig/kconfirm/output.rs create mode 100644 scripts/kconfig/kconfirm/symbol_table.rs diff --git a/Makefile b/Makefile index b568bfe8f3ec..e036af320486 100644 --- a/Makefile +++ b/Makefile @@ -298,7 +298,7 @@ no-dot-config-targets := $(clean-targets) \ %asm-generic kernelversion %src-pkg dt_binding_check \ dt_style_selftest \ outputmakefile rustavailable rustfmt rustfmtcheck \ - run-command + run-command kconfirm kconfirmtest 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 %/ @@ -1879,6 +1879,7 @@ help: @echo ' headerdep - Detect inclusion cycles in headers' @echo ' coccicheck - Check with Coccinelle' @echo ' kconfig-sym-check - Check for dangling Kconfig symbol references' + @echo ' kconfirm - Run static analysis on the Kconfig tree' @echo ' clang-analyzer - Check with clang static analyzer' @echo ' clang-tidy - Check with clang-tidy' @echo '' @@ -2328,7 +2329,7 @@ endif # Scripts to check various things for consistency # --------------------------------------------------------------------------- -PHONY += includecheck versioncheck coccicheck kconfig-sym-check +PHONY += includecheck versioncheck coccicheck kconfig-sym-check kconfirm kconfirmtest includecheck: find $(srctree)/* $(RCS_FIND_IGNORE) \ @@ -2346,6 +2347,18 @@ coccicheck: kconfig-sym-check: $(Q)$(PERL) $(srctree)/scripts/kconfig/kconfig-sym-check.pl $(srctree) $(KCONFIG_SYM_CHECK_EXCLUDES) +kconfirm-hostrustc = $(if $(filter 1,$(KBUILD_CLIPPY)),HOSTRUSTC=$(CLIPPY_DRIVER)) + +kconfirm: export CC_VERSION_TEXT := $(CC_VERSION_TEXT) +kconfirm: export RUSTC_VERSION_TEXT := $(RUSTC_VERSION_TEXT) +kconfirm: export PAHOLE_VERSION := $(PAHOLE_VERSION) +kconfirm: outputmakefile scripts_basic + $(Q)$(MAKE) $(build)=scripts/kconfig $(kconfirm-hostrustc) kconfirm || \ + (printf "\n kconfirm failed to build or run. It is built with the Rust\n toolchain (rustc, bindgen). See Documentation/dev-tools/kconfirm.rst\n\n" && false) + +kconfirmtest: outputmakefile scripts_basic + $(Q)$(MAKE) $(build)=scripts/kconfig $(kconfirm-hostrustc) kconfirmtest + PHONY += checkstack kernelrelease kernelversion image_name # UML needs a little special treatment here. It wants to use the host diff --git a/scripts/kconfig/.gitignore b/scripts/kconfig/.gitignore index 0b2ff775b2e3..0c2f9763029c 100644 --- a/scripts/kconfig/.gitignore +++ b/scripts/kconfig/.gitignore @@ -4,4 +4,5 @@ /[gmnq]conf-bin /[gmnq]conf-cflags /[gmnq]conf-libs +/kconfig_bindings.rs /qconf-moc.cc diff --git a/scripts/kconfig/Makefile b/scripts/kconfig/Makefile index 5baf1c44ffa2..ffdf36a21d79 100644 --- a/scripts/kconfig/Makefile +++ b/scripts/kconfig/Makefile @@ -154,6 +154,7 @@ help: @echo ' default value without prompting' @echo ' tinyconfig - Configure the tiniest possible kernel' @echo ' testconfig - Run Kconfig unit tests (requires python3 and pytest)' + @echo ' kconfirmtest - Run kconfirm tests (requires python3 and pytest)' @echo '' @echo 'Configuration topic targets:' @$(foreach f, $(all-config-fragments), \ @@ -234,3 +235,56 @@ $(obj)/%conf-cflags $(obj)/%conf-libs $(obj)/%conf-bin: $(src)/%conf-cfg.sh $(call cmd,conf_cfg) clean-files += *conf-cflags *conf-libs *conf-bin + +# kconfirm: analyzes Kconfig using the un-simplified parse tree. +hostprogs += kconfirm/kconfirm +kconfirm/kconfirm-rust := y +kconfirm/kconfirm-objs := $(common-objs) +targets += kconfig_bindings.rs + +HOSTRUSTFLAGS_kconfirm/kconfirm := \ + $(addprefix -Clink-arg=$(obj)/,$(kconfirm/kconfirm-objs)) +KCONFIG_BINDINGS := $(abspath $(obj)/kconfig_bindings.rs) +export KCONFIG_BINDINGS + +# The Rust bindings are generated from the parser's C headers. +quiet_cmd_kconfig_bindgen = BINDGEN $@ + cmd_kconfig_bindgen = \ + $(BINDGEN) $< --rust-target 1.85 \ + --no-doc-comments --no-prepend-enum-name \ + --allowlist-function 'conf_parse' \ + --allowlist-function 'conf_set_pre_finalize_callback' \ + --allowlist-function 'expr_print' \ + --allowlist-type 'expr' --allowlist-type 'menu' \ + --allowlist-type 'property' --allowlist-type 'symbol' \ + --allowlist-type 'expr_type' --allowlist-type 'menu_type' \ + --allowlist-type 'prop_type' --allowlist-type 'symbol_type' \ + -o $@ -- -I $(src) -I $(srctree)/scripts/include + +$(obj)/kconfig_bindings.rs: $(src)/lkc.h $(src)/expr.h $(src)/lkc_proto.h \ + $(srctree)/scripts/include/list_types.h FORCE + $(call if_changed,kconfig_bindgen) + +$(obj)/kconfirm/kconfirm: $(obj)/kconfig_bindings.rs \ + $(addprefix $(obj)/,$(kconfirm/kconfirm-objs)) + +# Alert the user when the Rust toolchain is missing or too old. +PHONY += kconfirm-tool-check +kconfirm-tool-check: + $(Q)$(CONFIG_SHELL) $(src)/kconfirm/kconfirm-cfg.sh + +$(obj)/kconfig_bindings.rs: | kconfirm-tool-check +$(obj)/kconfirm/kconfirm: | kconfirm-tool-check + +PHONY += kconfirm +kconfirm: $(obj)/kconfirm/kconfirm + $(Q)$< --linux-path $(abspath $(srctree)) --kconfig $(Kconfig) \ + $(KCONFIRM_ARGS) + +PHONY += kconfirmtest +kconfirmtest: $(obj)/kconfirm/kconfirm + $(Q)$(PYTHON3) -B -m pytest $(src)/kconfirm/tests \ + --kconfirm $(abspath $<) \ + -o cache_dir=$(abspath $(obj)/kconfirm/tests/.cache) \ + $(if $(findstring 1,$(KBUILD_VERBOSE)),--capture=no) +clean-files += kconfirm/tests/.cache diff --git a/scripts/kconfig/kconfig.rs b/scripts/kconfig/kconfig.rs new file mode 100644 index 000000000000..2b27f3c83a51 --- /dev/null +++ b/scripts/kconfig/kconfig.rs @@ -0,0 +1,445 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2026, Julian Braha + */ +//! Rust abstraction over the Kconfig parser and its bindings. Only covers +//! the unsimplified parse tree for now; useful for static analyzers, such as +//! kconfirm. +//! +//! The `raw` module below is generated by bindgen from the parser's C headers. +use std::{ + ffi::CStr, + fmt, + os::raw::{ + c_char, + c_int, + c_void, // + }, + panic::{ + catch_unwind, + AssertUnwindSafe, // + }, + ptr, // +}; + +#[allow( + clippy::all, + dead_code, + non_camel_case_types, + non_upper_case_globals, + unreachable_pub +)] +mod raw { + include!(env!("KCONFIG_BINDINGS")); +} + +use raw::*; + +type Expr = expr; +type Menu = menu; +type Symbol = symbol; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) struct Expression { + rendered: String, + negation: String, +} + +impl Expression { + pub(crate) fn is_negation_of(&self, other: &Self) -> bool { + self.negation == other.rendered + } + + fn constant(value: &str, negation: &str) -> Self { + Self { + rendered: value.to_string(), + negation: negation.to_string(), + } + } +} + +impl fmt::Display for Expression { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + self.rendered.fmt(formatter) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct DefaultAttribute { + pub(crate) expression: Expression, + pub(crate) r#if: Option, +} + +impl fmt::Display for DefaultAttribute { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + self.expression.fmt(formatter) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct Select { + pub(crate) symbol: String, + pub(crate) r#if: Option, +} + +impl fmt::Display for Select { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + self.symbol.fmt(formatter) + } +} + +pub(crate) type Imply = Select; + +#[derive(Clone, Debug)] +pub(crate) struct RangeBound { + pub(crate) value: String, +} + +impl fmt::Display for RangeBound { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + self.value.fmt(formatter) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct Range { + pub(crate) lower_bound: RangeBound, + pub(crate) upper_bound: RangeBound, + pub(crate) r#if: Option, +} + +impl fmt::Display for Range { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{} {}", self.lower_bound, self.upper_bound) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct Prompt { + pub(crate) r#if: Option, +} + +#[derive(Clone, Debug)] +pub(crate) enum Attribute { + Default(DefaultAttribute), + DependsOn(Expression), + Select(Select), + Imply(Imply), + Range(Range), + Help(String), + Prompt(Prompt), +} + +#[derive(Debug)] +pub(crate) struct Config { + pub(crate) symbol: String, + pub(crate) attributes: Vec, +} + +#[derive(Debug)] +pub(crate) struct KconfigMenu { + pub(crate) depends_on: Vec, + pub(crate) entries: Vec, +} + +#[derive(Debug)] +pub(crate) struct Choice { + pub(crate) options: Vec, + pub(crate) entries: Vec, +} + +#[derive(Debug)] +pub(crate) struct If { + pub(crate) condition: Expression, + pub(crate) entries: Vec, +} + +#[derive(Debug)] +pub(crate) enum Entry { + Config(Config), + Menu(KconfigMenu), + Choice(Choice), + If(If), + Comment, +} + +#[expect( + clippy::disallowed_methods, + reason = "host tools use the platform c_char ABI, not the kernel's unsigned c_char" +)] +fn string(pointer: *const c_char) -> Option { + if pointer.is_null() { + None + } else { + // SAFETY: All strings in the parse tree are NUL-terminated. + let string = unsafe { CStr::from_ptr(pointer) }; + Some(string.to_string_lossy().into_owned()) + } +} + +fn symbol_name(symbol: *const Symbol) -> String { + // SAFETY: Callers only pass non-null symbols from the parse tree. + string(unsafe { (*symbol).name }).unwrap_or_default() +} + +/// Appends one `expr_print()` fragment to the `String` that backs `data`. +/// +/// # Safety +/// +/// `data` must be the `String` pointer supplied by [`render()`], and `text` +/// must be NUL-terminated or null. +unsafe extern "C" fn append_expr_text( + data: *mut c_void, + _symbol: *mut Symbol, + text: *const c_char, +) { + // SAFETY: `render()` passes a `String` that outlives the `expr_print()` + // call. + let output = unsafe { &mut *data.cast::() }; + if let Some(text) = string(text) { + output.push_str(&text); + } +} + +/// Renders `expr` the way the frontends print it. +fn render(expr: *const Expr, prevtoken: expr_type) -> String { + let mut output = String::new(); + // SAFETY: `expr` points into the parse tree, the callback is + // synchronous, and `output` outlives the call. + unsafe { + expr_print( + expr, + Some(append_expr_text), + ptr::from_mut(&mut output).cast(), + prevtoken as c_int, + ); + } + output +} + +fn expression(expr: *const Expr) -> Option { + if expr.is_null() { + return None; + } + + let rendered = render(expr, E_NONE); + + // Render the negation of `expr` through the same printer, so that it + // can be compared with another rendered expression. + // SAFETY: `expr` is non-null, and `E_NOT` nodes keep their operand in + // `left.expr`. + let negation = if unsafe { (*expr).type_ } == E_NOT { + // SAFETY: `expr` is non-null, and `E_NOT` nodes keep their operand in + // `left.expr`. + render(unsafe { (*expr).left.expr }, E_NONE) + } else { + format!("!{}", render(expr, E_NOT)) + }; + + Some(Expression { rendered, negation }) +} + +fn dependencies(expr: *const Expr, output: &mut Vec) { + if expr.is_null() { + return; + } + + // SAFETY: `expr` points into the parse tree. + if unsafe { (*expr).type_ } == E_AND { + // SAFETY: `E_AND` nodes have an expression on both sides. + let (left, right) = unsafe { ((*expr).left.expr, (*expr).right.expr) }; + dependencies(left, output); + dependencies(right, output); + } else if let Some(expr) = expression(expr) { + output.push(expr); + } +} + +fn range_bound(symbol: *const Symbol) -> RangeBound { + RangeBound { + // SAFETY: Range expressions contain a symbol in each operand. + value: symbol_name(symbol), + } +} + +fn attributes(menu: *const Menu, symbol: *const Symbol) -> Vec { + let mut attributes = Vec::new(); + + let mut menu_dependencies = Vec::new(); + // SAFETY: `menu` points into the parse tree. + dependencies(unsafe { (*menu).dep }, &mut menu_dependencies); + attributes.extend(menu_dependencies.into_iter().map(Attribute::DependsOn)); + + // SAFETY: `symbol` points into the parse tree. + let mut property = unsafe { (*symbol).prop }; + while !property.is_null() { + // A property belongs to the menu node of the location that defined it. + // SAFETY: Property links are valid during the callback. + if ptr::eq(unsafe { (*property).menu }, menu) { + // SAFETY: Property expression pointers remain during the + // callback. + let (value, condition) = unsafe { ((*property).expr, (*property).visible.expr) }; + let condition = expression(condition); + // SAFETY: `property` points into the parse tree. + let property_type = unsafe { (*property).type_ }; + match property_type { + // The parser rewrites a `menuconfig` prompt to `P_MENU`. Menus + // and the mainmenu have no symbol for properties, so their + // `P_MENU` never ends up in a property list. + P_PROMPT | P_MENU => { + attributes.push(Attribute::Prompt(Prompt { r#if: condition })); + } + P_DEFAULT => { + if let Some(expression) = expression(value) { + attributes.push(Attribute::Default(DefaultAttribute { + expression, + r#if: condition, + })); + } + } + P_SELECT | P_IMPLY => { + // SAFETY: Select and imply values are `E_SYMBOL` + // expressions, which keep their symbol in `left.sym`. + let target = unsafe { (*value).left.sym }; + let select = Select { + // SAFETY: `target` is a non-null symbol. + symbol: symbol_name(target), + r#if: condition, + }; + if property_type == P_SELECT { + attributes.push(Attribute::Select(select)); + } else { + attributes.push(Attribute::Imply(select)); + } + } + P_RANGE => { + // SAFETY: Range values are `E_RANGE` expressions, which + // keep a symbol in each operand. + let (lower_bound, upper_bound) = + unsafe { ((*value).left.sym, (*value).right.sym) }; + attributes.push(Attribute::Range(Range { + lower_bound: range_bound(lower_bound), + upper_bound: range_bound(upper_bound), + r#if: condition, + })); + } + _ => {} + } + } + // SAFETY: `property` points into the property list. + property = unsafe { (*property).next }; + } + + // SAFETY: The help pointer is parser-owned and remains valid during the + // callback. + let help = unsafe { (*menu).help }; + if let Some(help) = string(help) { + attributes.push(Attribute::Help(help)); + } + + attributes +} + +fn menu_dependencies(menu: *const Menu) -> Vec { + let mut output = Vec::new(); + // SAFETY: `menu` points into the parse tree. + dependencies(unsafe { (*menu).dep }, &mut output); + output +} + +fn entries(parent: *const Menu) -> Vec { + let mut output = Vec::new(); + // SAFETY: `parent` points into the parse tree. + let mut menu: *const Menu = unsafe { (*parent).list }; + + while !menu.is_null() { + // SAFETY: Menu links and fields remain valid during the callback. + let symbol: *const Symbol = unsafe { (*menu).sym }; + // SAFETY: `menu` points into the parse tree. + let menu_type = unsafe { (*menu).type_ }; + let entry = match menu_type { + // The parser records `config` and `menuconfig` entries as a menu + // node carrying a symbol. Neither opens a submenu while parsing; + // `menu_finalize()` is what nests later entries underneath them, + // and it has not run yet. + M_NORMAL | M_MENU if !symbol.is_null() => Entry::Config(Config { + symbol: symbol_name(symbol), + attributes: attributes(menu, symbol), + }), + M_NORMAL => panic!("Kconfig 'config' entry without a symbol"), + M_MENU => Entry::Menu(KconfigMenu { + depends_on: menu_dependencies(menu), + entries: entries(menu), + }), + // A choice is recorded as a symbol with no name, so its `options` + // are read the same way a config's attributes are. + M_CHOICE => Entry::Choice(Choice { + options: attributes(menu, symbol), + entries: entries(menu), + }), + M_IF => Entry::If(If { + // SAFETY: `If` entries always have a dependency expression. + condition: expression(unsafe { (*menu).dep }) + .unwrap_or_else(|| Expression::constant("y", "n")), + entries: entries(menu), + }), + M_COMMENT => Entry::Comment, + value => panic!("unknown Kconfig menu type {value}"), + }; + output.push(entry); + + // SAFETY: `menu` points into the sibling list. + menu = unsafe { (*menu).next }; + } + + output +} + +struct ParseState { + entries: Option>, + panicked: bool, +} + +/// Collects the parse tree before the parser finalizes (simplifies) it. +/// +/// # Safety +/// +/// `data` must point to the [`ParseState`] supplied by [`parse_kconfig()`]. +unsafe extern "C" fn collect_entries(root: *const Menu, data: *mut c_void) { + // SAFETY: `parse_kconfig()` passes a non-null `ParseState` that outlives + // the synchronous `conf_parse()` call. + let state = unsafe { &mut *data.cast::() }; + match catch_unwind(AssertUnwindSafe(|| entries(root))) { + Ok(entries) => state.entries = Some(entries), + Err(_) => state.panicked = true, + } +} + +/// Parses the Kconfig tree rooted at `name` relative to the current directory. +/// +/// # Panics +/// +/// Panics if the parser does not invoke the traversal callback or if +/// traversing its parse tree encounters an unsupported node. +#[expect( + clippy::disallowed_methods, + reason = "host tools use the platform c_char ABI, not the kernel's unsigned c_char" +)] +pub(crate) fn parse_kconfig(name: &CStr) -> Vec { + let mut state = ParseState { + entries: None, + panicked: false, + }; + + // SAFETY: `conf_parse()` is synchronous, so `state` outlives the callback + // and its user-data pointer. + unsafe { + conf_set_pre_finalize_callback(Some(collect_entries), ptr::from_mut(&mut state).cast()); + conf_parse(name.as_ptr()); + conf_set_pre_finalize_callback(None, ptr::null_mut()); + } + assert!(!state.panicked, "failed to traverse Kconfig graph"); + state + .entries + .expect("Kconfig parser calls the traversal callback") +} diff --git a/scripts/kconfig/kconfirm/.gitignore b/scripts/kconfig/kconfirm/.gitignore new file mode 100644 index 000000000000..f21a4d460aee --- /dev/null +++ b/scripts/kconfig/kconfirm/.gitignore @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +/kconfirm diff --git a/scripts/kconfig/kconfirm/analyze.rs b/scripts/kconfig/kconfirm/analyze.rs new file mode 100644 index 000000000000..192669405a88 --- /dev/null +++ b/scripts/kconfig/kconfirm/analyze.rs @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2026, Julian Braha + */ +use crate::{ + dead_links::{ + self, + check_link, + LinkStatus, // + }, + kconfig::{ + Attribute::*, + Choice, + Config, + Entry, + Expression, + If, + Imply, + KconfigMenu, + Select, // + }, + output::{ + Finding, + Severity, // + }, + symbol_table::{ + AttributeDef, + SymbolUpdate, // + }, + AnalysisArgs, + Check, + SymbolTable, // +}; +use std::{ + collections::HashSet, // + option::Option, // +}; + +fn check_text( + unique_links: &mut HashSet, + text: &str, + args: &AnalysisArgs, + findings: &mut Vec, + symbol: Option<&str>, + arch: &str, + context: &str, +) { + if !args.is_enabled(Check::DeadLink) { + return; + } + + for link in dead_links::find_links(text) { + // Avoid checking the same link more than once. + if !unique_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.as_str() + ), + arch: arch.to_owned(), + }); + } + } +} + +#[derive(Clone)] +pub(crate) struct Context { + pub(crate) arch: String, + pub(crate) definition_condition: Vec, + pub(crate) visibility: Vec>, + pub(crate) dependencies: Vec, +} + +impl Context { + fn with_arch(arch: &str) -> Context { + Context { + arch: arch.to_owned(), + definition_condition: vec![], + visibility: vec![], + dependencies: vec![], + } + } + + 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) -> Self { + self.visibility.push(cond); + self + } + + fn with_definition(mut self, cond: Expression) -> Self { + self.definition_condition.push(cond); + self + } +} + +fn recurse_entries( + args: &AnalysisArgs, + symtab: &mut SymbolTable, + unique_links: &mut HashSet, + entries: Vec, + ctx: Context, + findings: &mut Vec, +) { + for entry in entries { + process_entry(args, symtab, unique_links, entry, ctx.clone(), findings); + } +} + +/// Traverses parsed Kconfig entries, populates `symtab`, and returns findings. +pub(crate) fn analyze( + args: &AnalysisArgs, + symtab: &mut SymbolTable, + arch: &str, + entries: Vec, +) -> Vec { + let mut findings = Vec::new(); + let mut unique_links = HashSet::new(); + + let ctx = Context::with_arch(arch); + + recurse_entries(args, symtab, &mut unique_links, entries, ctx, &mut findings); + + findings +} + +// Config here refers to a config option in the parse tree. +fn handle_config( + args: &AnalysisArgs, + symtab: &mut SymbolTable, + entry: Config, + ctx: &Context, + findings: &mut Vec, + unique_links: &mut HashSet, +) { + let config_symbol = entry.symbol; + + let mut child_ctx = ctx.child(); + + let mut dependencies = Vec::new(); + let mut kconfig_selects: Vec