* [RFC PATCH v3 20/28] Docs/mm/damon/design: document data attributes monitoring
From: SeongJae Park @ 2026-05-16 18:37 UTC (permalink / raw)
Cc: SeongJae Park, Liam R. Howlett, Andrew Morton, David Hildenbrand,
Jonathan Corbet, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, damon, linux-doc,
linux-kernel, linux-mm
In-Reply-To: <20260516183712.81393-1-sj@kernel.org>
Update DAMON design document for newly added data attributes monitoring
feature.
Signed-off-by: SeongJae Park <sj@kernel.org>
---
Documentation/mm/damon/design.rst | 37 +++++++++++++++++++++++++++++++
1 file changed, 37 insertions(+)
diff --git a/Documentation/mm/damon/design.rst b/Documentation/mm/damon/design.rst
index afc7d52bda2f7..aa08c899a3e5b 100644
--- a/Documentation/mm/damon/design.rst
+++ b/Documentation/mm/damon/design.rst
@@ -269,6 +269,43 @@ interval``, DAMON checks if the region's size and access frequency
(``nr_accesses``) has significantly changed. If so, the counter is reset to
zero. Otherwise, the counter is increased.
+Data Attributes Monitoring
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Data access pattern is only one type of data attributes. In some use cases,
+users need to know more data attributes information. For example, users may
+need to know how much of a given hot or cold memory region is backed by
+anonymous pages, or belong to a specific cgroup. For such use case, data
+attributes monitoring feature is provided.
+
+Using the feature, users can register data attributes of their interest to the
+DAMON :ref:`context <damon_design_execution_model_and_data_structures>`. The
+registration is made by specifying a probe per attribute. Each of the probe
+specifies a rule to determine if a given memory region has the related
+attribute. The rule is constructed with multiple filters. The filters work
+same to :ref:`DAMOS filters <damon_design_damos_filters>` except the supported
+filter types. Currently only ``anon`` filter type is supported for data
+attributes monitoring.
+
+If such probes are registered, DAMON executes the probes for each region's
+sampling memory when it does the access :ref:`sampling
+<damon_design_region_based_sampling>`. The number of samples that identified
+as having the data attribute (hitting the probe) per :ref:`aggregation interval
+<damon_design_monitoring>` is accounted in a per-region per-probe counter.
+Users can therefore know how much of a given DAMON region has a specific data
+attribute by reading the per-region per-probe probe hits counter after each
+aggregation interval.
+
+This is a sampling based mechanism. Hence, it is lightweight but the output
+may include some measurement errors. The output should be used with good
+understanding of statistics.
+
+Another way to do this for higher accuracy is using :ref:`DAMOS filter
+<damon_design_damos_filters>` with ``stat`` :ref:`action
+<damon_design_damos_action>` and ``sz_ops_filter_passed`` :ref:`stat
+<damon_design_damos_stat>`. This approach provides the data attributes
+information in page level. But, because it is operated in page level, the
+overhead is proportional to the size of the memory.
Dynamic Target Space Updates Handling
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--
2.47.3
^ permalink raw reply related
* [RFC PATCH v3 21/28] Docs/admin-guide/mm/damon/usage: document data attributes monitoring
From: SeongJae Park @ 2026-05-16 18:37 UTC (permalink / raw)
Cc: SeongJae Park, Liam R. Howlett, Andrew Morton, David Hildenbrand,
Jonathan Corbet, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, damon, linux-doc,
linux-kernel, linux-mm
In-Reply-To: <20260516183712.81393-1-sj@kernel.org>
Update DAMON usage document for the newly added data attributes
monitoring feature.
Signed-off-by: SeongJae Park <sj@kernel.org>
---
Documentation/admin-guide/mm/damon/usage.rst | 44 ++++++++++++++++++--
Documentation/mm/damon/design.rst | 2 +
2 files changed, 43 insertions(+), 3 deletions(-)
diff --git a/Documentation/admin-guide/mm/damon/usage.rst b/Documentation/admin-guide/mm/damon/usage.rst
index 534e1199cf091..7b6074a1666f3 100644
--- a/Documentation/admin-guide/mm/damon/usage.rst
+++ b/Documentation/admin-guide/mm/damon/usage.rst
@@ -71,6 +71,11 @@ comma (",").
│ │ │ │ │ │ intervals/sample_us,aggr_us,update_us
│ │ │ │ │ │ │ intervals_goal/access_bp,aggrs,min_sample_us,max_sample_us
│ │ │ │ │ │ nr_regions/min,max
+ │ │ │ │ │ │ :ref:`probes <damon_usage_sysfs_probes>`/nr_probes
+ │ │ │ │ │ │ │ 0/filters/nr_filters
+ │ │ │ │ │ │ │ │ 0/type,matching,allow
+ │ │ │ │ │ │ │ │ ...
+ │ │ │ │ │ │ │ ...
│ │ │ │ │ :ref:`targets <sysfs_targets>`/nr_targets
│ │ │ │ │ │ :ref:`0 <sysfs_target>`/pid_target,obsolete_target
│ │ │ │ │ │ │ :ref:`regions <sysfs_regions>`/nr_regions
@@ -95,6 +100,9 @@ comma (",").
│ │ │ │ │ │ │ :ref:`stats <sysfs_schemes_stats>`/nr_tried,sz_tried,nr_applied,sz_applied,sz_ops_filter_passed,qt_exceeds,nr_snapshots,max_nr_snapshots
│ │ │ │ │ │ │ :ref:`tried_regions <sysfs_schemes_tried_regions>`/total_bytes
│ │ │ │ │ │ │ │ 0/start,end,nr_accesses,age,sz_filter_passed
+ │ │ │ │ │ │ │ │ │ probes
+ │ │ │ │ │ │ │ │ │ │ 0/hits
+ │ │ │ │ │ │ │ │ │ │ ...
│ │ │ │ │ │ │ │ ...
│ │ │ │ │ │ ...
│ │ │ │ ...
@@ -221,8 +229,8 @@ contexts/<N>/monitoring_attrs/
Files for specifying attributes of the monitoring including required quality
and efficiency of the monitoring are in ``monitoring_attrs`` directory.
-Specifically, two directories, ``intervals`` and ``nr_regions`` exist in this
-directory.
+Specifically, three directories, ``intervals``, ``nr_regions`` and ``probes``
+exist in this directory.
Under ``intervals`` directory, three files for DAMON's sampling interval
(``sample_us``), aggregation interval (``aggr_us``), and update interval
@@ -256,6 +264,27 @@ tuning-applied current values of the two intervals can be read from the
``sample_us`` and ``aggr_us`` files after writing ``update_tuned_intervals`` to
the ``state`` file.
+.. _damon_usage_sysfs_probes:
+
+contexts/<N>/monitoring_attrs/probes/
+-------------------------------------
+
+A directory for registering :ref:`data attributes monitoring
+<damon_design_data_attrs_monitoring>` probes.
+
+In the beginning, this directory has only one file, ``nr_probes``. Writing a
+number (``N``) to the file creates the number of child directories named ``0``
+to ``N-1``. Each directory represents each monitoring probe.
+
+In each probe directory, one directory, ``filters`` exists. The directory
+contains files for installing filters for the probe, that is used to determine
+the data attribute for the probe.
+
+In the beginning, ``filters`` directory has only one file, ``nr_filters``.
+Writing a number (``N``) to the file creates the number of child directories
+named ``0`` to ``N-1``. Each directory represents each filter and works in a
+way similar to that for :ref:`DAMOS filter <sysfs_filters>`.
+
.. _sysfs_targets:
contexts/<N>/targets/
@@ -601,10 +630,19 @@ tried_regions/<N>/
------------------
In each region directory, you will find five files (``start``, ``end``,
-``nr_accesses``, ``age``, and ``sz_filter_passed``). Reading the files will
+``nr_accesses``, ``age`` and ``sz_filter_passed``). Reading the files will
show the properties of the region that corresponding DAMON-based operation
scheme ``action`` has tried to be applied.
+tried_regions/<N>/probes/
+-------------------------
+
+In each region directory, one directory (``probes``) also exists. In the
+directory, subdirectories named ``0`` to ``N-1`` exists. ``N`` is the number
+of installed probes. In each number-named directory, a file (``hits``) exist.
+Reading the file shows the number of data attributes monitoring probe-hit
+positive samples of the region.
+
Example
~~~~~~~
diff --git a/Documentation/mm/damon/design.rst b/Documentation/mm/damon/design.rst
index aa08c899a3e5b..85d668e929194 100644
--- a/Documentation/mm/damon/design.rst
+++ b/Documentation/mm/damon/design.rst
@@ -269,6 +269,8 @@ interval``, DAMON checks if the region's size and access frequency
(``nr_accesses``) has significantly changed. If so, the counter is reset to
zero. Otherwise, the counter is increased.
+.. _damon_design_data_attrs_monitoring:
+
Data Attributes Monitoring
~~~~~~~~~~~~~~~~~~~~~~~~~~
--
2.47.3
^ permalink raw reply related
* [RFC PATCH v3 27/28] Docs/mm/damon/design: update for memcg damon filter
From: SeongJae Park @ 2026-05-16 18:37 UTC (permalink / raw)
Cc: SeongJae Park, Liam R. Howlett, Andrew Morton, David Hildenbrand,
Jonathan Corbet, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, damon, linux-doc,
linux-kernel, linux-mm
In-Reply-To: <20260516183712.81393-1-sj@kernel.org>
Update DAMON design document for the newly added belonging memory cgroup
attribute monitoring feature.
Signed-off-by: SeongJae Park <sj@kernel.org>
---
Documentation/mm/damon/design.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Documentation/mm/damon/design.rst b/Documentation/mm/damon/design.rst
index 85d668e929194..8a2d68cbcefca 100644
--- a/Documentation/mm/damon/design.rst
+++ b/Documentation/mm/damon/design.rst
@@ -286,8 +286,8 @@ registration is made by specifying a probe per attribute. Each of the probe
specifies a rule to determine if a given memory region has the related
attribute. The rule is constructed with multiple filters. The filters work
same to :ref:`DAMOS filters <damon_design_damos_filters>` except the supported
-filter types. Currently only ``anon`` filter type is supported for data
-attributes monitoring.
+filter types. Currently only ``anon`` and ``memcg`` filter types are supported
+for data attributes monitoring.
If such probes are registered, DAMON executes the probes for each region's
sampling memory when it does the access :ref:`sampling
--
2.47.3
^ permalink raw reply related
* [RFC PATCH v3 28/28] Docs/admin-guide/mm/damon/usage: update for memcg damon filter
From: SeongJae Park @ 2026-05-16 18:37 UTC (permalink / raw)
Cc: SeongJae Park, Liam R. Howlett, Andrew Morton, David Hildenbrand,
Jonathan Corbet, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, damon, linux-doc,
linux-kernel, linux-mm
In-Reply-To: <20260516183712.81393-1-sj@kernel.org>
Update DAMON usage document for the newly added belonging memory cgroup
attribute monitoring feature.
Signed-off-by: SeongJae Park <sj@kernel.org>
---
Documentation/admin-guide/mm/damon/usage.rst | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/Documentation/admin-guide/mm/damon/usage.rst b/Documentation/admin-guide/mm/damon/usage.rst
index 7b6074a1666f3..b4584c0e4cd72 100644
--- a/Documentation/admin-guide/mm/damon/usage.rst
+++ b/Documentation/admin-guide/mm/damon/usage.rst
@@ -73,7 +73,7 @@ comma (",").
│ │ │ │ │ │ nr_regions/min,max
│ │ │ │ │ │ :ref:`probes <damon_usage_sysfs_probes>`/nr_probes
│ │ │ │ │ │ │ 0/filters/nr_filters
- │ │ │ │ │ │ │ │ 0/type,matching,allow
+ │ │ │ │ │ │ │ │ 0/type,matching,allow,path
│ │ │ │ │ │ │ │ ...
│ │ │ │ │ │ │ ...
│ │ │ │ │ :ref:`targets <sysfs_targets>`/nr_targets
@@ -283,7 +283,9 @@ the data attribute for the probe.
In the beginning, ``filters`` directory has only one file, ``nr_filters``.
Writing a number (``N``) to the file creates the number of child directories
named ``0`` to ``N-1``. Each directory represents each filter and works in a
-way similar to that for :ref:`DAMOS filter <sysfs_filters>`.
+way similar to that for :ref:`DAMOS filter <sysfs_filters>`. When the filter
+``type`` is ``memcg``, ``path`` file works the role of ``memcg_path`` for
+:ref:`DAMOS filter <sysfs_filters>`.
.. _sysfs_targets:
--
2.47.3
^ permalink raw reply related
* Re: [PATCH v11 3/6] iio: adc: ad4691: add triggered buffer support
From: Jonathan Cameron @ 2026-05-16 18:48 UTC (permalink / raw)
To: Radu Sabau via B4 Relay
Cc: radu.sabau, Lars-Peter Clausen, Michael Hennerich, David Lechner,
Nuno Sá, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Uwe Kleine-König, Liam Girdwood, Mark Brown,
Linus Walleij, Bartosz Golaszewski, Philipp Zabel,
Jonathan Corbet, Shuah Khan, linux-iio, devicetree, linux-kernel,
linux-pwm, linux-gpio, linux-doc
In-Reply-To: <20260515-ad4692-multichannel-sar-adc-driver-v11-3-eab27d852ac2@analog.com>
On Fri, 15 May 2026 16:31:32 +0300
Radu Sabau via B4 Relay <devnull+radu.sabau.analog.com@kernel.org> wrote:
> From: Radu Sabau <radu.sabau@analog.com>
>
> Add buffered capture support using the IIO triggered buffer framework.
>
> CNV Burst Mode: the GP pin identified by interrupt-names in the device
> tree is configured as DATA_READY output. The IRQ handler stops
> conversions and fires the IIO trigger; the trigger handler executes a
> pre-built SPI message that reads all active channels from the AVG_IN
> accumulator registers and then resets accumulator state and restarts
> conversions for the next cycle.
>
> Manual Mode: CNV is tied to SPI CS so each transfer simultaneously
> reads the previous result and starts the next conversion (pipelined
> N+1 scheme). At preenable time a pre-built, optimised SPI message of
> N+1 transfers is constructed (N channel reads plus one NOOP to drain
> the pipeline). The trigger handler executes the message in a single
> spi_sync() call and collects the results. An external trigger (e.g.
> iio-trig-hrtimer) is required to drive the trigger at the desired
> sample rate.
See below. Sashiko noticed an issue.
Note that I think the vast majority of what it came up with for this
version is garbage. Not this one though.
For manual mode you still register a trigger and attach it.
That trigger never fires...
I suspect it appears to all work because you just set the trigger
for manual mode before enabling it. However makes more sense to not
register the pointless trigger. Just a bit of code reorg probably
to fix this.
One other thing inline from the other other sashiko bit that was 'nearly'
right.
>
> Both modes share the same trigger handler and push a complete scan —
> one big-endian 16-bit (__be16) slot per active channel, densely packed
> in scan_index order, followed by a timestamp.
>
> The CNV Burst Mode sampling frequency (PWM period) is exposed as a
> buffer-level attribute via IIO_DEVICE_ATTR.
>
> Signed-off-by: Radu Sabau <radu.sabau@analog.com>
>
> +static int ad4691_setup_triggered_buffer(struct iio_dev *indio_dev,
> + struct ad4691_state *st)
> +{
> + struct device *dev = regmap_get_device(st->regmap);
> + struct iio_trigger *trig;
> + unsigned int i;
> + int irq, ret;
> +
> + indio_dev->channels = st->info->sw_info->channels;
> + indio_dev->num_channels = st->info->sw_info->num_channels;
> + indio_dev->info = st->manual_mode ? &ad4691_manual_info : &ad4691_cnv_burst_info;
> +
> + trig = devm_iio_trigger_alloc(dev, "%s-dev%d", indio_dev->name,
> + iio_device_id(indio_dev));
> + if (!trig)
> + return -ENOMEM;
> +
> + trig->ops = &ad4691_trigger_ops;
> + iio_trigger_set_drvdata(trig, st);
> +
> + ret = devm_iio_trigger_register(dev, trig);
> + if (ret)
> + return dev_err_probe(dev, ret, "IIO trigger register failed\n");
> +
> + indio_dev->trig = iio_trigger_get(trig);
One place here where I think sashiko may have a point... You register a trigger
for manual mode. What actually makes it fire as the only code that calls
iio_trigger_poll() is in the irq that isn't registered in this path.
> +
> + if (st->manual_mode)
> + return devm_iio_triggered_buffer_setup(dev, indio_dev,
> + &iio_pollfunc_store_time,
> + &ad4691_trigger_handler,
> + &ad4691_manual_buffer_setup_ops);
> +
> + /*
> + * The GP pin named in interrupt-names asserts at end-of-conversion.
> + * The IRQ handler stops conversions and fires the IIO trigger so
> + * the trigger handler can read and push the sample to the buffer.
> + * The IRQ is kept disabled until the buffer is enabled.
> + */
> + irq = -ENXIO;
> + for (i = 0; i < ARRAY_SIZE(ad4691_gp_names); i++) {
> + irq = fwnode_irq_get_byname(dev_fwnode(dev),
> + ad4691_gp_names[i]);
> + if (irq > 0 || irq == -EPROBE_DEFER)
> + break;
> + }
> + if (irq < 0)
> + return dev_err_probe(dev, irq, "failed to get GP interrupt\n");
> +
> + st->irq = irq;
> +
> + ret = ad4691_gpio_setup(st, i);
> + if (ret)
> + return ret;
> +
> + /*
> + * IRQ is kept disabled until the buffer is enabled to prevent
> + * spurious DATA_READY events before the SPI message is set up.
> + */
> + ret = devm_request_threaded_irq(dev, irq, NULL,
> + &ad4691_irq,
> + IRQF_ONESHOT | IRQF_NO_AUTOEN,
> + indio_dev->name, indio_dev);
Sashiko was moaning about something different but it made me look at this.
The irq handler her calls iio_trigger_poll but in a thread.
Either that should be a top half (so dev_request_irq() with flag to force
no threading) or should be iio_trigger_poll_nested()
I'm not sure why you weren't seeing a warning on this.
> + if (ret)
> + return ret;
> +
> + return devm_iio_triggered_buffer_setup_ext(dev, indio_dev,
> + &iio_pollfunc_store_time,
> + &ad4691_trigger_handler,
> + IIO_BUFFER_DIRECTION_IN,
> + &ad4691_cnv_burst_buffer_setup_ops,
> + ad4691_buffer_attrs);
> +}
^ permalink raw reply
* Re: [RFC PATCH v3 00/28] mm/damon: introduce data attributes monitoring
From: SeongJae Park @ 2026-05-16 18:50 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.
[...]
> Changes from RFC v2.1
> - rfc v2.1: https://lore.kernel.org/20260514140904.119781-1-sj@kernel.org
> - Rebase to mm-stable (7.1-rc3) to avoid Sashiko patch apply failure.
Still this seires is based on mm-stable (7.1-rc3) for the same reason. The
patches that based on mm-new is available at damon/next tree [1].
[1] https://origin.kernel.org/doc/html/latest/mm/damon/maintainer-profile.html#scm-trees
Thanks,
SJ
[...]
^ permalink raw reply
* Re: [PATCH v11 5/6] iio: adc: ad4691: add oversampling support
From: Jonathan Cameron @ 2026-05-16 18:55 UTC (permalink / raw)
To: Radu Sabau via B4 Relay
Cc: radu.sabau, Lars-Peter Clausen, Michael Hennerich, David Lechner,
Nuno Sá, Andy Shevchenko, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Uwe Kleine-König, Liam Girdwood, Mark Brown,
Linus Walleij, Bartosz Golaszewski, Philipp Zabel,
Jonathan Corbet, Shuah Khan, linux-iio, devicetree, linux-kernel,
linux-pwm, linux-gpio, linux-doc
In-Reply-To: <20260515-ad4692-multichannel-sar-adc-driver-v11-5-eab27d852ac2@analog.com>
On Fri, 15 May 2026 16:31:34 +0300
Radu Sabau via B4 Relay <devnull+radu.sabau.analog.com@kernel.org> wrote:
> From: Radu Sabau <radu.sabau@analog.com>
>
> Add per-channel oversampling ratio (OSR) support for CNV burst mode.
> The accumulator depth register (ACC_DEPTH_IN) is programmed with the
> selected OSR at buffer enable time and before each single-shot read.
>
> Supported OSR values: 1, 2, 4, 8, 16, 32.
>
> Introduce AD4691_MANUAL_CHANNEL() for manual mode channels, which do
> not expose the oversampling_ratio attribute since OSR is not applicable
> in that mode. A separate manual_channels array is added to
> struct ad4691_channel_info and selected at probe time.
>
> in_voltageN_sampling_frequency represents the effective output rate for
> channel N, defined as osc_freq / osr[N]. The chip has one internal
> oscillator shared by all channels; each channel independently
> accumulates osr[N] oscillator cycles before producing a result.
>
> Writing sampling_frequency computes needed_osc = freq * osr[N] and
> snaps down to the largest oscillator table entry that satisfies both
> osc <= needed_osc and osc % osr[N] == 0, guaranteeing an exact integer
> read-back. The result is stored in target_osc_freq_Hz and written to
> OSC_FREQ_REG at buffer enable and single-shot time, so sampling_frequency
> and oversampling_ratio can be set in any order.
>
> in_voltageN_sampling_frequency_available is computed dynamically from
> the channel's current OSR, listing only oscillator table entries that
> divide evenly by osr[N], expressed as effective rates. The list becomes
> sparser as OSR increases, capping at max_rate / osr[N].
>
> Writing oversampling_ratio stores the new OSR for that channel and snaps
> target_osc_freq_Hz to the largest oscillator table entry that is both
> <= old_effective_rate * new_osr and evenly divisible by new_osr. This
> preserves an integer read-back of in_voltageN_sampling_frequency after
> the OSR change while keeping the oscillator as close as possible to the
> previous effective rate.
>
> OSR defaults to 1 (no accumulation) for all channels.
>
> Signed-off-by: Radu Sabau <radu.sabau@analog.com>
Mostly to avoid others looking into it. We do indeed have some issues
in the IIO core with races around read_avail().
They've been there a long time and attempts to fix them haven't yet
made it upstream. Where possible it is better to precompute all the options
and pick a pointer rather than copying on the fly.
I think we can do that here but maybe I'm missing something.
I'm running out of energy tonight and feel like some Eurovision silliness
so I'm not going to do another full review today at least
> ---
> drivers/iio/adc/ad4691.c | 381 ++++++++++++++++++++++++++++++++++++++++++-----
> 1 file changed, 343 insertions(+), 38 deletions(-)
>
> diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
> index 25f7a6939b0f..39244e0e4a2d 100644
> --- a/drivers/iio/adc/ad4691.c
> +++ b/drivers/iio/adc/ad4691.c
>
> static int ad4691_read_avail(struct iio_dev *indio_dev,
> @@ -634,10 +802,46 @@ static int ad4691_read_avail(struct iio_dev *indio_dev,
> unsigned int start = ad4691_samp_freq_start(st->info);
>
> switch (mask) {
> - case IIO_CHAN_INFO_SAMP_FREQ:
> - *vals = &ad4691_osc_freqs_Hz[start];
> + case IIO_CHAN_INFO_SAMP_FREQ: {
> + unsigned int osr;
> + int n = 0;
> +
> + /*
> + * Hold the lock while reading osr[chan] and populating the
> + * scratch buffer: a concurrent oversampling_ratio write modifies
> + * both target_osc_freq_Hz and osr[] under the lock, so we must
> + * read osr atomically with respect to that write. The scratch
> + * buffer is per-channel, so concurrent reads on different
> + * channels do not race; concurrent reads on the same channel
> + * would compute identical values, but holding the lock avoids
> + * the formal data race.
The further issue that sashiko points out is we might rip whilst the
core is formatting this. It's actually worse than a small race as the
consumer interface might hold the pointer indefinitely. There are only
a few osr values, can we precompute the lot and make this a pick?
> + */
> + scoped_guard(mutex, &st->lock) {
> + osr = st->osr[chan->channel];
> +
> + /*
> + * Only oscillator frequencies evenly divisible by the
> + * channel's OSR yield an integer effective rate; expose
> + * those as effective rates (osc / osr) so the user works
> + * entirely in output-sample space.
> + */
> + for (unsigned int i = start;
> + i < ARRAY_SIZE(ad4691_osc_freqs_Hz); i++) {
> + if (ad4691_osc_freqs_Hz[i] % osr)
> + continue;
> + st->samp_freq_avail[chan->channel][n++] =
> + ad4691_osc_freqs_Hz[i] / osr;
> + }
> + }
> + *vals = st->samp_freq_avail[chan->channel];
> *type = IIO_VAL_INT;
> - *length = ARRAY_SIZE(ad4691_osc_freqs_Hz) - start;
> + *length = n;
> + return IIO_AVAIL_LIST;
> + }
> + case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
> + *vals = ad4691_oversampling_ratios;
> + *type = IIO_VAL_INT;
> + *length = ARRAY_SIZE(ad4691_oversampling_ratios);
> return IIO_AVAIL_LIST;
> default:
> return -EINVAL;
^ permalink raw reply
* [PATCH] landlock: Documentation wording cleanups
From: Günther Noack @ 2026-05-16 19:01 UTC (permalink / raw)
To: Mickaël Salaün
Cc: linux-doc, linux-security-module, Alejandro Colomar,
Günther Noack, Alejandro Colomar
Documentation cleanups suggested by Alejandro Colomar,
which we have also applied in the man pages.
Link: https://lore.kernel.org/all/agW4yMK6CinJGqXt@devuan/
Suggested-by: Alejandro Colomar <alx@kernel.org>
Signed-off-by: Günther Noack <gnoack3000@gmail.com>
---
include/uapi/linux/landlock.h | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index 10a346e55e95..48c12ddf1108 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -255,16 +255,16 @@ struct landlock_net_port_attr {
* :manpage:`connect(2)` as well as calls to :manpage:`sendmsg(2)` with an
* explicit recipient address.
*
- * This access right only applies to connections to UNIX server sockets which
+ * This access right applies only to connections to UNIX server sockets which
* were created outside of the newly created Landlock domain (e.g. from within
* a parent domain or from an unrestricted process). Newly created UNIX
* servers within the same Landlock domain continue to be accessible. In this
* regard, %LANDLOCK_ACCESS_FS_RESOLVE_UNIX has the same semantics as the
* ``LANDLOCK_SCOPE_*`` flags.
*
- * If a resolve attempt is denied, the operation returns an ``EACCES`` error,
- * in line with other filesystem access rights (but different to denials for
- * abstract UNIX domain sockets).
+ * If a resolution attempt is denied, the operation returns an ``EACCES``
+ * error, in line with other filesystem access rights (but different to
+ * denials for abstract UNIX domain sockets).
*
* This access right is available since the ninth version of the Landlock ABI.
*
--
2.54.0
^ permalink raw reply related
* [PATCH v2 0/2] raspberrypi: firmware and hwmon voltage support
From: Shubham Chakraborty @ 2026-05-16 19:15 UTC (permalink / raw)
To: Guenter Roeck, 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, Shubham Chakraborty
In-Reply-To: <20260516164407.25255-1-chakrabortyshubham66@gmail.com>
Changes in v2:
- Patch 1/2: no changes
- Patch 2/2:
- hide unsupported voltage sensors using .is_visible()
- replace the label switch with a string array
- update raspberrypi-hwmon documentation
Shubham Chakraborty (2):
soc: bcm2835: raspberrypi-firmware: Add voltage domain IDs
hwmon: raspberrypi: Add voltage input support
Documentation/hwmon/raspberrypi-hwmon.rst | 15 ++-
drivers/hwmon/raspberrypi-hwmon.c | 134 ++++++++++++++++++++-
include/soc/bcm2835/raspberrypi-firmware.h | 8 ++
3 files changed, 152 insertions(+), 5 deletions(-)
--
2.54.0
^ permalink raw reply
* [PATCH v2 1/2] soc: bcm2835: raspberrypi-firmware: Add voltage domain IDs
From: Shubham Chakraborty @ 2026-05-16 19:15 UTC (permalink / raw)
To: Guenter Roeck, 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, Shubham Chakraborty
In-Reply-To: <20260516191555.17978-1-chakrabortyshubham66@gmail.com>
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,
+ 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,
+};
+
/**
* struct rpi_firmware_clk_rate_request - Firmware Request for a rate
* @id: ID of the clock being queried
--
2.54.0
^ permalink raw reply related
* [PATCH v2 2/2] hwmon: raspberrypi: Add voltage input support
From: Shubham Chakraborty @ 2026-05-16 19:15 UTC (permalink / raw)
To: Guenter Roeck, 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, Shubham Chakraborty
In-Reply-To: <20260516191555.17978-1-chakrabortyshubham66@gmail.com>
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;
+
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;
+ 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;
+ }
+ case hwmon_in_lcrit_alarm:
+ if (channel == 0) {
+ *val = !!(data->last_throttled & UNDERVOLTAGE_STICKY_BIT);
+ return 0;
+ }
+ 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;
+
+ *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);
+
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");
--
2.54.0
^ permalink raw reply related
* Re: [PATCH] landlock: Documentation wording cleanups
From: Alejandro Colomar @ 2026-05-16 19:19 UTC (permalink / raw)
To: Günther Noack
Cc: Mickaël Salaün, linux-doc, linux-security-module
In-Reply-To: <20260516190112.4924-1-gnoack3000@gmail.com>
[-- Attachment #1: Type: text/plain, Size: 2294 bytes --]
Hi Günther,
> Cc: Alejandro Colomar <alx.manpages@gmail.com>
I don't use that address anymore. (The gmail account still exists, but
I'll eventually remove it.)
On 2026-05-16T21:01:12+0200, Günther Noack wrote:
> Documentation cleanups suggested by Alejandro Colomar,
> which we have also applied in the man pages.
>
> Link: https://lore.kernel.org/all/agW4yMK6CinJGqXt@devuan/
> Suggested-by: Alejandro Colomar <alx@kernel.org>
> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> ---
> include/uapi/linux/landlock.h | 8 ++++----
> 1 file changed, 4 insertions(+), 4 deletions(-)
>
> diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
> index 10a346e55e95..48c12ddf1108 100644
> --- a/include/uapi/linux/landlock.h
> +++ b/include/uapi/linux/landlock.h
> @@ -255,16 +255,16 @@ struct landlock_net_port_attr {
> * :manpage:`connect(2)` as well as calls to :manpage:`sendmsg(2)` with an
> * explicit recipient address.
> *
> - * This access right only applies to connections to UNIX server sockets which
> + * This access right applies only to connections to UNIX server sockets which
Yup. As a reminder, 'only' applies to whatever comes immediately after
it.
> * were created outside of the newly created Landlock domain (e.g. from within
> * a parent domain or from an unrestricted process). Newly created UNIX
> * servers within the same Landlock domain continue to be accessible. In this
> * regard, %LANDLOCK_ACCESS_FS_RESOLVE_UNIX has the same semantics as the
> * ``LANDLOCK_SCOPE_*`` flags.
> *
> - * If a resolve attempt is denied, the operation returns an ``EACCES`` error,
> - * in line with other filesystem access rights (but different to denials for
> - * abstract UNIX domain sockets).
> + * If a resolution attempt is denied, the operation returns an ``EACCES``
> + * error, in line with other filesystem access rights (but different to
> + * denials for abstract UNIX domain sockets).
I.e.: s/resolve/resolution/
I miss semantic newlines! :)
Have a lovely day!
Alex
> *
> * This access right is available since the ninth version of the Landlock ABI.
> *
> --
> 2.54.0
>
--
<https://www.alejandro-colomar.es>
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply
* Re: [PATCH v2 1/3] dt-bindings: iio: dac: Add AD5529R
From: David Lechner @ 2026-05-16 19:25 UTC (permalink / raw)
To: Jonathan Cameron, Janani Sunil
Cc: Lars-Peter Clausen, Michael Hennerich, Nuno Sá,
Andy Shevchenko, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Philipp Zabel, Jonathan Corbet, Shuah Khan, linux-iio, devicetree,
linux-kernel, linux-doc, Janani Sunil, rodrigo.alencar
In-Reply-To: <20260508134843.7646c4f5@jic23-huawei>
On 5/8/26 7:48 AM, Jonathan Cameron wrote:
> On Fri, 8 May 2026 13:55:47 +0200
> Janani Sunil <janani.sunil@analog.com> wrote:
>
>> Devicetree bindings for AD5529R 16 channel 12/16 bit high voltage,
>> buffered voltage output digital-to-analog converter (DAC) with an
>> integrated precision reference.
>>
>> Signed-off-by: Janani Sunil <janani.sunil@analog.com>
>> ---
...
>> + * Multiplexer for output voltage, load current sense and die temperature
>> +
>> + Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/ad5529r.pdf
>> +
>> +properties:
>> + compatible:
>> + const: adi,ad5529r
>> +
>> + reg:
>> + maxItems: 1
>> +
>> + spi-max-frequency:
>> + maximum: 50000000
>> +
>> + reset-gpios:
>> + maxItems: 1
>> + description:
>> + GPIO connected to the RESET pin. Active low. When asserted low,
>> + performs a power-on reset and initializes the device to its default state.
>> +
>> + vdd-supply:
>> + description: Digital power supply (typically 3.3V)
>> +
>> + avdd-supply:
>> + description: Analog power supply (typically 5V)
>> +
>> + hvdd-supply:
>> + description: High voltage positive supply (up to 40V for output range)
>> +
>> + hvss-supply:
>> + description: High voltage negative supply (ground or negative voltage)
>
> I don't mind doing it this way but in some similar cases where 0 is something that
> can be considered the 'default' we've made the supply optional. What was
> your reasoning for requiring it in this case?
>
> dt-bindings should be as complete as we can make them - with that in mind...
>
> There are some more interesting corners on this device the binding doesn't
> currently cover such as mux_out pin. We'd normally do that by making the
> driver potentially a client of an ADC
>
> Easier though is !alarm which smells like an interrupt.
> !clear probably a gpio. TG0-3 also GPIOs.
also optional vref-supply for external vs internal reference
>
>> +
>> +required:
>> + - compatible
>> + - reg
>> + - vdd-supply
>> + - avdd-supply
>> + - hvdd-supply
>> + - hvss-supply
>
^ permalink raw reply
* Re: [PATCH v2 2/3] iio: dac: Add AD5529R DAC driver support
From: David Lechner @ 2026-05-16 19:35 UTC (permalink / raw)
To: Janani Sunil, Lars-Peter Clausen, Michael Hennerich,
Jonathan Cameron, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Philipp Zabel, Jonathan Corbet,
Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-doc, Janani Sunil
In-Reply-To: <20260508-ad5529r-driver-v2-2-e315441685d7@analog.com>
On 5/8/26 6:55 AM, Janani Sunil wrote:
> Add support for AD5529R 16-channel, 12/16 bit Digital to Analog Converter
>
...
> +#define AD5529R_DAC_CHANNEL(chan, bits) { \
> + .type = IIO_VOLTAGE, \
> + .indexed = 1, \
> + .output = 1, \
> + .channel = (chan), \
> + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
> + BIT(IIO_CHAN_INFO_SCALE), \
> + .scan_type = { \
> + .sign = 'u', \
This field has a new name `.format`.
> + .realbits = (bits), \
> + .storagebits = 16, \
> + }, \
> +}
> +static struct regmap *ad5529r_get_regmap(struct ad5529r_state *st, unsigned int reg)
> +{
> + if (reg <= AD5529R_8BIT_REG_MAX)
> + return st->regmap_8bit;
> +
> + return st->regmap_16bit;
> +}
Another way we have done this is make custom read/write functions for the
regmap itself so that we don't have to have two regmaps.
> +
> +static int ad5529r_debugfs_reg_read(struct ad5529r_state *st, unsigned int reg,
> + unsigned int *val)
> +{
> + return regmap_read(ad5529r_get_regmap(st, reg), reg, val);
> +}
> +
> +static int ad5529r_debugfs_reg_write(struct ad5529r_state *st, unsigned int reg,
> + unsigned int val)
> +{
> + return regmap_write(ad5529r_get_regmap(st, reg), reg, val);
> +}
Would be more logical to move these closer to the struct that
references them.
(I snipped a bunch of functions here)
> +
> +static int ad5529r_reg_access(struct iio_dev *indio_dev,
> + unsigned int reg,
> + unsigned int writeval,
> + unsigned int *readval)
> +{
> + struct ad5529r_state *st = iio_priv(indio_dev);
> +
> + if (!readval)
Might as well swap these and avoid the !.
> + return ad5529r_debugfs_reg_write(st, reg, writeval);
> +
> + return ad5529r_debugfs_reg_read(st, reg, readval);
> +}
> +
^ permalink raw reply
* Re: [PATCH RESEND] Documentation: fix typo in heading for max31730
From: Krzysztof Kozlowski @ 2026-05-16 19:37 UTC (permalink / raw)
To: Guenter Roeck, Hassan Maazu
Cc: skhan@linuxfoundation.org, corbet@lwn.net,
linux-doc@vger.kernel.org, linux-hwmon@vger.kernel.org,
linux-kernel@vger.kernel.org
In-Reply-To: <ce11a8ba-8ebc-4c09-b6d0-7e98febeae6b@roeck-us.net>
On 16/05/2026 14:29, Guenter Roeck wrote:
> On Sat, May 16, 2026 at 06:00:01AM +0000, Hassan Maazu wrote:
>> Wrong device name used in heading.
>>
>
> That is not a proper commit description.
But luckily that one-liner was reviewed by Sashiko. :/
Best regards,
Krzysztof
^ permalink raw reply
* [PATCH] docs: gpu: drm-uapi: fix spelling of "unprivileged"
From: Godswill Onwusilike @ 2026-05-16 20:30 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Jonathan Corbet
Cc: Shuah Khan, dri-devel, linux-doc, linux-kernel,
Godswill Onwusilike
Correct the spelling of "unpriviledged" to "unprivileged" in DRM uAPI documentation.
Signed-off-by: Godswill Onwusilike <onwusilikegodswill@gmail.com>
---
Documentation/gpu/drm-uapi.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Documentation/gpu/drm-uapi.rst b/Documentation/gpu/drm-uapi.rst
index 579e87cb9ff7..8717744f0fec 100644
--- a/Documentation/gpu/drm-uapi.rst
+++ b/Documentation/gpu/drm-uapi.rst
@@ -568,7 +568,7 @@ ENOSPC:
EPERM/EACCES:
Returned for an operation that is valid, but needs more privileges.
E.g. root-only or much more common, DRM master-only operations return
- this when called by unpriviledged clients. There's no clear
+ this when called by unprivileged clients. There's no clear
difference between EACCES and EPERM.
ENODEV:
--
2.53.0
^ permalink raw reply related
* [PATCH] docs: gpu: todo: fix spelling of "fucntion"
From: Godswill Onwusilike @ 2026-05-16 21:01 UTC (permalink / raw)
To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter, Jonathan Corbet
Cc: Shuah Khan, dri-devel, linux-doc, linux-kernel,
Godswill Onwusilike
Correct the spelling of "fucntion" to "function" in todo.rst
documentation
Signed-off-by: Godswill Onwusilike <onwusilikegodswill@gmail.com>
---
Documentation/gpu/todo.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Documentation/gpu/todo.rst b/Documentation/gpu/todo.rst
index 841e4e986c48..7526ed31b0af 100644
--- a/Documentation/gpu/todo.rst
+++ b/Documentation/gpu/todo.rst
@@ -1001,4 +1001,4 @@ DRM driver that can run X11 and Weston.
Contact: Thomas Zimmermann <tzimmermann@suse.de>
-Level: Advanced
+Level: Advanced
\ No newline at end of file
--
2.53.0
^ permalink raw reply related
* [RFC PATCH 0/5] mm/damon: DAMOS quota controller and paddr migration walk fixes
From: Ravi Jonnalagadda @ 2026-05-16 21:03 UTC (permalink / raw)
To: sj, damon, linux-mm, linux-kernel, linux-doc
Cc: akpm, corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun,
ravis.opensrc
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Hi,
This series carries five fixes for the DAMOS quota controller and the
paddr migration walk. All five were surfaced during closed-loop tiering
testing on a heterogeneous-memory system (DRAM + CXL on a separate
NUMA node), but each fix is in code paths that benefit any caller --
not scoped to closed-loop tiering or to any specific goal metric.
Test envelope: AMD EPYC dual-socket host with CXL.mem on a separate
NUMA node, two-scheme migrate_hot PULL+PUSH setup driven by
node_eligible_mem_bp (now in linux-next)[1].
What each patch does
====================
Patch 1 - mm/damon/core: fix nr_accesses_bp underflow in damon_moving_sum
damon_moving_sum() can underflow when a region's access rate drops
to zero faster than the moving-sum window length. The internal
accumulator subtracts an outgoing sample without a lower bound,
producing a sentinel-large nr_accesses_bp that mis-classifies a
cold region as hot. Affects every DAMOS scheme, since
nr_accesses_bp is used on every access-rate update for every
region regardless of which scheme or goal metric is active.
Patch 2 - mm/damon/core: cap effective quota size to total monitored memory
The DAMOS quota tuner can compute an effective size (esz) larger
than the total monitored memory, because the tuner integrates over
cumulative deltas without bounding by the actual workload size.
Once esz exceeds total monitored memory the per-tick "remaining
quota" arithmetic stops being meaningful: any scheme can apply to
the entire monitored space and "remaining" stays positive
indefinitely. Cap esz at total monitored memory so the controller
remains within physically realisable bounds. Tuner-shape and
goal-metric agnostic.
Patch 3 - mm/damon/core: floor effective quota size at minimum region size
Symmetric to patch 2: the tuner can also compute esz < min_region_sz,
causing schemes to attempt zero-byte migrations for many ticks before
the tuner ramps esz back up. Observed under the CONSIST tuner with
a node_eligible_mem_bp goal: ftrace traced esz stuck at 1 byte for
96 seconds before the first region was tried; the first acted-on
region appeared at t=113s when esz crossed the min_region_sz
threshold. Floor esz at min_region_sz so schemes always have at
least one region's worth of quota when the tuner asks them to act.
Patch 4 - mm/damon/paddr: skip free pageblocks in migration walk
damon_pa_migrate() walks every 4KB PFN in a region. On
sparsely-populated lower-tier extents (e.g., a 549GB CXL region
with only ~8GB populated), this is ~144M PFN iterations per scheme
tick at ~40ns each = ~5.6 seconds of walk per tick. Use
pageblock-level free-page detection to skip unpopulated runs of
pages: only enter the per-page loop for pageblocks that contain at
least one allocated page. This brings the walk to
O(region_size / pageblock_size) skip-check cost plus
O(populated_pages) per-page work. On x86 pageblocks are 2MB, so
the same 549GB/8GB example becomes ~281K pageblock skip-checks
(microseconds total) plus ~2M per-page visits for the populated
pages -- ~80ms expected. Helps any migrate_hot/migrate_cold scheme
on paddr ops, regardless of what drives them.
Patch 5 - mm/damon/paddr: add time budget to migration page walk
Densely populated regions (e.g., a busy DRAM range where most
pageblocks contain at least one allocated page) can still consume
full ticks even with patch 4 applied. Add a 100ms wall-clock
budget with a ktime_get() check every 4096 pages walked
(~16MB worth). When the budget expires before reaching the end of
a region, kdamond returns control; subsequent ticks re-walk the
region from the start. Folios already on the target node are
dropped at migration time, so re-walks only re-do collection work,
not the migrate itself. Together with the per-scheme quota cap,
per-tick work is bounded and the workload converges over multiple
ticks for dense regions.
Worst-case migration walk contribution to a tick is bounded at
100ms per scheme regardless of region size or population density,
preserving kdamond's ability to service other DAMOS schemes and
user-space sysfs operations during heavy migration phases.
Testing context
===============
Hardware: AMD EPYC dual-socket, CXL.mem on a separate NUMA node.
Workload: 32GB hot working set across DRAM and CXL nodes.
DAMON config: paddr ops, two migrate_hot schemes (PULL CXL->DRAM,
PUSH DRAM->CXL) with complementary address filters,
node_eligible_mem_bp goal per scheme, temporal
quota tuner, 1s reset interval.
Each fix in this series was reproduced under the above setup, then
verified via ftrace and per-scheme stats after the fix landed.
References
==========
[1] mm/damon: add node_eligible_mem_bp goal metric
https://lore.kernel.org/damon/20260428030520.701-1-ravis.opensrc@gmail.com/
Ravi Jonnalagadda (5):
mm/damon/core: fix nr_accesses_bp underflow in damon_moving_sum
mm/damon/core: cap effective quota size to total monitored memory
mm/damon/core: floor effective quota size at minimum region size
mm/damon/paddr: skip free pageblocks in migration walk
mm/damon/paddr: add time budget to migration page walk
mm/damon/core.c | 29 ++++++++++++++++++++++++++++-
mm/damon/paddr.c | 40 +++++++++++++++++++++++++++++++++++++---
2 files changed, 65 insertions(+), 4 deletions(-)
base-commit: 0cec77cfd5314c0b3b03530abe1a4b32e991f639
--
2.43.0
^ permalink raw reply
* [RFC PATCH 1/5] mm/damon/core: fix nr_accesses_bp underflow in damon_moving_sum
From: Ravi Jonnalagadda @ 2026-05-16 21:03 UTC (permalink / raw)
To: sj, damon, linux-mm, linux-kernel, linux-doc
Cc: akpm, corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun,
ravis.opensrc
In-Reply-To: <20260516210357.2247-1-ravis.opensrc@gmail.com>
Guard against unsigned integer underflow when nomvsum/len_window
exceeds mvsum. When that subtraction wraps, the moving sum returns a
near-ULONG_MAX value and corrupts nr_accesses_bp.
If subtrahend > mvsum, return new_value: this clamps the moving-sum
estimate to the current observation rather than wrapping.
Signed-off-by: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
---
mm/damon/core.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/mm/damon/core.c b/mm/damon/core.c
index 3a8725e400c6b..9975f3d9ebfe9 100644
--- a/mm/damon/core.c
+++ b/mm/damon/core.c
@@ -3449,7 +3449,11 @@ int damon_set_region_system_rams_default(struct damon_target *t,
static unsigned int damon_moving_sum(unsigned int mvsum, unsigned int nomvsum,
unsigned int len_window, unsigned int new_value)
{
- return mvsum - nomvsum / len_window + new_value;
+ unsigned int subtrahend = nomvsum / len_window;
+
+ if (subtrahend > mvsum)
+ return new_value;
+ return mvsum - subtrahend + new_value;
}
/**
--
2.43.0
^ permalink raw reply related
* [RFC PATCH 2/5] mm/damon/core: cap effective quota size to total monitored memory
From: Ravi Jonnalagadda @ 2026-05-16 21:03 UTC (permalink / raw)
To: sj, damon, linux-mm, linux-kernel, linux-doc
Cc: akpm, corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun,
ravis.opensrc
In-Reply-To: <20260516210357.2247-1-ravis.opensrc@gmail.com>
The DAMOS quota goal tuner can compute an effective size (esz) larger
than the total monitored memory because it integrates over cumulative
deltas without bounding by the actual workload size. Once esz exceeds
total monitored memory, the per-tick "remaining quota" arithmetic
stops being meaningful: any scheme can apply to the entire monitored
space and "remaining" stays positive indefinitely.
Cap esz to the total size of all currently monitored regions as a
final bound after all other quota calculations. Add
damon_ctx_total_monitored_sz() helper that sums region sizes across
all targets.
The helper runs only inside damos_set_effective_quota(), which is
called at most once per quota reset_interval (default 1s) per scheme,
not per kdamond tick. Walk cost is O(nr_regions) at that frequency
and is dominated by the enclosing tuner work.
This bound is tuner-shape and goal-metric agnostic: it constrains the
quota controller to physically realisable values regardless of which
tuner or goal metric drives it.
Signed-off-by: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
---
mm/damon/core.c | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/mm/damon/core.c b/mm/damon/core.c
index 9975f3d9ebfe9..fd1db234ca304 100644
--- a/mm/damon/core.c
+++ b/mm/damon/core.c
@@ -2614,6 +2614,19 @@ static void damos_goal_tune_esz_bp_temporal(struct damon_ctx *c,
quota->esz_bp = ULONG_MAX;
}
+/* Sum of all monitored region sizes across all targets in @ctx. */
+static unsigned long damon_ctx_total_monitored_sz(struct damon_ctx *ctx)
+{
+ struct damon_target *t;
+ struct damon_region *r;
+ unsigned long total = 0;
+
+ damon_for_each_target(t, ctx)
+ damon_for_each_region(r, t)
+ total += damon_sz_region(r);
+ return total;
+}
+
/*
* Called only if quota->ms, or quota->sz are set, or quota->goals is not empty
*/
@@ -2621,6 +2634,7 @@ static void damos_set_effective_quota(struct damon_ctx *ctx, struct damos *s)
{
struct damos_quota *quota = &s->quota;
unsigned long throughput;
+ unsigned long total_sz;
unsigned long esz = ULONG_MAX;
if (!quota->ms && list_empty("a->goals)) {
@@ -2649,6 +2663,11 @@ static void damos_set_effective_quota(struct damon_ctx *ctx, struct damos *s)
if (quota->sz && quota->sz < esz)
esz = quota->sz;
+ /* Safety cap: never migrate more than total monitored memory */
+ total_sz = damon_ctx_total_monitored_sz(ctx);
+ if (total_sz && esz > total_sz)
+ esz = total_sz;
+
quota->esz = esz;
}
--
2.43.0
^ permalink raw reply related
* [RFC PATCH 3/5] mm/damon/core: floor effective quota size at minimum region size
From: Ravi Jonnalagadda @ 2026-05-16 21:03 UTC (permalink / raw)
To: sj, damon, linux-mm, linux-kernel, linux-doc
Cc: akpm, corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun,
ravis.opensrc
In-Reply-To: <20260516210357.2247-1-ravis.opensrc@gmail.com>
The CONSIST quota goal tuner initializes esz_bp to 0, producing an
effective quota size (esz) of 1 byte on the first tick.
damos_quota_is_full() rejects all regions when esz < min_region_sz
(default PAGE_SIZE = 4096), so no regions can be tried and no
feedback reaches the tuner — a bootstrapping deadlock.
Floor esz at ctx->min_region_sz after the tuner computes it, guarded
by an esz != 0 check. The guard preserves the temporal tuner's
intentional stop behavior: when score >= 10000 (goal met), temporal
sets esz_bp = 0 to halt migration; the floor must not override that.
Signed-off-by: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
---
mm/damon/core.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/mm/damon/core.c b/mm/damon/core.c
index fd1db234ca304..d33c4360cbd60 100644
--- a/mm/damon/core.c
+++ b/mm/damon/core.c
@@ -2650,6 +2650,10 @@ static void damos_set_effective_quota(struct damon_ctx *ctx, struct damos *s)
esz = quota->esz_bp / 10000;
}
+ /* avoid cold-start deadlock, but respect tuner stop signal (esz=0) */
+ if (esz)
+ esz = max_t(unsigned long, esz, ctx->min_region_sz);
+
if (quota->ms) {
if (quota->total_charged_ns)
throughput = mult_frac(quota->total_charged_sz,
--
2.43.0
^ permalink raw reply related
* [RFC PATCH 4/5] mm/damon/paddr: skip free pageblocks in migration walk
From: Ravi Jonnalagadda @ 2026-05-16 21:03 UTC (permalink / raw)
To: sj, damon, linux-mm, linux-kernel, linux-doc
Cc: akpm, corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun,
ravis.opensrc
In-Reply-To: <20260516210357.2247-1-ravis.opensrc@gmail.com>
damon_pa_migrate() walks every PFN in a region linearly, calling
damon_get_folio() for each one. On sparse physical address spaces
(e.g., CXL-attached memory), a single DAMON region can span hundreds
of gigabytes where most memory is free and sitting in the buddy
allocator. Most page lookups are fruitless and dominate kdamond
tick time.
Check at pageblock boundaries (2MB on x86_64) whether the block is
entirely free. If the first page of a pageblock is a buddy page at
pageblock_order or higher, the entire block is free and can be
skipped. Similarly skip pageblocks where pfn_to_online_page() returns
NULL.
This reduces the iteration from O(region_sz / PAGE_SIZE) to
O(region_sz / pageblock_sz) + O(populated_pages).
buddy_order_unsafe() is used without zone->lock. A transient false
positive (block becomes non-free between the PageBuddy and order
checks) costs at most one tick of missed candidates on that block;
the next tick re-scans. No correctness consequence as DAMON walks
are best-effort.
Signed-off-by: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
---
mm/damon/paddr.c | 25 ++++++++++++++++++++++---
1 file changed, 22 insertions(+), 3 deletions(-)
diff --git a/mm/damon/paddr.c b/mm/damon/paddr.c
index c4738cd5e221e..e844c990987b9 100644
--- a/mm/damon/paddr.c
+++ b/mm/damon/paddr.c
@@ -258,13 +258,32 @@ static unsigned long damon_pa_migrate(struct damon_region *r,
unsigned long addr_unit, struct damos *s,
unsigned long *sz_filter_passed)
{
- phys_addr_t addr, applied;
+ phys_addr_t addr, end, applied;
LIST_HEAD(folio_list);
struct folio *folio = NULL;
+ unsigned long pfn;
addr = damon_pa_phys_addr(r->ar.start, addr_unit);
- while (addr < damon_pa_phys_addr(r->ar.end, addr_unit)) {
- folio = damon_get_folio(PHYS_PFN(addr));
+ end = damon_pa_phys_addr(r->ar.end, addr_unit);
+ while (addr < end) {
+ pfn = PHYS_PFN(addr);
+
+ /* Skip pageblocks that are entirely free. */
+ if (IS_ALIGNED(pfn, pageblock_nr_pages)) {
+ struct page *page = pfn_to_online_page(pfn);
+
+ if (!page) {
+ addr += pageblock_nr_pages * PAGE_SIZE;
+ continue;
+ }
+ if (PageBuddy(page) &&
+ buddy_order_unsafe(page) >= pageblock_order) {
+ addr += pageblock_nr_pages * PAGE_SIZE;
+ continue;
+ }
+ }
+
+ folio = damon_get_folio(pfn);
if (damon_pa_invalid_damos_folio(folio, s)) {
addr += PAGE_SIZE;
continue;
--
2.43.0
^ permalink raw reply related
* [RFC PATCH 5/5] mm/damon/paddr: add time budget to migration page walk
From: Ravi Jonnalagadda @ 2026-05-16 21:03 UTC (permalink / raw)
To: sj, damon, linux-mm, linux-kernel, linux-doc
Cc: akpm, corbet, bijan311, ajayjoshi, honggyu.kim, yunjeong.mun,
ravis.opensrc
In-Reply-To: <20260516210357.2247-1-ravis.opensrc@gmail.com>
On populated physical address ranges the pageblock skip optimization
alone is insufficient — most pageblocks contain at least one allocated
page, so the walk still iterates millions of PFNs.
Add a 100ms wall-clock time budget to damon_pa_migrate(). Once the
deadline is reached, the walk breaks out and migrates whatever folios
have been collected so far.
The time check is amortized by only calling ktime_get() every 4096
pages (~16MB of address space), adding negligible overhead to the
fast path.
Signed-off-by: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
---
mm/damon/paddr.c | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/mm/damon/paddr.c b/mm/damon/paddr.c
index e844c990987b9..a2565287bc10f 100644
--- a/mm/damon/paddr.c
+++ b/mm/damon/paddr.c
@@ -14,6 +14,7 @@
#include <linux/swap.h>
#include <linux/memory-tiers.h>
#include <linux/mm_inline.h>
+#include <linux/ktime.h>
#include "../internal.h"
#include "ops-common.h"
@@ -254,6 +255,14 @@ static unsigned long damon_pa_deactivate_pages(struct damon_region *r,
return damon_pa_de_activate(r, addr_unit, s, false, sz_filter_passed);
}
+/* Maximum wall-clock time to spend in a single migration walk (ns) */
+#define DAMON_PA_MIGRATE_BUDGET_NS (100 * NSEC_PER_MSEC)
+
+/* Check the time budget every 4096 pages (~16MB) to amortize ktime_get(). */
+#define DAMON_PA_MIGRATE_TIME_CHECK_PAGES 4096
+#define DAMON_PA_MIGRATE_TIME_CHECK_MASK \
+ (DAMON_PA_MIGRATE_TIME_CHECK_PAGES - 1)
+
static unsigned long damon_pa_migrate(struct damon_region *r,
unsigned long addr_unit, struct damos *s,
unsigned long *sz_filter_passed)
@@ -262,6 +271,7 @@ static unsigned long damon_pa_migrate(struct damon_region *r,
LIST_HEAD(folio_list);
struct folio *folio = NULL;
unsigned long pfn;
+ ktime_t deadline = ktime_add_ns(ktime_get(), DAMON_PA_MIGRATE_BUDGET_NS);
addr = damon_pa_phys_addr(r->ar.start, addr_unit);
end = damon_pa_phys_addr(r->ar.end, addr_unit);
@@ -283,6 +293,11 @@ static unsigned long damon_pa_migrate(struct damon_region *r,
}
}
+ /* Time budget: keep kdamond responsive on long migration walks. */
+ if (!(pfn & DAMON_PA_MIGRATE_TIME_CHECK_MASK) &&
+ ktime_after(ktime_get(), deadline))
+ break;
+
folio = damon_get_folio(pfn);
if (damon_pa_invalid_damos_folio(folio, s)) {
addr += PAGE_SIZE;
--
2.43.0
^ permalink raw reply related
* [RFC v3 0/3] 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
Hi all,
kconfirm has shrunk a lot since v2!
Okay back to the RFC...
kconfirm is a tool to detect misusage of Kconfig. It detects dead code,
constant conditions, and invalid (reverse) ranges. There are also optional
checks to detect config options that select visible config options, and to
check for dead links in the help texts.
Following this discussion:
https://lore.kernel.org/all/20260405122749.4990dcb538d457769a3276e0@linux-foundation.org/
in which Andrew brought up the possibility of moving kconfirm in-tree,
I've prepared this RFC to do so. See also kconfirm's introduction to the
mailing list:
https://lore.kernel.org/all/6ec4df6d-1445-48ca-8f54-1d1a83c4716d@gmail.com/
False Alarms:
kconfirm aims for zero false-positives, which is currently true for the
default checks (as far as I'm aware - but there are hundreds to go
through); this is not really possible for dead link checks, as this
depends on an internet connection, and we do not attempt to bypass bot
blocks. For this reason, dead link checking is disabled by default, but
I've provided an example below of how to enable it. Additionally, you can
view my previous message to the mailing list with hand-verified dead links
here:
https://lore.kernel.org/all/6732bf08-41ee-40c4-83b2-4ae8bc0da7cf@gmail.com/
Additionally, there is an optional check to detect config options that
select visible config options, as requested by Jani during the review of
the first version of this RFC:
https://lore.kernel.org/all/dcb7439832f0bb35598fba653d922b5f6a4d0058@intel.com/
Even after deduplicating across architectures, there are well over 1,000
instances of these select-visible cases, and I suspect that, despite the
Kconfig documentation saying select-visible should be avoided, some
exceptions will be made. So, I have left this check disabled by default,
keeping in line with the goal of having a low-noise checker. If interested
in using it, I have included an example below of how to enable this check.
Current State of Alarms:
On Linux v7.1-rc3 (which this RFC is based), there are 489 alarms coming
from the default set of checks, and an additional 1,789 alarms if enabling
the optional select-visible check. These counts are with deduplication
across all architectures, a change that was made to the tool's CLI from
RFC v1 to RFC v2. The last time I checked linux-next (next-20260427),
there were 81 unique dead links.
The most critical check is the dead default statements, which has surfaced
a few misconfiguration bugs (fortunately, just for kunit tests), see
examples:
https://lore.kernel.org/all/20260323124118.1414913-1-julianbraha@gmail.com/
and:
https://lore.kernel.org/all/20260323123536.1413732-1-julianbraha@gmail.com/
But hopefully kconfirm can ease maintenance and we can prevent more of
these from making it into the tree in the future.
Use it:
You can test out kconfirm with this patch series by compiling and running
kconfirm like this:
`make kconfirm`
To enable the select-visible check:
`KCONFIRM_ARGS="--enable-check select_visible" make kconfirm`
And to enable dead link checks in the help texts:
`KCONFIRM_ARGS="--enable-check dead_link" make kconfirm`
kconfirm by default runs on the same architecture as the kernel build
would; though additional architectures can be enabled by passing
`--enable-arch` and the default architecture can be disabled using
`--disable-arch`. Alarms are tagged with the affected architecture. For
alarms that appear in multiple of the enabled architectures, they are
deduplicated and tagged like: [X86] or [X86, ARM].
Dependencies will need to first be downloaded from crates.io by running
the `cargo vendor` command in scripts/kconfirm/
Requested feedback:
1. I would like to know if anyone thinks that the select-visible check
should be enabled by default.
2. I'm still hoping for some usage feedback!
Thanks,
Julian Braha
---
Changes since v2:
- Reduce Rust dependencies significantly (follows Demi's suggestions):
- from 6 direct dependencies to 1
- from 107 indirect dependencies to 4
- Replace ureq crate with usage of system libcurl (thanks Demi)
- Replace clap crate with FFI bindings to libc's getopt_long (also Demi)
- Remove crates env_logger, regex
- Switch from vendoring dependencies to requiring users to first download
outside of Make (as suggested by Miguel)
- Various makefile improvements (as pointed out by Nicolas):
- Fix out-of-tree builds
- Only delete kconfirm artifacts with 'distclean' and 'mrproper'
- Add myself as maintainer of kconfirm (as discussed with Nicolas)
- Remove dedicated code license file (pointed out by Jani)
- Update documentation to explain tool setup
- Add hint to users to check documentation and download tool dependencies
- Address sashiko's many code-level and documentation suggestions:
- Follow the kernel's rust import style
- Fix a dead_range/duplicate_range alarm mixup
- Fix potential duplicates in default value style check
- Avoid panicking on errors
- Clarify parse failure check usage in documentation
- Fix typo in documentation
- Can now enable architectures and disable the default (host) architecture in the CLI
Link to v2:
https://lore.kernel.org/all/20260509203808.1142311-1-julianbraha@gmail.com/
Changes since v1:
- vendored dependencies instead of requiring an internet connection
- removed Cargo.lock
- replaced reqwest dependency with smaller ureq
- removed rustls, expect user to have openssl instead
- added select-visible check based on Jani's feature request
- added invalid (reverse) range check
- deduplicating alarms that appear for multiple architectures
- `make clean` no longer deletes kconfirm's build artifacts
- typo fixes in documentation
- added patch description for the main "add kconfirm" patch (patch 1/2)
Link to v1:
https://lore.kernel.org/all/20260427174429.779474-1-julianbraha@gmail.com/
---
Julian Braha (3):
scripts: add kconfirm
Documentation: add kconfirm
MAINTAINERS: create entry for kconfirm
Documentation/dev-tools/index.rst | 1 +
Documentation/dev-tools/kconfirm.rst | 222 ++++++
MAINTAINERS | 6 +
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 +++++
21 files changed, 2781 insertions(+), 5 deletions(-)
create mode 100644 Documentation/dev-tools/kconfirm.rst
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
--
2.53.0
^ permalink raw reply
* [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
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