* Re: [PATCH RFC v2 9/9] docs: iio: add documentation for ad9910 driver
From: Jonathan Cameron @ 2026-03-22 17:34 UTC (permalink / raw)
To: Rodrigo Alencar via B4 Relay
Cc: rodrigo.alencar, linux-iio, devicetree, linux-kernel, linux-doc,
Lars-Peter Clausen, Michael Hennerich, David Lechner,
Andy Shevchenko, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Philipp Zabel, Jonathan Corbet, Shuah Khan
In-Reply-To: <20260318-ad9910-iio-driver-v2-9-e79f93becf11@analog.com>
On Wed, 18 Mar 2026 17:56:09 +0000
Rodrigo Alencar via B4 Relay <devnull+rodrigo.alencar.analog.com@kernel.org> wrote:
> From: Rodrigo Alencar <rodrigo.alencar@analog.com>
>
> Add documentation for the AD9910 DDS IIO driver, which describes channels,
> DDS modes, attributes and ABI usage examples.
>
> Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
A few things inline. I've not cropped as there is a lot here and I'd like
it all to remain visible in the reply.
Overall this is a very interesting device so whilst I think we are making
progress it might still take a while to come to an overall conclusion
on the ABI!
> ---
> Documentation/iio/ad9910.rst | 654 +++++++++++++++++++++++++++++++++++++++++++
> Documentation/iio/index.rst | 1 +
> MAINTAINERS | 1 +
> 3 files changed, 656 insertions(+)
>
> diff --git a/Documentation/iio/ad9910.rst b/Documentation/iio/ad9910.rst
> new file mode 100644
> index 000000000000..116f6af4bc2e
> --- /dev/null
> +++ b/Documentation/iio/ad9910.rst
> @@ -0,0 +1,654 @@
> +.. SPDX-License-Identifier: GPL-2.0-only
> +
> +=============
> +AD9910 driver
> +=============
> +
> +DDS (Direct Digital Synthesizer) driver for the Analog Devices Inc. AD9910.
> +The module name is ``ad9910``.
> +
> +* `AD9910 <https://www.analog.com/en/products/ad9910.html>`_
> +
> +The AD9910 is a 1 GSPS DDS with a 14-bit DAC, driven over SPI. The driver
> +exposes the device through the IIO ``altvoltage`` channel type and supports
> +five DDS operating modes: single tone, parallel port modulation, digital ramp
> +generation (DRG), RAM playback and output shift keying (OSK). The device has
> +8 hardware profiles, each capable of storing independent single tone and RAM
> +playback parameters.
> +
> +
> +Channel hierarchy
> +=================
> +
> +The driver exposes the following IIO output channels, each identified by a
> +unique channel number and a human-readable label:
> +
> +* ``out_altvoltage100``: ``phy``: Physical output: system clock and profile control
> +
> + * ``out_altvoltage110``: ``single_tone``: Single tone mode: per-profile
> + frequency, phase, amplitude
> +
> + * ``out_altvoltage120``: ``parallel_port``: Parallel port modulation: enable
> + and offset/scale parameters
> +
> + * ``out_altvoltage130``: ``digital_ramp_generator``: DRG control: enable,
> + destination, operating mode
> +
> + * ``out_altvoltage131``: ``digital_ramp_up``: DRG ramp-up parameters:
> + limits, step sizes, ramp rate
> + * ``out_altvoltage132``: ``digital_ramp_down``: DRG ramp-down parameters:
> + limits, step sizes, ramp rate
> +
> + * ``out_altvoltage140``: ``ram_control``: RAM playback: enable, destination,
> + operating mode, address range
> +
> + * ``out_altvoltage150``: ``output_shift_keying``: OSK: enable, amplitude
> + scale, ramp rate, auto/manual control
> +
> +The ``phy`` channel is the root of the hierarchy. Changing its
> +``sampling_frequency`` reconfigures the system clock (SYSCLK) which affects all
> +other channels. The ``profile`` attribute on this channel selects the active
> +hardware profile (0-7) used by the single tone and RAM channels.
I asked out this profile thing in one of the other patches. Key to me is
that how we write non active profiles? The most similar thing we've seen
in the past has been setting other frequencies for FSK or phases for PSK or
more mundane DC DAC output that are symbol based. (often an external signal)
For those we have added an additional index so we can see which symbol we
are changing parameters for. Here it might need to be done in the channel
numbering. I'm not sure.
> +
> +All mode-specific channels (parallel port, DRG, RAM, OSK) have an ``enable``
> +attribute. The DRG and RAM channels additionally have ``destination`` and
> +``operating_mode`` attributes that configure which DDS core parameter is
> +modulated and how.
I wonder if we flatten things out and have separate channels for each type
of modulation. Might lead to a more standard looking interfaces. We don't really
have a standard path to control one type of thing feeding another, whereas
we do have simple 'enable' interfaces.
> +
> +DDS modes
> +=========
> +
> +The AD9910 supports multiple modes of operation that can be configured
> +independently or in combination. Such modes and their corresponding IIO channels
> +are described in this section. The following tables are extracted from the
> +AD9910 datasheet and summarizes the control parameters for each mode and their
> +priority when multiple sources are enabled simultaneously:
> +
> +.. flat-table:: DDS Frequency Control
> + :header-rows: 1
> +
> + * - Priority
> + - Data Source
> + - Conditions
> +
> + * - Highest Priority
> + - RAM
> + - RAM enabled and data destination is frequency
> +
> + * -
> + - DRG
> + - DRG enabled and data destination is frequency
> +
> + * -
> + - Parallel data and FTW (frequency_offset)
> + - Parallel data port enabled and data destination is frequency
> +
> + * -
> + - FTW (frequency)
> + - RAM enabled and data destination is not frequency
> +
> + * -
> + - FTW (frequency) in single tone channel for the active profile
> + - DRG enabled and data destination is not frequency
> +
> + * -
> + - FTW (frequency) in single tone channel for the active profile
> + - Parallel data port enabled and data destination is not frequency
> +
> + * - Lowest Priority
> + - FTW (frequency) in single tone channel for the active profile
> + - None
> +
> +.. flat-table:: DDS Phase Control
> + :header-rows: 1
> +
> + * - Priority
> + - Data Source
> + - Conditions
> +
> + * - Highest Priority
> + - RAM
> + - RAM enabled and data destination is phase or polar
> +
> + * -
> + - DRG
> + - DRG enabled and data destination is phase
> +
> + * -
> + - Parallel data port
> + - Parallel data port enabled and data destination is phase
> +
> + * -
> + - Parallel data port and POW register LSBs (phase_offset)
> + - Parallel data port enabled and data destination is polar
> +
> + * -
> + - POW (phase)
> + - RAM enabled and destination is not phase nor polar
> +
> + * -
> + - POW (phase) in single tone channel for the active profile
> + - DRG enabled and data destination is not phase
> +
> + * -
> + - POW (phase) in single tone channel for the active profile
> + - Parallel data port enabled and data destination is not phase nor polar
> +
> + * - Lowest Priority
> + - POW (phase) in single tone channel for the active profile
> + - None
> +
> +.. flat-table:: DDS Amplitude Control
> + :header-rows: 1
> +
> + * - Priority
> + - Data Source
> + - Conditions
> +
> + * - Highest Priority
> + - OSK generator
> + - OSK enabled (auto mode)
> +
> + * -
> + - ASF register
> + - OSK enabled (manual mode)
> +
> + * -
> + - RAM
> + - RAM enabled and data destination is amplitude or polar
> +
> + * -
> + - DRG
> + - DRG enabled and data destination is amplitude
> +
> + * -
> + - Parallel data port
> + - Parallel data port enabled and data destination is amplitude
> +
> + * -
> + - Parallel data port and ASF register LSBs (scale_offset)
> + - Parallel data port enabled and data destination is polar
> +
> + * - Lowest Priority
> + - ASF (scale) in single tone channel for the active profile
> + - (Amplitude scale is already enabled by default)
> +
> +Single tone mode
> +----------------
> +
> +Single tone is the baseline operating mode. The ``single_tone`` channel
> +provides per-profile frequency, phase and amplitude control:
> +
> +.. flat-table::
> + :header-rows: 1
> +
> + * - Attribute
> + - Unit
> + - Description
> +
> + * - ``out_altvoltage110_frequency``
> + - Hz
> + - Output frequency. Range [0, SYSCLK/2). Stored in the active profile's
> + frequency tuning word (FTW).
> +
> + * - ``out_altvoltage110_phase``
> + - rad
> + - Phase offset. Range [0, 2*pi). Stored in the active profile's phase
> + offset word (POW).
> +
> + * - ``out_altvoltage110_scale``
> + - fractional
> + - Amplitude scale factor. Range [0, 1]. Stored in the active profile's
> + amplitude scale factor (ASF).
> +
> +When RAM mode is enabled, single tone parameters are stored in a shadow
> +register and are not written to hardware until RAM mode is disabled.
> +
> +Usage examples
> +^^^^^^^^^^^^^^
> +
> +Set the active profile to 2 and configure a 100 MHz tone:
> +
> +.. code-block:: bash
> +
> + echo 2 > /sys/bus/iio/devices/iio:device0/out_altvoltage100_profile
> + echo 100000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage110_frequency
> + echo 0.5 > /sys/bus/iio/devices/iio:device0/out_altvoltage110_scale
> + echo 0 > /sys/bus/iio/devices/iio:device0/out_altvoltage110_phase
> +
> +Read back the current single tone frequency:
> +
> +.. code-block:: bash
> +
> + cat /sys/bus/iio/devices/iio:device0/out_altvoltage110_frequency
> +
> +Parallel port mode
> +------------------
> +
> +When enabled, the parallel port allows real-time modulation of DDS parameters
> +through a 16-bit external data bus.
> +
> +.. flat-table::
> + :header-rows: 1
> +
> + * - Attribute
> + - Unit
> + - Description
> +
> + * - ``out_altvoltage120_en``
> + - boolean
> + - Enable/disable the parallel data port.
> +
> + * - ``out_altvoltage120_frequency_scale``
> + - power-of-2
> + - FM gain multiplier applied to 16-bit parallel input. Range [1, 32768],
> + must be a power of 2.
> +
> + * - ``out_altvoltage120_frequency_offset``
> + - Hz
> + - Base FTW to which scaled parallel data is added. Range [0, SYSCLK/2).
> +
> + * - ``out_altvoltage120_phase_offset``
> + - rad
> + - Base phase for polar modulation. Lower 8 bits of POW register.
> + Range [0, 2*pi/256).
> +
> + * - ``out_altvoltage120_scale_offset``
> + - fractional
> + - Base amplitude for polar modulation. Lower 6 bits of ASF register.
> + Range [0, 1/256).
> +
> +Usage examples
> +^^^^^^^^^^^^^^
> +
> +Enable parallel port with a frequency scale of 16 and a 50 MHz offset:
> +
> +.. code-block:: bash
> +
> + echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage120_en
> + echo 16 > /sys/bus/iio/devices/iio:device0/out_altvoltage120_frequency_scale
> + echo 50000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage120_frequency_offset
> +
> +Digital ramp generator (DRG)
> +----------------------------
> +
> +The DRG produces linear frequency, phase or amplitude sweeps using dedicated
> +hardware. It is controlled through three channels: a parent control channel
> +(``digital_ramp_generator``) and two child ramp channels
> +(``digital_ramp_up``, ``digital_ramp_down``).
> +
> +Control channel attributes
> +^^^^^^^^^^^^^^^^^^^^^^^^^^
> +
> +.. flat-table::
> + :header-rows: 1
> +
> + * - Attribute
> + - Unit
> + - Description
> +
> + * - ``out_altvoltage130_en``
> + - boolean
> + - Enable/disable the DRG.
> +
> + * - ``out_altvoltage130_destination``
> + - enum
> + - Which DDS parameter is swept: ``frequency``, ``phase`` or
> + ``amplitude``.
> +
> + * - ``out_altvoltage130_destination_available``
> + - string
> + - Lists available destination values.
> +
> + * - ``out_altvoltage130_operating_mode``
> + - enum
> + - Ramp behavior (see table below).
> +
> + * - ``out_altvoltage130_operating_mode_available``
> + - string
> + - Lists available operating mode values.
> +
> +DRG operating modes:
> +
> +.. flat-table::
> + :header-rows: 1
> +
> + * - Mode
> + - Description
> +
> + * - ``bidirectional``
> + - Ramp up then down, dwelling at limits.
> +
> + * - ``ramp_down``
> + - No-dwell low; resets to upper limit at lower limit.
> +
> + * - ``ramp_up``
> + - No-dwell high; resets to lower limit at upper limit.
> +
> + * - ``bidirectional_continuous``
> + - Continuous sweep without dwelling at either limit.
> +
> +Ramp channel attributes
> +^^^^^^^^^^^^^^^^^^^^^^^^
> +
> +The ``digital_ramp_up`` (channel 131) and ``digital_ramp_down`` (channel 132)
> +channels share the same attribute set but configure ascending and descending
> +ramp parameters independently:
> +
> +.. flat-table::
> + :header-rows: 1
> +
> + * - Attribute
> + - Unit
> + - Description
> +
> + * - ``frequency``
> + - Hz
> + - Ramp limit when destination is ``frequency``. Range [0, SYSCLK/2).
> +
> + * - ``phase``
> + - rad
> + - Ramp limit when destination is ``phase``. Range [0, 2*pi).
> +
> + * - ``scale``
> + - fractional
> + - Ramp limit when destination is ``amplitude``. Range [0, 1).
> +
> + * - ``sampling_frequency``
> + - Hz
> + - Ramp clock rate: SYSCLK / (4 * divider).
> +
> + * - ``frequency_step``
> + - Hz
> + - Per-tick frequency increment/decrement when destination is
> + ``frequency``.
> +
> + * - ``phase_step``
> + - rad
> + - Per-tick phase increment/decrement when destination is ``phase``.
> +
> + * - ``scale_step``
> + - fractional
> + - Per-tick amplitude increment/decrement when destination is
> + ``amplitude``. Range [0, 1).
> +
> +Usage examples
> +^^^^^^^^^^^^^^
> +
> +Configure a frequency sweep from 10 MHz to 100 MHz at a 1 MHz step:
> +
> +.. code-block:: bash
> +
> + # Set DRG destination to frequency
> + echo frequency > /sys/bus/iio/devices/iio:device0/out_altvoltage130_destination
> +
> + # Set operating mode
> + echo bidirectional_continuous > /sys/bus/iio/devices/iio:device0/out_altvoltage130_operating_mode
> +
> + # Set ramp limits
> + echo 60000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage131_frequency
> + echo 40000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage132_frequency
> +
> + # Set ramp step size to 1 MHz
> + echo 1000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage131_frequency_step
> + echo 1000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage132_frequency_step
> +
> + # Set ramp clock rate
> + echo 50000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage131_sampling_frequency
> +
> + # Enable the DRG
> + echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage130_en
> +
> +Read the current DRG operating mode:
> +
> +.. code-block:: bash
> +
> + cat /sys/bus/iio/devices/iio:device0/out_altvoltage130_operating_mode
> +
> +RAM mode
> +--------
> +
> +The AD9910 contains a 1024 x 32-bit RAM that can be loaded with waveform data
> +and played back to modulate frequency, phase, amplitude, or polar (phase +
> +amplitude) parameters.
> +
> +RAM control channel attributes
> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> +
> +.. flat-table::
> + :header-rows: 1
> +
> + * - Attribute
> + - Unit
> + - Description
> +
> + * - ``out_altvoltage140_en``
> + - boolean
> + - Enable/disable RAM playback. Toggling swaps profile registers between
> + single tone and RAM configurations across all 8 profiles.
> +
> + * - ``out_altvoltage140_destination``
> + - enum
> + - RAM data target: ``frequency``, ``phase``, ``amplitude`` or ``polar``.
> + Cannot be changed while RAM mode is enabled.
> +
> + * - ``out_altvoltage140_destination_available``
> + - string
> + - Lists available destination values.
> +
> + * - ``out_altvoltage140_operating_mode``
> + - enum
> + - Playback behavior (see table below).
> +
> + * - ``out_altvoltage140_operating_mode_available``
> + - string
> + - Lists available operating mode values.
> +
> + * - ``out_altvoltage140_frequency``
> + - Hz
> + - Frequency tuning word used as the single tone frequency when
> + RAM destination is not ``frequency``. Range [0, SYSCLK/2).
> +
> + * - ``out_altvoltage140_phase``
> + - rad
> + - Phase offset word used as the single tone phase when RAM destination
> + is not ``phase``. Range [0, 2*pi).
> +
> + * - ``out_altvoltage140_sampling_frequency``
> + - Hz
> + - RAM playback step rate controlling how fast the address counter
> + advances: SYSCLK / (4 * step_rate). Stored per-profile.
> +
> + * - ``out_altvoltage140_address_start``
Do we need this flexibility to set the start? We needed a length, but
if we want different effective start can just load a different image.
> + - integer
> + - Start address for the active profile. Range [0, 1023]. Cannot be
> + changed while RAM mode is enabled. If set above current end address,
> + end address is automatically adjusted.
> +
> + * - ``out_altvoltage140_address_end``
> + - integer
> + - End address for the active profile. Range [address_start, 1023].
> + Cannot be changed while RAM mode is enabled.
> +
> +RAM operating modes:
> +
> +.. flat-table::
> + :header-rows: 1
> +
> + * - Mode
> + - Description
> +
> + * - ``direct_switch``
> + - Start address defines a fixed word used by the selected profile.
> +
> + * - ``ramp_up``
> + - One-shot ramp through the current profile's address range.
> +
> + * - ``bidirectional``
> + - Ramp up then down through profile 0's address range.
> +
> + * - ``bidirectional_continuous``
> + - Continuous ramp up/down through current profile's address range.
> +
> + * - ``ramp_up_continuous``
> + - Continuous ramp up through current profile's address range.
> +
> + * - ``sequenced``
> + - Sequential playback from profile 0 to the active profile.
> + Requires active profile > 0.
> +
> + * - ``sequenced_continuous``
> + - Continuous sequential playback. Requires active profile > 0.
> +
> +Loading RAM data
> +^^^^^^^^^^^^^^^^
> +
> +RAM data is loaded through the firmware upload framework. The driver registers
> +a firmware upload device named ``iio_deviceX:ram``. Data must be a multiple of
> +4 bytes (32-bit words) and at most 4096 bytes (1024 words).
> +
> +Usage examples
> +^^^^^^^^^^^^^^
> +
> +Configure RAM mode with frequency destination and load a waveform:
> +
> +.. code-block:: bash
> +
> + # Set RAM address range for profile 0
> + echo 0 > /sys/bus/iio/devices/iio:device0/out_altvoltage140_address_start
> + echo 999 > /sys/bus/iio/devices/iio:device0/out_altvoltage140_address_end
> +
> + # Set destination and operating mode
> + echo frequency > /sys/bus/iio/devices/iio:device0/out_altvoltage140_destination
> + echo ramp_up_continuous > /sys/bus/iio/devices/iio:device0/out_altvoltage140_operating_mode
> +
> + # Set playback rate
> + echo 250000 > /sys/bus/iio/devices/iio:device0/out_altvoltage140_sampling_frequency
> +
> + # Load RAM data via firmware upload
> + echo 1 > /sys/class/firmware/iio\:device0\:ram/loading
> + cat waveform.bin > /sys/class/firmware/iio\:device0\:ram/data
> + echo 0 > /sys/class/firmware/iio\:device0\:ram/loading
> +
> + # Enable RAM mode
> + echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage140_en
> +
> +Read the current RAM operating mode:
> +
> +.. code-block:: bash
> +
> + cat /sys/bus/iio/devices/iio:device0/out_altvoltage140_operating_mode
> +
> +Output shift keying (OSK)
> +-------------------------
> +
> +OSK controls the output amplitude envelope, allowing the output to be ramped
> +on/off rather than switched abruptly.
> +
> +.. flat-table::
> + :header-rows: 1
> +
> + * - Attribute
> + - Unit
> + - Description
> +
> + * - ``out_altvoltage150_en``
> + - boolean
> + - Enable/disable OSK.
> +
> + * - ``out_altvoltage150_scale``
> + - fractional
> + - Target amplitude for the OSK ramp. 14-bit ASF field. Range [0, 1).
> +
> + * - ``out_altvoltage150_sampling_frequency``
> + - Hz
> + - OSK ramp rate: SYSCLK / (4 * divider).
> +
> + * - ``out_altvoltage150_pinctrl_en``
> + - boolean
> + - Enable manual external pin control. When enabled, the OSK pin directly
> + gates the output on/off instead of using the automatic ramp.
> +
> + * - ``out_altvoltage150_scale_step``
> + - fractional
> + - Automatic OSK amplitude step. Writing non-zero enables automatic OSK
> + and sets the per-tick increment. Writing ``0`` disables it. Rounded to
> + nearest hardware step: 0.000061, 0.000122, 0.000244 or 0.000488.
> +
> +Usage examples
> +^^^^^^^^^^^^^^
> +
> +Enable OSK with automatic ramping:
> +
> +.. code-block:: bash
> +
> + # Set ramp rate
> + echo 1000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_sampling_frequency
> +
> + # Enable automatic OSK with step size
> + echo 0.000244 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_scale_step
> +
> + # Enable OSK
> + echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_en
> +
> +Enable manual pin-controlled OSK:
> +
> +.. code-block:: bash
> +
> + # Set target amplitude to full scale
> + echo 1.0 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_scale
> +
> + # Enable manual pin control
> + echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_pinctrl_en
> + echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage150_en
> +
> +
> +Physical channel
> +================
> +
> +The ``phy`` channel provides device-level control:
> +
> +.. flat-table::
> + :header-rows: 1
> +
> + * - Attribute
> + - Unit
> + - Description
> +
> + * - ``out_altvoltage100_sampling_frequency``
> + - Hz
> + - System clock (SYSCLK) frequency. With PLL enabled, configures the PLL
> + multiplier (range 420-1000 MHz). Without PLL, ref clock can only be
> + divided by 2.
> +
> + * - ``out_altvoltage100_profile``
> + - integer
> + - Active profile index [0, 7]. Selected via GPIO pins. Each profile
> + stores an independent set of single tone and RAM playback parameters.
> +
> + * - ``out_altvoltage100_powerdown``
> + - boolean
> + - Software power-down. Writing 1 powers down the digital core, DAC,
> + reference clock input and auxiliary DAC simultaneously.
> +
> +Usage examples
> +--------------
> +
> +Set the system clock to 1 GHz and select profile 3:
> +
> +.. code-block:: bash
> +
> + echo 1000000000 > /sys/bus/iio/devices/iio:device0/out_altvoltage100_sampling_frequency
> + echo 3 > /sys/bus/iio/devices/iio:device0/out_altvoltage100_profile
> +
> +Read current system clock frequency:
> +
> +.. code-block:: bash
> +
> + cat /sys/bus/iio/devices/iio:device0/out_altvoltage100_sampling_frequency
> +
> +Power down the device:
> +
> +.. code-block:: bash
> +
> + echo 1 > /sys/bus/iio/devices/iio:device0/out_altvoltage100_powerdown
> diff --git a/Documentation/iio/index.rst b/Documentation/iio/index.rst
> index ba3e609c6a13..55cb1ce84ba8 100644
> --- a/Documentation/iio/index.rst
> +++ b/Documentation/iio/index.rst
> @@ -29,6 +29,7 @@ Industrial I/O Kernel Drivers
> ad7606
> ad7625
> ad7944
> + ad9910
> ade9000
> adis16475
> adis16480
> diff --git a/MAINTAINERS b/MAINTAINERS
> index edd87ee7da5f..14e4272357ce 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -1637,6 +1637,7 @@ S: Supported
> W: https://ez.analog.com/linux-software-drivers
> F: Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910
> F: Documentation/devicetree/bindings/iio/frequency/adi,ad9910.yaml
> +F: Documentation/iio/ad9910.rst
> F: drivers/iio/frequency/ad9910.c
>
> ANALOG DEVICES INC MAX22007 DRIVER
>
^ permalink raw reply
* Re: [PATCH] docs: add advanced search for kernel documentation
From: Rito Rhymes @ 2026-03-22 17:25 UTC (permalink / raw)
To: Rito Rhymes, Jonathan Corbet, Mauro Carvalho Chehab, linux-doc
Cc: Shuah Khan, linux-kernel
In-Reply-To: <DH9H5TPM3W0J.2XCBEY9U8NQF6@ritovision.com>
Also for reference, I have twice implemented an advanced search
feature for the OWASP Vulnerable Web Applications Directory
(VWAD): first for an earlier version of the site, and more
recently for its newly launched current version.
https://vwad.owasp.org
The kernel documentation search work here is a different system with
a different purpose, but it is adjacent experience, and I would
appreciate any feedback you or others may have on improving the
patch.
Rito
^ permalink raw reply
* Re: [PATCH RFC v2 8/9] Documentation: ABI: testing: add docs for ad9910 sysfs entries
From: Jonathan Cameron @ 2026-03-22 17:22 UTC (permalink / raw)
To: Rodrigo Alencar via B4 Relay
Cc: rodrigo.alencar, linux-iio, devicetree, linux-kernel, linux-doc,
Lars-Peter Clausen, Michael Hennerich, David Lechner,
Andy Shevchenko, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Philipp Zabel, Jonathan Corbet, Shuah Khan
In-Reply-To: <20260318-ad9910-iio-driver-v2-8-e79f93becf11@analog.com>
On Wed, 18 Mar 2026 17:56:08 +0000
Rodrigo Alencar via B4 Relay <devnull+rodrigo.alencar.analog.com@kernel.org> wrote:
> From: Rodrigo Alencar <rodrigo.alencar@analog.com>
>
> Add ABI documentation file for the DDS AD9910 with sysfs entries to
> control Parallel Port, Digital Ramp Generator, RAM and OSK parameters.
>
> Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
> ---
Thanks for writing this up. Let's see how others view it. There is a lot
going on here! My main comment has been around trying to write the
docs as a generic thing rather than focusing on device details.
Jonathan
> .../ABI/testing/sysfs-bus-iio-frequency-ad9910 | 182 +++++++++++++++++++++
> MAINTAINERS | 1 +
> 2 files changed, 183 insertions(+)
>
> diff --git a/Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910 b/Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910
> new file mode 100644
> index 000000000000..120de494f6b1
> --- /dev/null
> +++ b/Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910
> @@ -0,0 +1,182 @@
> +What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_profile
> +KernelVersion:
> +Contact: linux-iio@vger.kernel.org
> +Description:
> + Read/write the active profile index [0, 7] from/to the physical
> + channel. The AD9910 supports 8 profiles, each storing a complete
> + set of single tone (frequency, phase, amplitude) and RAM playback
> + parameters.
This one is interesting. Can we treat them as symbols that we are picking
between? We have similar DAC ABIs for that already.
Is this picking between them for purposes of configuration or setting which one is
in being output currently?
> +
> +What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_frequency_offset
> +KernelVersion:
> +Contact: linux-iio@vger.kernel.org
> +Description:
> + Read/write the parallel port frequency offset in Hz. This is the
> + base frequency tuning word (FTW register) to which the scaled
> + parallel port data is added during parallel data port modulation.
> + Valid range is [0, SYSCLK/2).
Ideally think about how these controls generalize (if they do) and avoid
device specific descriptions.
> +
> +What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_frequency_scale
> +KernelVersion:
> +Contact: linux-iio@vger.kernel.org
> +Description:
> + Read/write the parallel port frequency modulation gain. The value
> + must be a power of 2 in the [1, 2^15] range. This value scales the
> + 16-bit parallel data port input before adding it to the
> + frequency_offset value.
Can we provide an _available for this with all 16 values? Then avoid the specific
device nature of the documentation instead saying see the _available for what is possible.
> +
> +What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_phase_offset
> +KernelVersion:
> +Contact: linux-iio@vger.kernel.org
> +Description:
> + Read/write the parallel port phase offset in radians. Valid range
> + is [0, 2*pi/256). This sets the lower 8 bits of the phase offset
> + word (POW register) used as a base during parallel port polar
> + modulation.
Given it does full phase shift, I don't think for userspace docs we care about the 8
bits of whatever register.
> +
> +What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_scale_offset
> +KernelVersion:
> +Contact: linux-iio@vger.kernel.org
> +Description:
> + Read/write the parallel port amplitude scale offset. Valid range
> + is [0, 1/256). This sets the lower 6 bits of the amplitude scale
> + factor (ASF register) used as a base during parallel port polar
> + modulation.
> +
> +What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_destination
> +KernelVersion:
> +Contact: linux-iio@vger.kernel.org
> +Description:
> + Read/write the digital ramp generator (DRG) or the RAM control
> + destination parameter. Determines which DDS core parameter is to
> + be modulated when the child mode channel is enabled.
> +
> + Available values can be read from the corresponding
> + out_altvoltageY_destination_available attribute.
> +
> + Valid values: "polar" (only for RAM control), "frequency", "phase"
> + and "amplitude"
This is very device specific. Maybe we are better representing these as separate
channels each with their own controls for DRG. No problem if changing one changes
another.
> +
> +What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_destination_available
> +KernelVersion:
> +Contact: linux-iio@vger.kernel.org
> +Description:
> + Lists the available destination values for the DRG channel:
> + "frequency phase amplitude"; or for the RAM control channel:
> + "frequency phase amplitude polar".
> +
> +What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_operating_mode
> +KernelVersion:
> +Contact: linux-iio@vger.kernel.org
> +Description:
> + Read/write the DRG or RAM control operating mode. For the DRG
> + channel it controls the no-dwell behavior of the ramp.
> +
> + Available values can be read from the corresponding
> + out_altvoltageY_operating_mode_available attribute.
> +
> + Valid values for DRG channel:
> +
> + - "bidirectional": Normal ramp generation (ramp up then
> + down, dwelling at limits).
Some sort of trapezium wave? Maybe this and continuous forms are combined
and we have a separate dwell time control?
> + - "ramp_down": No-dwell low; the ramp resets to upper
> + limit upon reaching the lower limit.
> + - "ramp_up": No-dwell high; the ramp resets to lower
> + limit upon reaching the upper limit.
> + - "bidirectional_continuous": Both no-dwell high and low;
> + the ramp continuously sweeps without dwelling.
Triangle wave? bidirectional continuous is a rather confusing term so maybe
we should rethink this one.
> +
> + Valid values for RAM control channel:
> +
> + - "direct_switch": start address defines fixed word to be used
> + by the selected profile.
> + - "ramp_up": One-shot ramp up through current profile's address
> + range.
> + - "bidirectional": Ramp up then down through PROFILE0 pin.
Avoid specifics like this. Can we call external control pin or something like that?
> + - "bidirectional_continuous": Continuous ramp up/down
> + through current profile's address range.
> + - "ramp_up_continuous": Continuous ramp up through
> + current profile's address range.
I guess this goes back to start on finishing ramping up?
> + - "sequenced": Sequenced playback of RAM profiles up to
> + the active profile. Requires active profile > 0.
Is this just running through each profile one after another? (other than profile 0)?
> + - "sequenced_continuous": Continuous sequenced playback
> + of RAM profiles up to the active profile. Requires
> + active profile > 0.
Similar to above, maybe separate out dwell time if that's the difference between
sequenced and sequenced_continuous.
> +
> +What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_operating_mode_available
> +KernelVersion:
> +Contact: linux-iio@vger.kernel.org
> +Description:
> + For the DRG channel it lists the available operating mode values:
> + "bidirectional ramp_down ramp_up bidirectional_continuous".
> +
> + For the RAM control channel it lists the available operating mode
> + values:
> + "direct_switch ramp_up bidirectional bidirectional_continuous
> + ramp_up_continuous sequenced sequenced_continuous".
> +
> +What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_frequency_step
> +KernelVersion:
> +Contact: linux-iio@vger.kernel.org
> +Description:
> + Read/write the DRG frequency step size in Hz for ramp up and ramp
> + down DRG channels. This is the increment/decrement step applied to
> + the DRG frequency value, which is input to the DDS core and it is
> + updated at each ramp clock tick when the DRG destination is
> + set to "frequency". Valid range is [0, sysclk/2).
> +
> +What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_phase_step
> +KernelVersion:
> +Contact: linux-iio@vger.kernel.org
> +Description:
> + Read/write the DRG phase step size in radians for ramp up and ramp
> + down DRG channels. This is the increment/decrement step applied to
> + the DRG phase value, which is input to the DDS core and it is
> + updated at each ramp clock tick when the DRG destination is
> + set to "phase". Valid range is [0, 2*pi).
> +
> +What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_scale_step
> +KernelVersion:
> +Contact: linux-iio@vger.kernel.org
> +Description:
> + For the DRG ramp up/down channels this is used to read/write the
> + DRG amplitude step size, which is applied as an
> + increment/decrement to the DRG amplitude value, input to the DDS
> + core, updated at each ramp clock tick when the DRG destination is
> + set to "amplitude". Valid range is [0, 1].
> +
> + For the OSK channel this is used to read/write the automatic OSK
> + amplitude ramp step size. Writing a non-zero value enables
> + automatic OSK mode and sets the amplitude step size. Writing "0"
> + disables automatic OSK mode. The value is rounded to the nearest
> + hardware supported step: 0.000061, 0.000122, 0.000244, or
> + 0.000488.
Those need to come from available attribute. Don't belong in the docs as if we
have them here there is little chance of generalizing later to cover more devices.
> +
> +What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_address_start
> +KernelVersion:
> +Contact: linux-iio@vger.kernel.org
> +Description:
> + Read/write the RAM start address for the active profile. Defines
> + the first RAM word address used during playback. Cannot be
> + changed while RAM mode is enabled. Valid range is [0, 1023].
> + If set above the current end address, the end address is
> + automatically adjusted to match.
> +
> +What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_address_end
> +KernelVersion:
> +Contact: linux-iio@vger.kernel.org
> +Description:
> + Read/write the RAM end address for the active profile. Defines
> + the last RAM word address used during playback. Cannot be
> + changed while RAM mode is enabled. Valid range is
> + [address_start, 1023].
> +
> +What: /sys/bus/iio/devices/iio:deviceX/out_altvoltageY_pinctrl_en
> +KernelVersion:
> +Contact: linux-iio@vger.kernel.org
> +Description:
> + Read/write the OSK manual external control enable. Writing '1'
> + enables manual control of the output amplitude envelope via an
> + external pin. Writing '0' disables it. When enabled, the OSK pin
> + directly controls the amplitude on/off state rather than using
> + the automatic OSK ramp.
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 6403439b530d..edd87ee7da5f 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -1635,6 +1635,7 @@ M: Rodrigo Alencar <rodrigo.alencar@analog.com>
> L: linux-iio@vger.kernel.org
> S: Supported
> W: https://ez.analog.com/linux-software-drivers
> +F: Documentation/ABI/testing/sysfs-bus-iio-frequency-ad9910
> F: Documentation/devicetree/bindings/iio/frequency/adi,ad9910.yaml
> F: drivers/iio/frequency/ad9910.c
>
>
^ permalink raw reply
* Re: (sashiko status) [RFC PATCH v4 00/10] mm/damon: let DAMON be paused and resumed
From: SeongJae Park @ 2026-03-22 17:11 UTC (permalink / raw)
To: SeongJae Park
Cc: Liam R. Howlett, Andrew Morton, Brendan Higgins, David Gow,
David Hildenbrand, Jonathan Corbet, Lorenzo Stoakes, Michal Hocko,
Mike Rapoport, Shuah Khan, Shuah Khan, Suren Baghdasaryan,
Vlastimil Babka, damon, kunit-dev, linux-doc, linux-kernel,
linux-kselftest, linux-mm
In-Reply-To: <20260322170506.82977-1-sj@kernel.org>
On Sun, 22 Mar 2026 10:05:05 -0700 SeongJae Park <sj@kernel.org> wrote:
> Forwarding sashiko.dev review status for this thread.
>
> # review url: https://sashiko.dev/#/patchset/20260322155728.81434-1-sj@kernel.org
>
> - [RFC PATCH v4 01/10] mm/damon/core: introduce damon_ctx->paused
> - status: Reviewed
> - review: ISSUES MAY FOUND
> - [RFC PATCH v4 02/10] mm/damon/sysfs: add pause file under context dir
> - status: Reviewed
> - review: No issues found.
> - [RFC PATCH v4 03/10] Docs/mm/damon/design: update for context pause/resume feature
> - status: Reviewed
> - review: No issues found.
> - [RFC PATCH v4 04/10] Docs/admin-guide/mm/damon/usage: update for pause file
> - status: Reviewed
> - review: No issues found.
> - [RFC PATCH v4 05/10] Docs/ABI/damon: update for pause sysfs file
> - status: Reviewed
> - review: No issues found.
> - [RFC PATCH v4 06/10] mm/damon/tests/core-kunit: test pause commitment
> - status: Reviewed
> - review: No issues found.
> - [RFC PATCH v4 07/10] selftests/damon/_damon_sysfs: support pause file staging
> - status: Pending
> - review: ISSUES MAY FOUND
The status is pending. 'ISSUES MAY FOUND' is a bug of hkml. I'm fixing it.
Thanks,
SJ
[...]
^ permalink raw reply
* Re: [PATCH] docs: add advanced search for kernel documentation
From: Rito Rhymes @ 2026-03-22 17:08 UTC (permalink / raw)
To: Jonathan Corbet, Rito Rhymes, Mauro Carvalho Chehab, linux-doc
Cc: Shuah Khan, linux-kernel
In-Reply-To: <87cy0vetu7.fsf@trenco.lwn.net>
Hi Jon
> Documentation/process/coding-assistants.rst
That was my oversight. I failed to include the appropriate
coding-assistant attribution/disclosure, and I will reroll my patches
accordingly.
> I'm curious about where you are going with this in general
I am not contributing as a kernel developer. My background is in
front-end engineering, product/UX, and developer-facing documentation
and platform surfaces, and that is where I believe I can add value here.
Linux is important infrastructure, and I have already been making
related contributions in its ecosystem. In trying to improve
lore.kernel.org, I contributed patches merged upstream to Public Inbox
for small-screen layout behavior and for enabling admin-injected meta
tags in the document head.
After my Git patches for gitweb mobile responsiveness were merged,
I prepared a kernel.org mobile-responsiveness patch series
(current theme is only built for desktop and breaks on small screens).
Since there was not an established mailing-list path for merging that
work, Johannes Schindelin introduced me to Konstantin in an archived
thread, which opened a concrete path for contributing to kernel.org.
More broadly, I have worked on improving developer-facing surfaces,
including documentation and related tooling, in other OSS projects.
That is the kind of value I am aiming to add here: not direct kernel
development, but improving usability, discoverability, and developer
experience around important technical infrastructure.
Rito
^ permalink raw reply
* Re: (sashiko status) [RFC PATCH v4 00/10] mm/damon: let DAMON be paused and resumed
From: SeongJae Park @ 2026-03-22 17:05 UTC (permalink / raw)
To: SeongJae Park
Cc: Liam R. Howlett, Andrew Morton, Brendan Higgins, David Gow,
David Hildenbrand, Jonathan Corbet, Lorenzo Stoakes, Michal Hocko,
Mike Rapoport, Shuah Khan, Shuah Khan, Suren Baghdasaryan,
Vlastimil Babka, damon, kunit-dev, linux-doc, linux-kernel,
linux-kselftest, linux-mm
In-Reply-To: <20260322155728.81434-1-sj@kernel.org>
Forwarding sashiko.dev review status for this thread.
# review url: https://sashiko.dev/#/patchset/20260322155728.81434-1-sj@kernel.org
- [RFC PATCH v4 01/10] mm/damon/core: introduce damon_ctx->paused
- status: Reviewed
- review: ISSUES MAY FOUND
- [RFC PATCH v4 02/10] mm/damon/sysfs: add pause file under context dir
- status: Reviewed
- review: No issues found.
- [RFC PATCH v4 03/10] Docs/mm/damon/design: update for context pause/resume feature
- status: Reviewed
- review: No issues found.
- [RFC PATCH v4 04/10] Docs/admin-guide/mm/damon/usage: update for pause file
- status: Reviewed
- review: No issues found.
- [RFC PATCH v4 05/10] Docs/ABI/damon: update for pause sysfs file
- status: Reviewed
- review: No issues found.
- [RFC PATCH v4 06/10] mm/damon/tests/core-kunit: test pause commitment
- status: Reviewed
- review: No issues found.
- [RFC PATCH v4 07/10] selftests/damon/_damon_sysfs: support pause file staging
- status: Pending
- review: ISSUES MAY FOUND
- [RFC PATCH v4 08/10] selftests/damon/drgn_dump_damon_status: dump pause
- status: Reviewed
- review: No issues found.
- [RFC PATCH v4 09/10] selftests/damon/sysfs.py: check pause on assert_ctx_committed()
- status: Reviewed
- review: No issues found.
- [RFC PATCH v4 10/10] selftets/damon/sysfs.py: pause DAMON before dumping status
- status: Reviewed
- review: ISSUES MAY FOUND
# hkml [1] generated a draft of this mail. It can be regenerated
# using below command:
#
# hkml patch sashiko_dev --thread_status --for_forwarding \
# 20260322155728.81434-1-sj@kernel.org
#
# [1] https://github.com/sjp38/hackermail
Sent using hkml (https://github.com/sjp38/hackermail)
^ permalink raw reply
* Re: [PATCH RFC v2 5/9] iio: frequency: ad9910: add RAM mode support
From: Jonathan Cameron @ 2026-03-22 17:05 UTC (permalink / raw)
To: Rodrigo Alencar via B4 Relay
Cc: rodrigo.alencar, linux-iio, devicetree, linux-kernel, linux-doc,
Lars-Peter Clausen, Michael Hennerich, David Lechner,
Andy Shevchenko, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Philipp Zabel, Jonathan Corbet, Shuah Khan
In-Reply-To: <20260318-ad9910-iio-driver-v2-5-e79f93becf11@analog.com>
On Wed, 18 Mar 2026 17:56:05 +0000
Rodrigo Alencar via B4 Relay <devnull+rodrigo.alencar.analog.com@kernel.org> wrote:
> From: Rodrigo Alencar <rodrigo.alencar@analog.com>
>
> Add RAM channel with support for profile-based control. This includes:
> - RAM data loading via firmware upload interface;
> - Per-profile RAM configuration (start/end address, step rate, operating
> mode, dwell control);
> - RAM destination control (frequency, phase, amplitude, polar);
> - RAM operating modes (direct switch, ramp up, bidirectional ramp,
> continuous bidirectional, continuous recirculate);
> - Profile switching for RAM playback;
> - Sampling frequency control via profile step rate;
> - ram-enable-aware read/write paths that redirect single tone
> frequency/phase/amplitude access through reg_profile cache when RAM is
> active;
>
> When RAM is enabled, the DDS core parameters (frequency, phase, amplitude)
> for the single tone channel are sourced from a shadow register cache
> (reg_profile[]) since the profile registers are repurposed for RAM control.
>
> Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
A few minor things. Again I've left discussion of interfaces for docs patches.
> ---
> drivers/iio/frequency/Kconfig | 2 +
> drivers/iio/frequency/ad9910.c | 464 ++++++++++++++++++++++++++++++++++++++++-
> 2 files changed, 462 insertions(+), 4 deletions(-)
>
> diff --git a/drivers/iio/frequency/Kconfig b/drivers/iio/frequency/Kconfig
> index 180e74f62d11..a5b2e5cb5269 100644
> --- a/drivers/iio/frequency/Kconfig
> +++ b/drivers/iio/frequency/Kconfig
> @@ -29,6 +29,8 @@ config AD9910
> tristate "Analog Devices AD9910 Direct Digital Synthesizer"
> depends on SPI
> depends on GPIOLIB
> + select FW_LOADER
> + select FW_UPLOAD
> help
> Say yes here to build support for Analog Devices AD9910
> 1 GSPS, 14-Bit DDS with integrated DAC.
> diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
> index d3367e211dcf..747f4f407536 100644
> --- a/drivers/iio/frequency/ad9910.c
> +++ b/drivers/iio/frequency/ad9910.c
> @@ -1288,12 +1610,26 @@ static int ad9910_write_raw(struct iio_dev *indio_dev,
> return ad9910_reg32_update(st, AD9910_REG_DRG_RATE,
> AD9910_DRG_RATE_DEC_MSK,
> tmp32, true);
> + case AD9910_CHANNEL_RAM:
> + if (!AD9910_RAM_ENABLED(st)) {
> + FIELD_MODIFY(AD9910_PROFILE_RAM_STEP_RATE_MSK,
> + &st->reg_profile[st->profile], tmp32);
> + return 0;
> + }
> +
> + tmp64 = FIELD_PREP(AD9910_PROFILE_RAM_STEP_RATE_MSK, tmp32);
> + return ad9910_reg64_update(st, AD9910_REG_PROFILE(st->profile),
> + AD9910_PROFILE_RAM_STEP_RATE_MSK,
> + tmp64, true);
> +
> default:
> return -EINVAL;
> }
> default:
> return -EINVAL;
> }
> +
> + return ret;
Seems a bit odd if you can now get here. Probably means some return missing
that would make more sense than a break somewhere above this.
> }
> +
> static const struct iio_info ad9910_info = {
> .read_raw = ad9910_read_raw,
> .write_raw = ad9910_write_raw,
> @@ -1503,6 +1922,13 @@ static int ad9910_setup(struct ad9910_state *st, struct reset_control *dev_rst)
> if (ret)
> return ret;
>
> + for (int i = 0; i < AD9910_NUM_PROFILES; i++) {
> + st->reg_profile[i] = AD9910_PROFILE_RAM_OPEN_MSK;
Add a definition for maximum value and explicitly write that via FIELD_PREP()
as that will make it easier to see what is going on here.
> + st->reg_profile[i] |= FIELD_PREP(AD9910_PROFILE_RAM_STEP_RATE_MSK, 1);
> + st->reg_profile[i] |= FIELD_PREP(AD9910_PROFILE_RAM_END_ADDR_MSK,
> + AD9910_RAM_ADDR_MAX);
> + }
> @@ -1519,6 +1947,24 @@ static void ad9910_release(void *data)
> true);
> }
>
> +static inline void ad9910_debugfs_init(struct ad9910_state *st,
> + struct iio_dev *indio_dev)
> +{
> +#ifdef CONFIG_DEBUG_FS
Why? There are stubs for debugfs_create_symlink() and the compiler
should tidyup the rest if that's stubbed out.
Whether this interfaces makes sense is a question I'll leave for ABI docs.
> + char buf[64];
> +
> + /*
> + * symlinks are created here so iio userspace tools can refer to them
> + * as debug attributes.
> + */
> + snprintf(buf, sizeof(buf), "/sys/class/firmware/%s/loading", st->ram_fwu_name);
> + debugfs_create_symlink("ram_loading", iio_get_debugfs_dentry(indio_dev), buf);
> +
> + snprintf(buf, sizeof(buf), "/sys/class/firmware/%s/data", st->ram_fwu_name);
> + debugfs_create_symlink("ram_data", iio_get_debugfs_dentry(indio_dev), buf);
> +#endif
> +}
>
^ permalink raw reply
* Re: [PATCH RFC v2 3/9] iio: frequency: ad9910: add simple parallel port mode support
From: Jonathan Cameron @ 2026-03-22 16:52 UTC (permalink / raw)
To: Andy Shevchenko
Cc: rodrigo.alencar, linux-iio, devicetree, linux-kernel, linux-doc,
Lars-Peter Clausen, Michael Hennerich, David Lechner,
Andy Shevchenko, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Philipp Zabel, Jonathan Corbet, Shuah Khan
In-Reply-To: <abru0mNtpJSPSJux@ashevche-desk.local>
On Wed, 18 Mar 2026 20:28:34 +0200
Andy Shevchenko <andriy.shevchenko@intel.com> wrote:
> On Wed, Mar 18, 2026 at 05:56:03PM +0000, Rodrigo Alencar via B4 Relay wrote:
>
> > Add parallel port channel with frequency scale, frequency offset, phase
> > offset, and amplitude offset extended attributes for configuring the
> > parallel data path.
>
> ...
>
> > + ret = iio_str_to_fixpoint(buf, MICRO / 10, &val, &val2);
>
> I think here we just use 100000 as it's in so many drivers de facto use.
> ideally this should be fixed on API level.
I wouldn't mind a series tidying this up, but if anyone proposes to do
that we'll want to not use the same naming so it is obvious if any
new drivers assume the old scaling.
I can't really remember why we ended up with the odd interface :(
Jonathan
^ permalink raw reply
* Re: [PATCH RFC v2 2/9] iio: frequency: ad9910: initial driver implementation
From: Jonathan Cameron @ 2026-03-22 16:50 UTC (permalink / raw)
To: Rodrigo Alencar via B4 Relay
Cc: rodrigo.alencar, linux-iio, devicetree, linux-kernel, linux-doc,
Lars-Peter Clausen, Michael Hennerich, David Lechner,
Andy Shevchenko, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Philipp Zabel, Jonathan Corbet, Shuah Khan
In-Reply-To: <20260318-ad9910-iio-driver-v2-2-e79f93becf11@analog.com>
On Wed, 18 Mar 2026 17:56:02 +0000
Rodrigo Alencar via B4 Relay <devnull+rodrigo.alencar.analog.com@kernel.org> wrote:
> From: Rodrigo Alencar <rodrigo.alencar@analog.com>
>
> Add the core AD9910 DDS driver infrastructure with single tone mode
> support. This includes SPI register access, profile management via GPIO
> pins, PLL/DAC configuration from firmware properties, and single tone
> frequency/phase/amplitude control through IIO attributes.
>
> Signed-off-by: Rodrigo Alencar <rodrigo.alencar@analog.com>
Hi Rodrigo
I want some time for the discussion on the ABI to take place and haven't
made any real comments on that here. Focus was more on the code.
So various minor things inline,
Thanks,
Jonathan
> diff --git a/drivers/iio/frequency/Kconfig b/drivers/iio/frequency/Kconfig
> index 583cbdf4e8cd..180e74f62d11 100644
> --- a/drivers/iio/frequency/Kconfig
> +++ b/drivers/iio/frequency/Kconfig
> diff --git a/drivers/iio/frequency/ad9910.c b/drivers/iio/frequency/ad9910.c
> new file mode 100644
> index 000000000000..a362d96cf651
> --- /dev/null
> +++ b/drivers/iio/frequency/ad9910.c
> @@ -0,0 +1,1006 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * AD9910 SPI DDS (Direct Digital Synthesizer) driver
> + *
> + * Copyright 2026 Analog Devices Inc.
> + */
> +
> +#include <linux/array_size.h>
> +#include <linux/bitfield.h>
> +#include <linux/clk.h>
> +#include <linux/delay.h>
> +#include <linux/device.h>
Generally can avoid including device.h in favour of more specific
headers. There are a few exceptions where we can't such as actual
dereferencing of struct device, but I don't recall seeing a case in here.
> +
> +#define AD9910_ASF_MAX (BIT(14) - 1)
> +#define AD9910_POW_MAX (BIT(16) - 1)
GENMASK() tends to be clearer for max values.
> +
> +static const char * const ad9910_power_supplies[] = {
> + "dvdd-io33", "avdd33", "dvdd18", "avdd18",
> +};
> +
> +static const char * const ad9910_refclk_out_drv0[] = {
> + "disabled", "low", "medium", "high",
> +};
These are only used in very localized bits of code. I'd move them down near
them rather than having them up here at the top of the file.
> +static int ad9910_profile_set(struct ad9910_state *st, u8 profile)
> +{
> + DECLARE_BITMAP(values, BITS_PER_TYPE(profile));
> +
> + if (profile >= AD9910_NUM_PROFILES)
> + return -EINVAL;
> +
> + st->profile = profile;
> + values[0] = profile;
> + gpiod_multi_set_value_cansleep(st->gpio_profile, values);
Trivial but blank line here. Having one before a simple return statement
just makes it a little easier to read.
> + return 0;
> +}
>
> +
> +#define AD9910_EXT_INFO(_name, _ident, _shared) { \
> + .name = _name, \
> + .read = ad9910_ext_info_read, \
> + .write = ad9910_ext_info_write, \
> + .private = _ident, \
> + .shared = _shared, \
If there are only a few of these, I'd put it long hand rather than
using a macro. Tends to end up easier to read.
> +}
> +
> +static const struct iio_chan_spec_ext_info ad9910_phy_ext_info[] = {
> + AD9910_EXT_INFO("profile", AD9910_PROFILE, IIO_SEPARATE),
> + AD9910_EXT_INFO("powerdown", AD9910_POWERDOWN, IIO_SEPARATE),
> + { }
> +};
> +static int ad9910_read_raw(struct iio_dev *indio_dev,
> + struct iio_chan_spec const *chan,
> + int *val, int *val2, long info)
> +{
> + struct ad9910_state *st = iio_priv(indio_dev);
> + u64 tmp64;
> + u32 tmp32;
> +
> + guard(mutex)(&st->lock);
> +
> + switch (info) {
> + case IIO_CHAN_INFO_FREQUENCY:
> + switch (chan->channel) {
> + case AD9910_CHANNEL_SINGLE_TONE:
I haven't read on yet, but if you never have any other cases in here,
perhaps us an if() as it will reduce indent of the code that follows.
> + tmp32 = FIELD_GET(AD9910_PROFILE_ST_FTW_MSK,
> + st->reg[AD9910_REG_PROFILE(st->profile)].val64);
> + break;
> + default:
> + return -EINVAL;
> + }
> + tmp64 = (u64)tmp32 * st->data.sysclk_freq_hz;
> + *val = upper_32_bits(tmp64);
> + *val2 = upper_32_bits((u64)lower_32_bits(tmp64) * MICRO);
I've no idea how this *val2 assignment works... Perhaps some comments?
> + return IIO_VAL_INT_PLUS_MICRO;
> + case IIO_CHAN_INFO_PHASE:
> + switch (chan->channel) {
> + case AD9910_CHANNEL_SINGLE_TONE:
> + tmp64 = FIELD_GET(AD9910_PROFILE_ST_POW_MSK,
> +
> +static int ad9910_reg_access(struct iio_dev *indio_dev,
> + unsigned int reg,
> + unsigned int writeval,
> + unsigned int *readval)
> +{
> + struct ad9910_state *st = iio_priv(indio_dev);
> + int ret;
> + u64 tmp64;
> + u32 tmp32;
> + u16 tmp16;
> + bool high32 = FIELD_GET(AD9910_REG_HIGH32_FLAG_MSK, reg);
> +
> + /*
> + * HIGH32 flag is a workaround to allow access to upper 32 bits of
> + * 64-bit registers one at a time due to debugfs_reg_access limitations
> + * of only supporting 32-bit values.
> + */
> + reg &= ~AD9910_REG_HIGH32_FLAG_MSK;
> + if (reg >= AD9910_REG_RAM)
> + return -EINVAL;
> +
> + guard(mutex)(&st->lock);
> +
> + switch (reg) {
Split this in to two helpers. It's rather hard to follow with read and
write paths mixed up in the code flow.
> + case AD9910_REG_DRG_LIMIT:
> + case AD9910_REG_DRG_STEP:
> + case AD9910_REG_PROFILE0:
Can you do
case AD9910_REG_PROFILE0 ... AD9910_REG_PROFILE7:
here to help readability?
> + case AD9910_REG_PROFILE1:
> + case AD9910_REG_PROFILE2:
> + case AD9910_REG_PROFILE3:
> + case AD9910_REG_PROFILE4:
> + case AD9910_REG_PROFILE5:
> + case AD9910_REG_PROFILE6:
> + case AD9910_REG_PROFILE7:
> + if (readval) {
> + ret = ad9910_reg64_read(st, reg, &tmp64);
> + if (ret < 0)
> + return ret;
> +
> + if (high32)
> + *readval = upper_32_bits(tmp64);
> + else
> + *readval = lower_32_bits(tmp64);
Might as well return 0 here.
> + } else {
> + tmp64 = st->reg[reg].val64;
> + if (high32)
> + FIELD_MODIFY(GENMASK_ULL(63, 32), &tmp64, writeval);
> + else
> + FIELD_MODIFY(GENMASK_ULL(31, 0), &tmp64, writeval);
> +
> + return ad9910_reg64_write(st, reg, tmp64, true);
> + }
> + break;
> + case AD9910_REG_POW:
> + if (!readval)
> + return ad9910_reg16_write(st, reg, writeval, true);
> +
> + ret = ad9910_reg16_read(st, reg, &tmp16);
> + if (ret < 0)
> + return ret;
> + *readval = tmp16;
> + break;
return 0;
> + default:
> + if (!readval)
> + return ad9910_reg32_write(st, reg, writeval, true);
> +
> + ret = ad9910_reg32_read(st, reg, &tmp32);
> + if (ret < 0)
> + return ret;
> + *readval = tmp32;
> + break;
and return 0; here. No point in break to go to as return and nothing else.
> + }
> +
> + return ret;
> +}
> +
> +static int ad9910_cfg_sysclk(struct ad9910_state *st, bool update)
> +{
> + u32 tmp32, cfr3 = AD9910_CFR3_OPEN_MSK;
> +
> + cfr3 |= AD9910_CFR3_VCO_SEL_MSK |
> + FIELD_PREP(AD9910_CFR3_DRV0_MSK, st->data.refclk_out_drv);
> +
> + if (st->data.pll_enabled) {
> + tmp32 = st->data.pll_charge_pump_current - AD9910_ICP_MIN_uA;
> + tmp32 = DIV_ROUND_CLOSEST(tmp32, AD9910_ICP_STEP_uA);
> + cfr3 |= FIELD_PREP(AD9910_CFR3_ICP_MSK, tmp32) |
> + AD9910_CFR3_PLL_EN_MSK;
> + } else {
> + cfr3 |= AD9910_CFR3_ICP_MSK |
For this, be explicit what value you are setting, probably be defining a max value
that the field can take. Whilst just setting the mask is the same it doesn't
convey the same meaning to someone reading the code.
> + AD9910_CFR3_REFCLK_DIV_RESETB_MSK |
> + AD9910_CFR3_PFD_RESET_MSK;
> + }
> + st->reg[AD9910_REG_CFR3].val32 = cfr3;
> +
> + return ad9910_set_sysclk_freq(st, AD9910_PLL_OUT_MAX_FREQ_HZ, update);
> +}
> +
> +static int ad9910_parse_fw(struct ad9910_state *st)
> +{
> + struct device *dev = &st->spi->dev;
> + u32 tmp;
> + int ret;
> +
> + st->data.pll_enabled = device_property_read_bool(dev, "adi,pll-enable");
> + if (st->data.pll_enabled) {
> + tmp = AD9910_ICP_MAX_uA;
Defaulting to max current seems unusual. What's the motivation? Normal instinct is
go minimum if no other info.
> + device_property_read_u32(dev, "adi,charge-pump-current-microamp", &tmp);
> + if (tmp < AD9910_ICP_MIN_uA || tmp > AD9910_ICP_MAX_uA)
> + return dev_err_probe(dev, -ERANGE,
> + "invalid charge pump current %u\n", tmp);
> + st->data.pll_charge_pump_current = tmp;
> +
> + st->data.refclk_out_drv = AD9910_REFCLK_OUT_DRV_DISABLED;
> + ret = device_property_match_property_string(dev,
> + "adi,refclk-out-drive-strength",
> + ad9910_refclk_out_drv0,
> + ARRAY_SIZE(ad9910_refclk_out_drv0));
> + if (ret >= 0)
> + st->data.refclk_out_drv = ret;
> + }
> +
> + tmp = AD9910_DAC_IOUT_DEFAULT_uA;
> + device_property_read_u32(dev, "adi,dac-output-current-microamp", &tmp);
> + if (tmp < AD9910_DAC_IOUT_MIN_uA || tmp > AD9910_DAC_IOUT_MAX_uA)
> + return dev_err_probe(dev, -ERANGE,
> + "Invalid DAC output current %u uA\n", tmp);
> + st->data.dac_output_current = tmp;
> +
> + return 0;
> +}
> +
> +static int ad9910_setup(struct ad9910_state *st, struct reset_control *dev_rst)
> +{
> + u32 reg32;
> + int ret;
> +
> + ret = reset_control_deassert(dev_rst);
> + if (ret)
> + return ret;
> +
> + reg32 = AD9910_CFR1_SDIO_INPUT_ONLY_MSK;
> + ret = ad9910_reg32_write(st, AD9910_REG_CFR1, reg32, false);
Trivial but I'd not bother using the local variable for simple values.
ret = ad9910_reg32_write(st, AD9910_REG_CFR1,
AD9910_CFR1_SDIO_INPUT_ONLY_MSK, false);
is fine.
> + if (ret)
> + return ret;
> +
> + reg32 = AD9910_CFR2_AMP_SCALE_SINGLE_TONE_MSK;
This split seems odd. Why not combine above and the next block?
> + reg32 |= AD9910_CFR2_SYNC_TIMING_VAL_DISABLE_MSK |
> + AD9910_CFR2_DRG_NO_DWELL_MSK |
> + AD9910_CFR2_DATA_ASM_HOLD_LAST_MSK |
> + AD9910_CFR2_SYNC_CLK_EN_MSK |
> + AD9910_CFR2_PDCLK_ENABLE_MSK;
> + ret = ad9910_reg32_write(st, AD9910_REG_CFR2, reg32, false);
> + if (ret)
> + return ret;
> +
> + ret = ad9910_cfg_sysclk(st, false);
> + if (ret)
> + return ret;
> +
> + ret = ad9910_set_dac_current(st, false);
> + if (ret)
> + return ret;
> +
> + return ad9910_io_update(st);
> +}
> +
> +static void ad9910_release(void *data)
> +{
> + struct ad9910_state *st = data;
> +
> + if (!ad9910_powerdown_set(st, true))
> + return;
> +
> + ad9910_reg32_update(st, AD9910_REG_CFR1,
> + AD9910_CFR1_SOFT_POWER_DOWN_MSK,
> + AD9910_CFR1_SOFT_POWER_DOWN_MSK,
> + true);
> +}
> +
> +static int ad9910_probe(struct spi_device *spi)
> +{
> + struct reset_control *dev_rst;
> + struct gpio_desc *io_rst_gpio;
> + struct device *dev = &spi->dev;
> + struct iio_dev *indio_dev;
> + struct ad9910_state *st;
> + int ret;
> +
> + indio_dev = devm_iio_device_alloc(dev, sizeof(*st));
> + if (!indio_dev)
> + return -ENOMEM;
> +
> + st = iio_priv(indio_dev);
> + st->spi = spi;
> +
> + st->refclk = devm_clk_get_enabled(dev, "ref_clk");
> + if (IS_ERR(st->refclk))
> + return dev_err_probe(dev, PTR_ERR(st->refclk),
> + "Failed to get reference clock\n");
> +
> + ret = devm_regulator_bulk_get_enable(dev,
> + ARRAY_SIZE(ad9910_power_supplies),
> + ad9910_power_supplies);
Maybe we can just call it ad9910_supplies without loss of meaning and have
slightly shorter lines? Could also drag that const array into scope of this
function perhaps as we only need it in here.
> + if (ret)
> + return dev_err_probe(dev, ret, "Failed to get regulators\n");
> +
> + ret = devm_mutex_init(dev, &st->lock);
> + if (ret)
> + return ret;
> +
> + indio_dev->name = "ad9910";
> + indio_dev->info = &ad9910_info;
> + indio_dev->modes = INDIO_DIRECT_MODE;
> + indio_dev->channels = ad9910_channels;
> + indio_dev->num_channels = ARRAY_SIZE(ad9910_channels);
> +
> + dev_rst = devm_reset_control_get_optional_exclusive(dev, NULL);
> + if (IS_ERR(dev_rst))
> + return dev_err_probe(dev, PTR_ERR(dev_rst),
> + "failed to get device reset control\n");
> +
> + ret = reset_control_assert(dev_rst);
Do we need this? I 'think' that the gpio reset controller will ensure that
the reset line is GPIOD_OUT_HIGH, on registering it.
https://elixir.bootlin.com/linux/v6.19.9/source/drivers/reset/reset-gpio.c#L81
and that should I think correspond to asserted.
I was curious why there were _deasserted() variants of the get but not
_asserted() ones and went looking. Seems assumption is that in general
should already be in asserted state as firmware deals with that (here the dt
binding / gpio reset driver).
> + if (ret)
> + return dev_err_probe(dev, ret,
> + "failed to assert device reset control\n");
...
> + ret = ad9910_parse_fw(st);
> + if (ret)
> + return ret;
> +
> + ret = ad9910_setup(st, dev_rst);
> + if (ret)
> + return dev_err_probe(dev, ret, "device setup failed\n");
> +
> + ret = devm_iio_device_register(dev, indio_dev);
> + if (ret)
> + return ret;
> +
> + return devm_add_action_or_reset(dev, ad9910_release, st);
What is this undoing? Very unusual to have a register write that you
want to happen 'before' the userspace interfaces go away in the remove()
path. Perhaps it is paired with ad9910_setup()?
> +}
>
^ permalink raw reply
* Re: [PATCH v4 0/4] kallsyms: embed source file:line info in kernel stack traces
From: Andrew Morton @ 2026-03-22 16:35 UTC (permalink / raw)
To: Sasha Levin
Cc: Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
James Bottomley, Jonathan Corbet, Nathan Chancellor,
Nicolas Schier, Petr Pavlu, Daniel Gomez, Greg KH, Petr Mladek,
Steven Rostedt, Kees Cook, Peter Zijlstra, Thorsten Leemhuis,
Vlastimil Babka, Helge Deller, Randy Dunlap, Laurent Pinchart,
Vivian Wang, linux-kernel, linux-kbuild, linux-modules, linux-doc
In-Reply-To: <20260322131543.971079-1-sashal@kernel.org>
On Sun, 22 Mar 2026 09:15:39 -0400 Sasha Levin <sashal@kernel.org> wrote:
> This series adds CONFIG_KALLSYMS_LINEINFO, which embeds source file:line
> information directly in the kernel image so that stack traces annotate
> every frame with the originating source location - no external tools, no
> debug symbols at runtime, and safe to use in NMI/panic context.
Sashiko review hasn't completed yet, but it has things to say:
https://sashiko.dev/#/patchset/20260322131543.971079-1-sashal@kernel.org
^ permalink raw reply
* Re: [PATCH] docs: add advanced search for kernel documentation
From: Jonathan Corbet @ 2026-03-22 16:01 UTC (permalink / raw)
To: Rito Rhymes, Mauro Carvalho Chehab, linux-doc
Cc: Shuah Khan, linux-kernel, Rito Rhymes
In-Reply-To: <20260321181511.11706-1-rito@ritovision.com>
Rito Rhymes <rito@ritovision.com> writes:
> Replace the stock Sphinx search page with one that reuses the
> existing searchindex.js while adding structured result grouping,
> filtering, and exact identifier matching.
>
> Results are grouped into Symbols, Sections, Index entries, and
> Pages, each in a collapsible section with a count. An Advanced
> panel exposes filters for documentation area, object type, result
> kind, and exact match mode. All state is URL-encoded so searches
> remain shareable.
>
> Page summary snippets are lazy-loaded via IntersectionObserver to
> avoid fetching every matching page up front.
>
> The sidebar keeps the existing quick-search box and adds an
> "Advanced search" link below it.
>
> Signed-off-by: Rito Rhymes <rito@ritovision.com>
> ---
> Documentation/sphinx-static/custom.css | 163 ++++
> Documentation/sphinx-static/kernel-search.js | 746 ++++++++++++++++++
> Documentation/sphinx/templates/search.html | 106 +++
> Documentation/sphinx/templates/searchbox.html | 18 +
> 4 files changed, 1033 insertions(+)
> create mode 100644 Documentation/sphinx-static/kernel-search.js
> create mode 100644 Documentation/sphinx/templates/search.html
> create mode 100644 Documentation/sphinx/templates/searchbox.html
Without looking into detail at the work (yet), can you tell me something
about how this work was created? We do have guidance for the disclosure
of the use of coding tools in
Documentation/process/coding-assistants.rst ...
I'm curious about where you are going with this in general. A look at
ritovision.com does not suggest "kernel developer" to me.
Thanks,
jon
^ permalink raw reply
* [RFC PATCH v4 04/10] Docs/admin-guide/mm/damon/usage: update for pause file
From: SeongJae Park @ 2026-03-22 15:57 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: <20260322155728.81434-1-sj@kernel.org>
Update DAMON usage document for the DAMON context execution pause/resume
feature.
Signed-off-by: SeongJae Park <sj@kernel.org>
---
Documentation/admin-guide/mm/damon/usage.rst | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/Documentation/admin-guide/mm/damon/usage.rst b/Documentation/admin-guide/mm/damon/usage.rst
index 534e1199cf091..bfdb717441f05 100644
--- a/Documentation/admin-guide/mm/damon/usage.rst
+++ b/Documentation/admin-guide/mm/damon/usage.rst
@@ -66,7 +66,8 @@ comma (",").
│ :ref:`kdamonds <sysfs_kdamonds>`/nr_kdamonds
│ │ :ref:`0 <sysfs_kdamond>`/state,pid,refresh_ms
│ │ │ :ref:`contexts <sysfs_contexts>`/nr_contexts
- │ │ │ │ :ref:`0 <sysfs_context>`/avail_operations,operations,addr_unit
+ │ │ │ │ :ref:`0 <sysfs_context>`/avail_operations,operations,addr_unit,
+ │ │ │ │ pause
│ │ │ │ │ :ref:`monitoring_attrs <sysfs_monitoring_attrs>`/
│ │ │ │ │ │ intervals/sample_us,aggr_us,update_us
│ │ │ │ │ │ │ intervals_goal/access_bp,aggrs,min_sample_us,max_sample_us
@@ -194,9 +195,9 @@ details). At the moment, only one context per kdamond is supported, so only
contexts/<N>/
-------------
-In each context directory, three files (``avail_operations``, ``operations``
-and ``addr_unit``) and three directories (``monitoring_attrs``, ``targets``,
-and ``schemes``) exist.
+In each context directory, four files (``avail_operations``, ``operations``,
+``addr_unit`` and ``pause``) and three directories (``monitoring_attrs``,
+``targets``, and ``schemes``) exist.
DAMON supports multiple types of :ref:`monitoring operations
<damon_design_configurable_operations_set>`, including those for virtual address
@@ -214,6 +215,9 @@ reading from the ``operations`` file.
``addr_unit`` file is for setting and getting the :ref:`address unit
<damon_design_addr_unit>` parameter of the operations set.
+``pause`` file is for setting and getting the :ref:`pause request
+<damon_design_execution_model_and_data_structures>` parameter of the context.
+
.. _sysfs_monitoring_attrs:
contexts/<N>/monitoring_attrs/
--
2.47.3
^ permalink raw reply related
* [RFC PATCH v4 03/10] Docs/mm/damon/design: update for context pause/resume feature
From: SeongJae Park @ 2026-03-22 15:57 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: <20260322155728.81434-1-sj@kernel.org>
Update DAMON design document for the context execution pause/resume
feature.
Signed-off-by: SeongJae Park <sj@kernel.org>
---
Documentation/mm/damon/design.rst | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/Documentation/mm/damon/design.rst b/Documentation/mm/damon/design.rst
index afc7d52bda2f7..510ec6375178d 100644
--- a/Documentation/mm/damon/design.rst
+++ b/Documentation/mm/damon/design.rst
@@ -19,6 +19,13 @@ types of monitoring.
To know how user-space can do the configurations and start/stop DAMON, refer to
:ref:`DAMON sysfs interface <sysfs_interface>` documentation.
+Users can also request each context execution to be paused and resumed. When
+it is paused, the kdamond does nothing other than applying online parameter
+update.
+
+To know how user-space can pause/resume each context, refer to :ref:`DAMON
+sysfs context <sysfs_context>` usage documentation.
+
Overall Architecture
====================
--
2.47.3
^ permalink raw reply related
* [RFC PATCH v4 00/10] mm/damon: let DAMON be paused and resumed
From: SeongJae Park @ 2026-03-22 15:57 UTC (permalink / raw)
Cc: SeongJae Park, Liam R. Howlett, Andrew Morton, Brendan Higgins,
David Gow, David Hildenbrand, Jonathan Corbet, Lorenzo Stoakes,
Michal Hocko, Mike Rapoport, Shuah Khan, Shuah Khan,
Suren Baghdasaryan, Vlastimil Babka, damon, kunit-dev, linux-doc,
linux-kernel, linux-kselftest, linux-mm
DAMON utilizes a few mechanisms that enhance itself over time. Adaptive
regions adjustment, goal-based DAMOS quota auto-tuning and monitoring
intervals auto-tuning like self-training mechanisms are such examples.
It also adds access frequency stability information (age) to the
monitoring results, which makes it enhanced over time.
Sometimes users have to stop DAMON. In this case, DAMON internal state
that enhanced over the time of the last execution simply goes away.
Restarted DAMON have to train itself and enhance its output from the
scratch. This makes DAMON less useful in such cases. Introducing three
such use cases below.
Investigation of DAMON. It is best to do the investigation online,
especially when it is a production environment. DAMON therefore
provides features for such online investigations, including DAMOS stats,
monitoring result snapshot exposure, and multiple tracepoints. When
those are insufficient, and there are additional clues that could be
interfered by DAMON, users have to temporarily stop DAMON to collect the
additional clues. It is not very useful since many of DAMON internal
clues are gone when DAMON is stopped. The loss of the monitoring
results that improved over time is also problematic, especially in
production environments.
Monitoring of workloads that have different user-known phases. For
example, in Android, applications are known to have very different
access patterns and behaviors when they are running on the foreground
and the background. It can therefore be useful to separate monitoring
of apps based on whether they are running on the foreground and on the
background. Having two DAMON threads per application that paused and
resumed for the apps foreground/background switches can be useful for
the purpose. But such pause/resume of the execution is not supported.
Tests of DAMON. A few DAMON selftests are using drgn to dump the
internal DAMON status. The tests show if the dumped status is the same
as what the test code expected. Because DAMON keeps running and
modifying its internal status, there are chances of data races that can
cause false test results. Stopping DAMON can avoid the race. But,
since the internal state of DAMON is dropped, the test coverage will be
limited.
Let DAMON execution be paused and resumed without loss of the internal
state, to overhaul the limitations. For this, introduce a new DAMON
context parameter, namely 'pause'. API callers can update it while the
context is running, using the online parameters update functions
(damon_commit_ctx() and damon_call()). Once it is set, kdamond_fn()
main loop will do only limited works excluding the monitoring and DAMOS
works, while sleeping sampling intervals per the work. The limited
works include handling of the online parameters update. Hence users can
unset the 'pause' parameter again. Once it is unset, kdamond_fn() main
loop will do all the work again (resumed). Under the paused state, it
also does stop condition checks and handling of it, so that paused DAMON
can also be stopped if needed. Expose the feature to the user space via
DAMON sysfs interface. Also, update existing drgn-based tests to test
and use the feature.
Tests
=====
I confirmed the feature functionality using real time tracing ('perf
trace' or 'trace-cmd stream') of damon:damon_aggregated DAMON
tracepoint. By pausing and resuming the DAMON execution, I was able to
see the trace stops and continued as expected. Note that the pause
feature support is added to DAMON user-space tool (damo) after v3.1.9.
Users can use '--pause_ctx' command line option of damo for that, and I
actually used it for my test. The extended drgn-based selftests are
also testing a part of the functionality.
Patches Sequence
================
Patch 1 introduces the new core API for the pause feature. Patch 2
extend DAMON sysfs interface for the new parameter. Patches 3-5 update
design, usage and ABI documents for the new sysfs file, respectively.
The following five patches are for tests. Patch 6 implements a new
kunit test for the pause parameter online commitment. Patches 7 and 8
extend DAMON selftest helpers to support the new feature. Patch 9
extends selftest to test the commitment of the feature. Finally, patch
10 updates existing selftest to be safe from the race condition using
the pause/resume feature.
Changelog
=========
Changes from v1 (or, RFC v3)
(https://lore.kernel.org/20260321181343.93971-1-sj@kernel.org)
- Add RFC tag again.
- Handle maybe_corrupted inside pause-loop.
- Reduce unnecessary commits in sysfs.py selftest.
Changes from RFC v2
(https://lore.kernel.org/20260319052157.99433-1-sj@kernel.org)
- Move damon_ctx->pause to public fields section.
- Wordsmith design doc change.
- Fix unintended resume of contexts in multiple contexts use case.
- Rebase to latest mm-new.
Changes from RFC v1
(https://lore.kernel.org/20260315210012.94846-1-sj@kernel.org)
- Continuously cancel new damos_walk() requests when paused.
- Initialize damon_sysfs_context->pause.
- Make sysfs.py dump-purpose pausing to work for all contexts.
SeongJae Park (10):
mm/damon/core: introduce damon_ctx->paused
mm/damon/sysfs: add pause file under context dir
Docs/mm/damon/design: update for context pause/resume feature
Docs/admin-guide/mm/damon/usage: update for pause file
Docs/ABI/damon: update for pause sysfs file
mm/damon/tests/core-kunit: test pause commitment
selftests/damon/_damon_sysfs: support pause file staging
selftests/damon/drgn_dump_damon_status: dump pause
selftests/damon/sysfs.py: check pause on assert_ctx_committed()
selftets/damon/sysfs.py: pause DAMON before dumping status
.../ABI/testing/sysfs-kernel-mm-damon | 7 ++++
Documentation/admin-guide/mm/damon/usage.rst | 12 ++++--
Documentation/mm/damon/design.rst | 7 ++++
include/linux/damon.h | 2 +
mm/damon/core.c | 9 +++++
mm/damon/sysfs.c | 31 +++++++++++++++
mm/damon/tests/core-kunit.h | 4 ++
tools/testing/selftests/damon/_damon_sysfs.py | 10 ++++-
.../selftests/damon/drgn_dump_damon_status.py | 1 +
tools/testing/selftests/damon/sysfs.py | 39 +++++++++++++++++++
10 files changed, 117 insertions(+), 5 deletions(-)
base-commit: 73b971e012fbe1b2e8cd4992602898d5c9633ca4
--
2.47.3
^ permalink raw reply
* Re: [RFC PATCH 1/1] leds: Introduce the multi_max_intensity sysfs attribute
From: Jacek Anaszewski @ 2026-03-22 15:53 UTC (permalink / raw)
To: Armin Wolf, lee, pavel
Cc: corbet, skhan, linux-leds, linux-doc, linux-kernel, wse, pobrn
In-Reply-To: <20260314230107.65444-2-W_Armin@gmx.de>
Hi Armin,
On 3/15/26 12:01 AM, Armin Wolf wrote:
> Some multicolor LEDs support global brightness control in hardware,
> meaning that the maximum intensity of the color components is not
> connected to the maximum global brightness. Such LEDs cannot be
> described properly by the current multicolor LED class interface,
> because it assumes that the maximum intensity of each color component
> is described by the maximum global brightness of the LED.
>
> Fix this by introducing a new sysfs attribute called
> "multi_max_intensity" holding the maximum intensity values for the
> color components of a multicolor LED class device. Drivers can use
> the new max_intensity field inside struct mc_subled to tell the
> multicolor LED class code about those values. Intensity values written
> by userspace applications will be limited to this maximum value.
>
> Drivers for multicolor LEDs that do not support global brightness
> control in hardware might still want to use the maximum global LED
> brightness supplied via devicetree as the maximum intensity of each
> individual color component. Such drivers should set max_intensity
> to LED_USE_MAX_BRIGHTNESS so that the multicolor LED core can act
Since LED_USE_MAX_BRIGHTNESS value is 0, then I propose not to touch
the drivers which we want to stick to the default max_intensity,
unless they don't initialize their mc_subled structs to 0, or don't
use struct initialization list, which in case the property is omitted,
initializes it to 0.
> accordingly.
>
> Compile-tested only.
>
> Signed-off-by: Armin Wolf <W_Armin@gmx.de>
> ---
> .../ABI/testing/sysfs-class-led-multicolor | 16 +++++++--
> Documentation/leds/leds-class-multicolor.rst | 21 ++++++++++-
> drivers/hid/hid-lg-g15.c | 3 ++
> drivers/hid/hid-playstation.c | 3 ++
> drivers/leds/flash/leds-mt6360.c | 8 ++---
> drivers/leds/led-class-multicolor.c | 36 +++++++++++++++++--
> drivers/leds/leds-blinkm.c | 3 ++
> drivers/leds/leds-cros_ec.c | 1 +
> drivers/leds/leds-lp50xx.c | 1 +
> drivers/leds/leds-lp55xx-common.c | 7 ++--
> drivers/leds/leds-max77705.c | 1 +
> drivers/leds/leds-sun50i-a100.c | 3 ++
> drivers/leds/leds-turris-omnia.c | 1 +
> drivers/leds/rgb/leds-group-multicolor.c | 1 +
> drivers/leds/rgb/leds-ktd202x.c | 1 +
> drivers/leds/rgb/leds-lp5812.c | 1 +
> drivers/leds/rgb/leds-mt6370-rgb.c | 8 ++---
> drivers/leds/rgb/leds-ncp5623.c | 4 +--
> drivers/leds/rgb/leds-pwm-multicolor.c | 1 +
> drivers/leds/rgb/leds-qcom-lpg.c | 1 +
> drivers/platform/x86/silicom-platform.c | 30 ++++++++++++++++
> drivers/platform/x86/uniwill/uniwill-acpi.c | 4 +--
> include/linux/led-class-multicolor.h | 32 ++++++++++++++++-
> 23 files changed, 164 insertions(+), 23 deletions(-)
>
> diff --git a/Documentation/ABI/testing/sysfs-class-led-multicolor b/Documentation/ABI/testing/sysfs-class-led-multicolor
> index 16fc827b10cb..fbd95c5003e2 100644
> --- a/Documentation/ABI/testing/sysfs-class-led-multicolor
> +++ b/Documentation/ABI/testing/sysfs-class-led-multicolor
> @@ -17,8 +17,20 @@ KernelVersion: 5.9
> Contact: Dan Murphy <dmurphy@ti.com>
> Description: read/write
> This file contains array of integers. Order of components is
> - described by the multi_index array. The maximum intensity should
> - not exceed /sys/class/leds/<led>/max_brightness.
> + described by the multi_index array.
> + For additional details please refer to
> + Documentation/leds/leds-class-multicolor.rst.
Why not to refer to the newly introduced multi_max_intensity file here
instead?
> +
> +What: /sys/class/leds/<led>/multi_max_intensity
> +Date: March 2026
> +KernelVersion: 7.1
> +Contact: Armin Wolf <W_Armin@gmx.de>
> +Description: read
> + This file contains an array of integers describing the maximum
> + intensity value for each intensity component. Writing intensity
> + values larger than the maximum value of a given component will
> + result in those values being clamped.
>
> For additional details please refer to
> Documentation/leds/leds-class-multicolor.rst.
> diff --git a/Documentation/leds/leds-class-multicolor.rst b/Documentation/leds/leds-class-multicolor.rst
> index c6b47b4093c4..8f42f10078ad 100644
> --- a/Documentation/leds/leds-class-multicolor.rst
> +++ b/Documentation/leds/leds-class-multicolor.rst
> @@ -25,10 +25,14 @@ color name to indexed value.
> The ``multi_index`` file is an array that contains the string list of the colors as
> they are defined in each ``multi_*`` array file.
>
> -The ``multi_intensity`` is an array that can be read or written to for the
> +The ``multi_intensity`` file is an array that can be read or written to for the
> individual color intensities. All elements within this array must be written in
> order for the color LED intensities to be updated.
>
> +The ``multi_max_intensity`` file is an array that contains the maximum intensity
> +value supported by each color intensity. Intensity values above this will be
> +automatically clamped into the supported range.
> +
> Directory Layout Example
> ========================
> .. code-block:: console
> @@ -38,6 +42,7 @@ Directory Layout Example
> -r--r--r-- 1 root root 4096 Oct 19 16:16 max_brightness
> -r--r--r-- 1 root root 4096 Oct 19 16:16 multi_index
> -rw-r--r-- 1 root root 4096 Oct 19 16:16 multi_intensity
> + -r--r--r-- 1 root root 4096 OCt 19 16:16 multi_max_intensity
>
> ..
>
> @@ -104,3 +109,17 @@ the color LED group.
> 128
>
> ..
> +
> +Writing intensity values larger than the maximum specified in ``multi_max_intensity``
> +will result in those values being clamped into the supported range.
> +
> +.. code-block:: console
> +
> + # cat /sys/class/leds/multicolor:status/multi_max_intensity
> + 255 255 255
> +
> + # echo 512 512 512 > /sys/class/leds/multicolor:status/multi_intensity
> + # cat /sys/class/leds/multicolor:status/multi_intensity
> + 255 255 255
> +
> +..
> diff --git a/drivers/hid/hid-lg-g15.c b/drivers/hid/hid-lg-g15.c
> index 1a88bc44ada4..227f50b21c06 100644
> --- a/drivers/hid/hid-lg-g15.c
> +++ b/drivers/hid/hid-lg-g15.c
> @@ -975,14 +975,17 @@ static void lg_g15_setup_led_rgb(struct lg_g15_data *g15, int index)
> case LED_COLOR_ID_RED:
> subled_info[i].color_index = LED_COLOR_ID_RED;
> subled_info[i].intensity = gled->red;
> + subled_info[i].max_intensity = LED_USE_MAX_BRIGHTNESS;
> break;
> case LED_COLOR_ID_GREEN:
> subled_info[i].color_index = LED_COLOR_ID_GREEN;
> subled_info[i].intensity = gled->green;
> + subled_info[i].max_intensity = LED_USE_MAX_BRIGHTNESS;
> break;
> case LED_COLOR_ID_BLUE:
> subled_info[i].color_index = LED_COLOR_ID_BLUE;
> subled_info[i].intensity = gled->blue;
> + subled_info[i].max_intensity = LED_USE_MAX_BRIGHTNESS;
> break;
> }
> subled_info[i].channel = i;
> diff --git a/drivers/hid/hid-playstation.c b/drivers/hid/hid-playstation.c
> index 3c0db8f93c82..a6de4dd49750 100644
> --- a/drivers/hid/hid-playstation.c
> +++ b/drivers/hid/hid-playstation.c
> @@ -874,8 +874,11 @@ static int ps_lightbar_register(struct ps_device *ps_dev, struct led_classdev_mc
> return -ENOMEM;
>
> mc_led_info[0].color_index = LED_COLOR_ID_RED;
> + mc_led_info[0].max_intensity = LED_USE_MAX_BRIGHTNESS;
> mc_led_info[1].color_index = LED_COLOR_ID_GREEN;
> + mc_led_info[1].max_intensity = LED_USE_MAX_BRIGHTNESS;
> mc_led_info[2].color_index = LED_COLOR_ID_BLUE;
> + mc_led_info[2].max_intensity = LED_USE_MAX_BRIGHTNESS;
>
> lightbar_mc_dev->subled_info = mc_led_info;
> lightbar_mc_dev->num_colors = 3;
> diff --git a/drivers/leds/flash/leds-mt6360.c b/drivers/leds/flash/leds-mt6360.c
> index 462a902f54e0..f0331cbec6aa 100644
> --- a/drivers/leds/flash/leds-mt6360.c
> +++ b/drivers/leds/flash/leds-mt6360.c
> @@ -100,7 +100,7 @@ static int mt6360_mc_brightness_set(struct led_classdev *lcdev,
> struct led_classdev_mc *mccdev = lcdev_to_mccdev(lcdev);
> struct mt6360_led *led = container_of(mccdev, struct mt6360_led, mc);
> struct mt6360_priv *priv = led->priv;
> - u32 real_bright, enable_mask = 0, enable = 0;
> + u32 enable_mask = 0, enable = 0;
> int i, ret;
>
> mutex_lock(&priv->lock);
> @@ -110,14 +110,13 @@ static int mt6360_mc_brightness_set(struct led_classdev *lcdev,
> for (i = 0; i < mccdev->num_colors; i++) {
> struct mc_subled *subled = mccdev->subled_info + i;
>
> - real_bright = min(lcdev->max_brightness, subled->brightness);
> ret = regmap_update_bits(priv->regmap, MT6360_REG_ISNK(i),
> - MT6360_ISNK_MASK, real_bright);
> + MT6360_ISNK_MASK, subled->brightness);
> if (ret)
> goto out;
>
> enable_mask |= MT6360_ISNK_ENMASK(subled->channel);
> - if (real_bright)
> + if (subled->brightness)
> enable |= MT6360_ISNK_ENMASK(subled->channel);
> }
>
> @@ -660,6 +659,7 @@ static int mt6360_init_isnk_properties(struct mt6360_led *led,
> priv->leds_active |= BIT(reg);
> sub_led[num_color].color_index = color;
> sub_led[num_color].channel = reg;
> + sub_led[num_color].max_intensity = LED_USE_MAX_BRIGHTNESS;
> num_color++;
> }
>
> diff --git a/drivers/leds/led-class-multicolor.c b/drivers/leds/led-class-multicolor.c
> index 6b671f3f9c61..72323e825331 100644
> --- a/drivers/leds/led-class-multicolor.c
> +++ b/drivers/leds/led-class-multicolor.c
> @@ -7,6 +7,7 @@
> #include <linux/init.h>
> #include <linux/led-class-multicolor.h>
> #include <linux/math.h>
> +#include <linux/minmax.h>
> #include <linux/module.h>
> #include <linux/slab.h>
> #include <linux/uaccess.h>
> @@ -27,6 +28,30 @@ int led_mc_calc_color_components(struct led_classdev_mc *mcled_cdev,
> }
> EXPORT_SYMBOL_GPL(led_mc_calc_color_components);
>
> +static ssize_t multi_max_intensity_show(struct device *dev,
> + struct device_attribute *intensity_attr, char *buf)
> +{
> + struct led_classdev *led_cdev = dev_get_drvdata(dev);
> + struct led_classdev_mc *mcled_cdev = lcdev_to_mccdev(led_cdev);
> + unsigned int max_intensity;
> + int len = 0;
> + int i;
> +
> + for (i = 0; i < mcled_cdev->num_colors; i++) {
> + max_intensity = mcled_cdev->subled_info[i].max_intensity;
> + if (max_intensity == LED_USE_MAX_BRIGHTNESS)
> + max_intensity = led_cdev->max_brightness;
> +
> + len += sprintf(buf + len, "%u", max_intensity);
> + if (i < mcled_cdev->num_colors - 1)
> + len += sprintf(buf + len, " ");
> + }
> +
> + buf[len++] = '\n';
> + return len;
> +}
> +static DEVICE_ATTR_RO(multi_max_intensity);
> +
> static ssize_t multi_intensity_store(struct device *dev,
> struct device_attribute *intensity_attr,
> const char *buf, size_t size)
> @@ -35,6 +60,7 @@ static ssize_t multi_intensity_store(struct device *dev,
> struct led_classdev_mc *mcled_cdev = lcdev_to_mccdev(led_cdev);
> int nrchars, offset = 0;
> unsigned int intensity_value[LED_COLOR_ID_MAX];
> + unsigned int max_intensity;
> int i;
> ssize_t ret;
>
> @@ -56,8 +82,13 @@ static ssize_t multi_intensity_store(struct device *dev,
> goto err_out;
> }
>
> - for (i = 0; i < mcled_cdev->num_colors; i++)
> - mcled_cdev->subled_info[i].intensity = intensity_value[i];
> + for (i = 0; i < mcled_cdev->num_colors; i++) {
> + max_intensity = mcled_cdev->subled_info[i].max_intensity;
> + if (max_intensity == LED_USE_MAX_BRIGHTNESS)
> + max_intensity = led_cdev->max_brightness;
> +
> + mcled_cdev->subled_info[i].intensity = min(intensity_value[i], max_intensity);
> + }
>
> if (!test_bit(LED_BLINK_SW, &led_cdev->work_flags))
> led_set_brightness(led_cdev, led_cdev->brightness);
> @@ -111,6 +142,7 @@ static ssize_t multi_index_show(struct device *dev,
> static DEVICE_ATTR_RO(multi_index);
>
> static struct attribute *led_multicolor_attrs[] = {
> + &dev_attr_multi_max_intensity.attr,
> &dev_attr_multi_intensity.attr,
> &dev_attr_multi_index.attr,
> NULL,
> diff --git a/drivers/leds/leds-blinkm.c b/drivers/leds/leds-blinkm.c
> index 577497b9d426..7d1140767c76 100644
> --- a/drivers/leds/leds-blinkm.c
> +++ b/drivers/leds/leds-blinkm.c
> @@ -694,8 +694,11 @@ static int register_multicolor(struct i2c_client *client, struct blinkm_data *da
> return -ENOMEM;
>
> mc_led_info[RED].color_index = LED_COLOR_ID_RED;
> + mc_led_info[RED].max_intensity = LED_USE_MAX_BRIGHTNESS;
> mc_led_info[GREEN].color_index = LED_COLOR_ID_GREEN;
> + mc_led_info[GREEN].max_intensity = LED_USE_MAX_BRIGHTNESS;
> mc_led_info[BLUE].color_index = LED_COLOR_ID_BLUE;
> + mc_led_info[BLUE].max_intensity = LED_USE_MAX_BRIGHTNESS;
>
> mc_led->cdev.mcled_cdev.subled_info = mc_led_info;
> mc_led->cdev.mcled_cdev.num_colors = NUM_LEDS;
> diff --git a/drivers/leds/leds-cros_ec.c b/drivers/leds/leds-cros_ec.c
> index bea3cc3fbfd2..b891471812ff 100644
> --- a/drivers/leds/leds-cros_ec.c
> +++ b/drivers/leds/leds-cros_ec.c
> @@ -202,6 +202,7 @@ static int cros_ec_led_probe_one(struct device *dev, struct cros_ec_device *cros
> continue;
>
> subleds[subled].color_index = cros_ec_led_to_linux_id[i];
> + subleds[subled].max_intensity = LED_USE_MAX_BRIGHTNESS;
> if (subled == 0)
> subleds[subled].intensity = 100;
> subled++;
> diff --git a/drivers/leds/leds-lp50xx.c b/drivers/leds/leds-lp50xx.c
> index e2a9c8592953..69c3550f1a31 100644
> --- a/drivers/leds/leds-lp50xx.c
> +++ b/drivers/leds/leds-lp50xx.c
> @@ -525,6 +525,7 @@ static int lp50xx_probe_dt(struct lp50xx *priv)
> }
>
> mc_led_info[multi_index].color_index = color_id;
> + mc_led_info[multi_index].max_intensity = 255;
> num_colors++;
> }
>
> diff --git a/drivers/leds/leds-lp55xx-common.c b/drivers/leds/leds-lp55xx-common.c
> index ea131177de96..f637e74e8780 100644
> --- a/drivers/leds/leds-lp55xx-common.c
> +++ b/drivers/leds/leds-lp55xx-common.c
> @@ -495,10 +495,9 @@ static int lp55xx_init_led(struct lp55xx_led *led,
> led_cdev->brightness_set_blocking = lp55xx_set_mc_brightness;
> led->mc_cdev.num_colors = pdata->led_config[chan].num_colors;
> for (i = 0; i < led->mc_cdev.num_colors; i++) {
> - mc_led_info[i].color_index =
> - pdata->led_config[chan].color_id[i];
> - mc_led_info[i].channel =
> - pdata->led_config[chan].output_num[i];
> + mc_led_info[i].color_index = pdata->led_config[chan].color_id[i];
> + mc_led_info[i].channel = pdata->led_config[chan].output_num[i];
> + mc_led_info[i].max_intensity = LED_USE_MAX_BRIGHTNESS;
> }
>
> led->mc_cdev.subled_info = mc_led_info;
> diff --git a/drivers/leds/leds-max77705.c b/drivers/leds/leds-max77705.c
> index 1e2054c1bf80..bd2c078b42f3 100644
> --- a/drivers/leds/leds-max77705.c
> +++ b/drivers/leds/leds-max77705.c
> @@ -149,6 +149,7 @@ static int max77705_parse_subled(struct device *dev, struct fwnode_handle *np,
> "failed to parse \"color\" of %pOF\n", np);
>
> info->color_index = color;
> + info->max_intensity = LED_USE_MAX_BRIGHTNESS;
>
> return 0;
> }
> diff --git a/drivers/leds/leds-sun50i-a100.c b/drivers/leds/leds-sun50i-a100.c
> index 2c9bd360ab81..6e77cbcb8b54 100644
> --- a/drivers/leds/leds-sun50i-a100.c
> +++ b/drivers/leds/leds-sun50i-a100.c
> @@ -504,10 +504,13 @@ static int sun50i_a100_ledc_probe(struct platform_device *pdev)
> fwnode_property_read_u32(child, "reg", &led->addr);
>
> led->subled_info[0].color_index = LED_COLOR_ID_RED;
> + led->subled_info[0].max_intensity = LED_USE_MAX_BRIGHTNESS;
> led->subled_info[0].channel = 0;
> led->subled_info[1].color_index = LED_COLOR_ID_GREEN;
> + led->subled_info[1].max_intensity = LED_USE_MAX_BRIGHTNESS;
> led->subled_info[1].channel = 1;
> led->subled_info[2].color_index = LED_COLOR_ID_BLUE;
> + led->subled_info[2].max_intensity = LED_USE_MAX_BRIGHTNESS;
> led->subled_info[2].channel = 2;
>
> led->mc_cdev.num_colors = ARRAY_SIZE(led->subled_info);
> diff --git a/drivers/leds/leds-turris-omnia.c b/drivers/leds/leds-turris-omnia.c
> index 25ee5c1eb820..fc41891fe971 100644
> --- a/drivers/leds/leds-turris-omnia.c
> +++ b/drivers/leds/leds-turris-omnia.c
> @@ -233,6 +233,7 @@ static int omnia_led_register(struct i2c_client *client, struct omnia_led *led,
> /* Initial color is white */
> for (int i = 0; i < OMNIA_LED_NUM_CHANNELS; ++i) {
> led->subled_info[i].intensity = 255;
> + led->subled_info[i].max_intensity = LED_USE_MAX_BRIGHTNESS;
> led->subled_info[i].brightness = 255;
> led->subled_info[i].channel = i;
> }
> diff --git a/drivers/leds/rgb/leds-group-multicolor.c b/drivers/leds/rgb/leds-group-multicolor.c
> index 548c7dd63ba1..c472ccbd506e 100644
> --- a/drivers/leds/rgb/leds-group-multicolor.c
> +++ b/drivers/leds/rgb/leds-group-multicolor.c
> @@ -111,6 +111,7 @@ static int leds_gmc_probe(struct platform_device *pdev)
>
> /* Configure the LED intensity to its maximum */
> subled[i].intensity = max_brightness;
> + subled[i].max_intensity = LED_USE_MAX_BRIGHTNESS;
> }
>
> /* Initialise the multicolor's LED class device */
> diff --git a/drivers/leds/rgb/leds-ktd202x.c b/drivers/leds/rgb/leds-ktd202x.c
> index e4f0f25a5e45..4b054175b3b5 100644
> --- a/drivers/leds/rgb/leds-ktd202x.c
> +++ b/drivers/leds/rgb/leds-ktd202x.c
> @@ -423,6 +423,7 @@ static int ktd202x_setup_led_rgb(struct ktd202x *chip, struct fwnode_handle *fwn
> info[i].color_index = mono_color;
> info[i].channel = reg;
> info[i].intensity = KTD202X_MAX_BRIGHTNESS;
> + info[i].max_intensity = LED_USE_MAX_BRIGHTNESS;
> i++;
> }
>
> diff --git a/drivers/leds/rgb/leds-lp5812.c b/drivers/leds/rgb/leds-lp5812.c
> index ce6d703641e8..61fa8df493fb 100644
> --- a/drivers/leds/rgb/leds-lp5812.c
> +++ b/drivers/leds/rgb/leds-lp5812.c
> @@ -353,6 +353,7 @@ static int lp5812_init_led(struct lp5812_led *led, struct lp5812_chip *chip, int
> for (i = 0; i < led->mc_cdev.num_colors; i++) {
> mc_led_info[i].color_index = chip->led_config[chan].color_id[i];
> mc_led_info[i].channel = chip->led_config[chan].led_id[i];
> + mc_led_info[i].max_intensity = LED_USE_MAX_BRIGHTNESS;
> }
>
> led->mc_cdev.subled_info = mc_led_info;
> diff --git a/drivers/leds/rgb/leds-mt6370-rgb.c b/drivers/leds/rgb/leds-mt6370-rgb.c
> index c5927d0eb830..ea11ca9d3326 100644
> --- a/drivers/leds/rgb/leds-mt6370-rgb.c
> +++ b/drivers/leds/rgb/leds-mt6370-rgb.c
> @@ -437,10 +437,7 @@ static int mt6370_mc_brightness_set(struct led_classdev *lcdev, enum led_brightn
> disable = enable;
>
> for (i = 0; i < mccdev->num_colors; i++) {
> - u32 brightness;
> -
> subled = mccdev->subled_info + i;
> - brightness = min(subled->brightness, lcdev->max_brightness);
> disable &= ~MT6370_CHEN_BIT(subled->channel);
>
> if (level == 0) {
> @@ -453,14 +450,14 @@ static int mt6370_mc_brightness_set(struct led_classdev *lcdev, enum led_brightn
> continue;
> }
>
> - if (brightness == 0) {
> + if (subled->brightness == 0) {
> enable &= ~MT6370_CHEN_BIT(subled->channel);
> continue;
> }
>
> enable |= MT6370_CHEN_BIT(subled->channel);
>
> - ret = mt6370_set_led_brightness(priv, subled->channel, brightness);
> + ret = mt6370_set_led_brightness(priv, subled->channel, subled->brightness);
> if (ret)
> goto out_unlock;
> }
> @@ -742,6 +739,7 @@ static int mt6370_assign_multicolor_info(struct device *dev, struct mt6370_led *
> sub_led[num_color].color_index = color;
> sub_led[num_color].channel = reg;
> sub_led[num_color].intensity = 0;
> + sub_led[num_color].max_intensity = LED_USE_MAX_BRIGHTNESS;
> num_color++;
> }
>
> diff --git a/drivers/leds/rgb/leds-ncp5623.c b/drivers/leds/rgb/leds-ncp5623.c
> index 85d6be6fff2b..f2528f06507d 100644
> --- a/drivers/leds/rgb/leds-ncp5623.c
> +++ b/drivers/leds/rgb/leds-ncp5623.c
> @@ -56,8 +56,7 @@ static int ncp5623_brightness_set(struct led_classdev *cdev,
> for (int i = 0; i < mc_cdev->num_colors; i++) {
> ret = ncp5623_write(ncp->client,
> NCP5623_PWM_REG(mc_cdev->subled_info[i].channel),
> - min(mc_cdev->subled_info[i].intensity,
> - NCP5623_MAX_BRIGHTNESS));
> + mc_cdev->subled_info[i].intensity);
> if (ret)
> return ret;
> }
> @@ -190,6 +189,7 @@ static int ncp5623_probe(struct i2c_client *client)
> goto release_led_node;
>
> subled_info[ncp->mc_dev.num_colors].channel = reg;
> + subled_info[ncp->mc_dev.num_colors].max_intensity = NCP5623_MAX_BRIGHTNESS;
> subled_info[ncp->mc_dev.num_colors++].color_index = color_index;
> }
>
> diff --git a/drivers/leds/rgb/leds-pwm-multicolor.c b/drivers/leds/rgb/leds-pwm-multicolor.c
> index e0d7d3c9215c..578e67dcc881 100644
> --- a/drivers/leds/rgb/leds-pwm-multicolor.c
> +++ b/drivers/leds/rgb/leds-pwm-multicolor.c
> @@ -95,6 +95,7 @@ static int iterate_subleds(struct device *dev, struct pwm_mc_led *priv,
> }
>
> subled[priv->mc_cdev.num_colors].color_index = color;
> + subled[priv->mc_cdev.num_colors].max_intensity = LED_USE_MAX_BRIGHTNESS;
> priv->mc_cdev.num_colors++;
> }
>
> diff --git a/drivers/leds/rgb/leds-qcom-lpg.c b/drivers/leds/rgb/leds-qcom-lpg.c
> index f6061c47f863..390dcbe1cd18 100644
> --- a/drivers/leds/rgb/leds-qcom-lpg.c
> +++ b/drivers/leds/rgb/leds-qcom-lpg.c
> @@ -1411,6 +1411,7 @@ static int lpg_add_led(struct lpg *lpg, struct device_node *np)
>
> info[i].color_index = led->channels[i]->color;
> info[i].intensity = 0;
> + info[i].max_intensity = LED_USE_MAX_BRIGHTNESS;
> i++;
> }
>
> diff --git a/drivers/platform/x86/silicom-platform.c b/drivers/platform/x86/silicom-platform.c
> index 266f7bc5e416..5ed115b7dc01 100644
> --- a/drivers/platform/x86/silicom-platform.c
> +++ b/drivers/platform/x86/silicom-platform.c
> @@ -283,18 +283,21 @@ static struct mc_subled plat_0222_wan_mc_subled_info[] __initdata = {
> .color_index = LED_COLOR_ID_WHITE,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0c, 7),
> },
> {
> .color_index = LED_COLOR_ID_YELLOW,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0c, 6),
> },
> {
> .color_index = LED_COLOR_ID_RED,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0c, 5),
> },
> };
> @@ -304,18 +307,21 @@ static struct mc_subled plat_0222_sys_mc_subled_info[] __initdata = {
> .color_index = LED_COLOR_ID_WHITE,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0c, 4),
> },
> {
> .color_index = LED_COLOR_ID_AMBER,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0c, 3),
> },
> {
> .color_index = LED_COLOR_ID_RED,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0c, 2),
> },
> };
> @@ -325,24 +331,28 @@ static struct mc_subled plat_0222_stat1_mc_subled_info[] __initdata = {
> .color_index = LED_COLOR_ID_RED,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0c, 1),
> },
> {
> .color_index = LED_COLOR_ID_GREEN,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0c, 0),
> },
> {
> .color_index = LED_COLOR_ID_BLUE,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0d, 7),
> },
> {
> .color_index = LED_COLOR_ID_YELLOW,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0d, 6),
> },
> };
> @@ -352,24 +362,28 @@ static struct mc_subled plat_0222_stat2_mc_subled_info[] __initdata = {
> .color_index = LED_COLOR_ID_RED,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0d, 5),
> },
> {
> .color_index = LED_COLOR_ID_GREEN,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0d, 4),
> },
> {
> .color_index = LED_COLOR_ID_BLUE,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0d, 3),
> },
> {
> .color_index = LED_COLOR_ID_YELLOW,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0d, 2),
> },
> };
> @@ -379,24 +393,28 @@ static struct mc_subled plat_0222_stat3_mc_subled_info[] __initdata = {
> .color_index = LED_COLOR_ID_RED,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0d, 1),
> },
> {
> .color_index = LED_COLOR_ID_GREEN,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0d, 0),
> },
> {
> .color_index = LED_COLOR_ID_BLUE,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0e, 1),
> },
> {
> .color_index = LED_COLOR_ID_YELLOW,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x0e, 0),
> },
> };
> @@ -497,24 +515,28 @@ static struct mc_subled cordoba_fp_left_mc_subled_info[] __initdata = {
> .color_index = LED_COLOR_ID_RED,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x08, 6),
> },
> {
> .color_index = LED_COLOR_ID_GREEN,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x08, 5),
> },
> {
> .color_index = LED_COLOR_ID_BLUE,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x09, 7),
> },
> {
> .color_index = LED_COLOR_ID_AMBER,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x09, 4),
> },
> };
> @@ -524,24 +546,28 @@ static struct mc_subled cordoba_fp_center_mc_subled_info[] __initdata = {
> .color_index = LED_COLOR_ID_RED,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x08, 7),
> },
> {
> .color_index = LED_COLOR_ID_GREEN,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x08, 4),
> },
> {
> .color_index = LED_COLOR_ID_BLUE,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x08, 3),
> },
> {
> .color_index = LED_COLOR_ID_AMBER,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x09, 6),
> },
> };
> @@ -551,24 +577,28 @@ static struct mc_subled cordoba_fp_right_mc_subled_info[] __initdata = {
> .color_index = LED_COLOR_ID_RED,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x08, 2),
> },
> {
> .color_index = LED_COLOR_ID_GREEN,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x08, 1),
> },
> {
> .color_index = LED_COLOR_ID_BLUE,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x08, 0),
> },
> {
> .color_index = LED_COLOR_ID_AMBER,
> .brightness = 1,
> .intensity = 0,
> + .max_intensity = LED_USE_MAX_BRIGHTNESS,
> .channel = OFFSET_BIT_TO_CHANNEL(0x09, 5),
> },
> };
> diff --git a/drivers/platform/x86/uniwill/uniwill-acpi.c b/drivers/platform/x86/uniwill/uniwill-acpi.c
> index fee93537aa43..9468a93f7a59 100644
> --- a/drivers/platform/x86/uniwill/uniwill-acpi.c
> +++ b/drivers/platform/x86/uniwill/uniwill-acpi.c
> @@ -1082,8 +1082,7 @@ static int uniwill_led_brightness_set(struct led_classdev *led_cdev, enum led_br
> guard(mutex)(&data->led_lock);
>
> for (int i = 0; i < LED_CHANNELS; i++) {
> - /* Prevent the brightness values from overflowing */
> - value = min(LED_MAX_BRIGHTNESS, data->led_mc_subled_info[i].brightness);
> + value = data->led_mc_subled_info[i].brightness;
> ret = regmap_write(data->regmap, uniwill_led_channel_to_ac_reg[i], value);
> if (ret < 0)
> return ret;
> @@ -1182,6 +1181,7 @@ static int uniwill_led_init(struct uniwill_data *data)
> return ret;
>
> data->led_mc_subled_info[i].intensity = value;
> + data->led_mc_subled_info[i].max_intensity = LED_USE_MAX_BRIGHTNESS;
> data->led_mc_subled_info[i].channel = i;
> }
>
> diff --git a/include/linux/led-class-multicolor.h b/include/linux/led-class-multicolor.h
> index db9f34c6736e..67ac0ac0fd54 100644
> --- a/include/linux/led-class-multicolor.h
> +++ b/include/linux/led-class-multicolor.h
> @@ -9,10 +9,33 @@
> #include <linux/leds.h>
> #include <dt-bindings/leds/common.h>
>
> +#define LED_USE_MAX_BRIGHTNESS 0
> +
> +/**
> + * struct mc_subled - Color component description.
> + * @color_index: Color ID.
> + * @brightness: Scaled intensity.
> + * @intensity: Current intensity.
> + * @max_intensity: Maximum supported intensity value.
> + * @channel: Channel index.
> + *
> + * Describes a color component of a multicolor LED. Many multicolor LEDs
> + * do no support gobal brightness control in hardware, so they use
> + * the brightness field in connection with led_mc_calc_color_components()
> + * to perform the intensity scaling in software.
> + * Such drivers should set max_intensity to LED_USE_MAX_BRIGHTNESS
I'd just mention here that the field needs to be left initialized to 0,
to let the LED subsystem to adjust it to max_brightness. Note that in
case of max_brightness we allow for the same - if it is 0, then
led-class.c adjusts it to legacy LED_FULL (255).
> + * to signal the multicolor LED core that the maximum global brightness of the
> + * LED class device should be used for limiting incoming intensity values.
> + *
> + * Multicolor LEDs that do support global brightness control in hardware
> + * should instead set max_intensity to the maximum intensity value supported
> + * by the hardware for a given color component.
> + */
> struct mc_subled {
> unsigned int color_index;
> unsigned int brightness;
> unsigned int intensity;
> + unsigned int max_intensity;
> unsigned int channel;
> };
>
> @@ -53,7 +76,14 @@ int led_classdev_multicolor_register_ext(struct device *parent,
> */
> void led_classdev_multicolor_unregister(struct led_classdev_mc *mcled_cdev);
>
> -/* Calculate brightness for the monochrome LED cluster */
> +/**
> + * led_mc_calc_color_components() - Calculate brightness for the monochrome LED cluster.
It does not calculate brightness for the whole LED cluster, but for each
sub-LED individually. Brightness of the whole cluster can be deemed a
lightness of the multicolor LED, and the blend of the color components
can be deemed its hue.
In this case I'd document it as "Calculates brightness values for the
LEDs of the monochrome LED cluster".
> + * @mcled_cdev - Multicolor LED class device of the LED cluster.
> + * @led_brightness - Global brightness of the LED cluster.
> + *
> + * Calculates the brightness values for each color component of a monochrome LED cluster,
> + * see Documentation/leds/leds-class-multicolor.rst for details.
> + */
> int led_mc_calc_color_components(struct led_classdev_mc *mcled_cdev,
> enum led_brightness brightness);
>
--
Best regards,
Jacek Anaszewski
^ permalink raw reply
* [PATCH v4 4/4] kallsyms: add KUnit tests for lineinfo feature
From: Sasha Levin @ 2026-03-22 13:15 UTC (permalink / raw)
To: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
James Bottomley
Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, Helge Deller,
Randy Dunlap, Laurent Pinchart, Vivian Wang, linux-kernel,
linux-kbuild, linux-modules, linux-doc, Sasha Levin
In-Reply-To: <20260322131543.971079-1-sashal@kernel.org>
Add a KUnit test module (CONFIG_LINEINFO_KUNIT_TEST) that verifies the
kallsyms lineinfo feature produces correct source file:line annotations
in stack traces.
Export sprint_backtrace() and sprint_backtrace_build_id() as GPL symbols
so the test module can exercise the backtrace APIs.
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
MAINTAINERS | 1 +
kernel/kallsyms.c | 2 +
lib/Kconfig.debug | 10 +
lib/tests/Makefile | 3 +
lib/tests/lineinfo_kunit.c | 813 +++++++++++++++++++++++++++++++++++++
5 files changed, 829 insertions(+)
create mode 100644 lib/tests/lineinfo_kunit.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 535e992ca5a20..118711f72b874 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13733,6 +13733,7 @@ M: Sasha Levin <sashal@kernel.org>
S: Maintained
F: Documentation/admin-guide/kallsyms-lineinfo.rst
F: include/linux/mod_lineinfo.h
+F: lib/tests/lineinfo_kunit.c
F: scripts/gen-mod-lineinfo.sh
F: scripts/gen_lineinfo.c
diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
index 76e30cac3a277..e6f796d43dd70 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -625,6 +625,7 @@ int sprint_backtrace(char *buffer, unsigned long address)
{
return __sprint_symbol(buffer, address, -1, 1, 0);
}
+EXPORT_SYMBOL_GPL(sprint_backtrace);
/**
* sprint_backtrace_build_id - Look up a backtrace symbol and return it in a text buffer
@@ -645,6 +646,7 @@ int sprint_backtrace_build_id(char *buffer, unsigned long address)
{
return __sprint_symbol(buffer, address, -1, 1, 1);
}
+EXPORT_SYMBOL_GPL(sprint_backtrace_build_id);
/* To avoid using get_symbol_offset for every symbol, we carry prefix along. */
struct kallsym_iter {
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 93f356d2b3d95..688bbcb3eaa62 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -3048,6 +3048,16 @@ config LONGEST_SYM_KUNIT_TEST
If unsure, say N.
+config LINEINFO_KUNIT_TEST
+ tristate "KUnit tests for kallsyms lineinfo" if !KUNIT_ALL_TESTS
+ depends on KUNIT && KALLSYMS_LINEINFO
+ default KUNIT_ALL_TESTS
+ help
+ KUnit tests for the kallsyms source line info feature.
+ Verifies that stack traces include correct (file.c:line) annotations.
+
+ If unsure, say N.
+
config HW_BREAKPOINT_KUNIT_TEST
bool "Test hw_breakpoint constraints accounting" if !KUNIT_ALL_TESTS
depends on HAVE_HW_BREAKPOINT
diff --git a/lib/tests/Makefile b/lib/tests/Makefile
index 05f74edbc62bf..c6add3b04bbd5 100644
--- a/lib/tests/Makefile
+++ b/lib/tests/Makefile
@@ -36,6 +36,9 @@ obj-$(CONFIG_LIVEUPDATE_TEST) += liveupdate.o
CFLAGS_longest_symbol_kunit.o += $(call cc-disable-warning, missing-prototypes)
obj-$(CONFIG_LONGEST_SYM_KUNIT_TEST) += longest_symbol_kunit.o
+CFLAGS_lineinfo_kunit.o += $(call cc-option,-fno-inline-functions-called-once)
+obj-$(CONFIG_LINEINFO_KUNIT_TEST) += lineinfo_kunit.o
+
obj-$(CONFIG_MEMCPY_KUNIT_TEST) += memcpy_kunit.o
obj-$(CONFIG_MIN_HEAP_KUNIT_TEST) += min_heap_kunit.o
CFLAGS_overflow_kunit.o = $(call cc-disable-warning, tautological-constant-out-of-range-compare)
diff --git a/lib/tests/lineinfo_kunit.c b/lib/tests/lineinfo_kunit.c
new file mode 100644
index 0000000000000..81696fa0000aa
--- /dev/null
+++ b/lib/tests/lineinfo_kunit.c
@@ -0,0 +1,813 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for kallsyms lineinfo (CONFIG_KALLSYMS_LINEINFO).
+ *
+ * Copyright (c) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Verifies that sprint_symbol() and related APIs append correct
+ * " (file.c:NNN)" annotations to kernel symbol lookups.
+ *
+ * Build with: CONFIG_LINEINFO_KUNIT_TEST=m (or =y)
+ * Run with: ./tools/testing/kunit/kunit.py run lineinfo
+ */
+
+#include <kunit/test.h>
+#include <linux/kallsyms.h>
+#include <linux/module.h>
+#include <linux/smp.h>
+#include <linux/string.h>
+#include <linux/slab.h>
+#include <linux/mod_lineinfo.h>
+
+/* --------------- helpers --------------- */
+
+static char *alloc_sym_buf(struct kunit *test)
+{
+ return kunit_kzalloc(test, KSYM_SYMBOL_LEN, GFP_KERNEL);
+}
+
+/*
+ * Return true if @buf contains a lineinfo annotation matching
+ * the pattern " (<path>:<digits>)".
+ *
+ * The path may be a full path like "lib/tests/lineinfo_kunit.c" or
+ * a shortened form from module lineinfo (e.g., just a directory name).
+ */
+static bool has_lineinfo(const char *buf)
+{
+ const char *p, *colon, *end;
+
+ p = strstr(buf, " (");
+ if (!p)
+ return false;
+ p += 2; /* skip " (" */
+
+ colon = strchr(p, ':');
+ if (!colon || colon == p)
+ return false;
+
+ /* After colon: one or more digits then ')' */
+ end = colon + 1;
+ if (*end < '0' || *end > '9')
+ return false;
+ while (*end >= '0' && *end <= '9')
+ end++;
+ return *end == ')';
+}
+
+/*
+ * Extract line number from a lineinfo annotation.
+ * Returns 0 if not found.
+ */
+static unsigned int extract_line(const char *buf)
+{
+ const char *p, *colon;
+ unsigned int line = 0;
+
+ p = strstr(buf, " (");
+ if (!p)
+ return 0;
+
+ colon = strchr(p + 2, ':');
+ if (!colon)
+ return 0;
+
+ colon++;
+ while (*colon >= '0' && *colon <= '9') {
+ line = line * 10 + (*colon - '0');
+ colon++;
+ }
+ return line;
+}
+
+/*
+ * Check if the lineinfo annotation contains the given filename substring.
+ */
+static bool lineinfo_contains_file(const char *buf, const char *name)
+{
+ const char *p, *colon;
+
+ p = strstr(buf, " (");
+ if (!p)
+ return false;
+
+ colon = strchr(p + 2, ':');
+ if (!colon)
+ return false;
+
+ /* Search for @name between '(' and ':' */
+ return strnstr(p + 1, name, colon - p - 1) != NULL;
+}
+
+/* --------------- target functions --------------- */
+
+static noinline int lineinfo_target_normal(void)
+{
+ barrier();
+ return 42;
+}
+
+static noinline int lineinfo_target_short(void)
+{
+ barrier();
+ return 1;
+}
+
+static noinline int lineinfo_target_with_arg(int x)
+{
+ barrier();
+ return x + 1;
+}
+
+static noinline int lineinfo_target_many_lines(void)
+{
+ int a = 0;
+
+ barrier();
+ a += 1;
+ a += 2;
+ a += 3;
+ a += 4;
+ a += 5;
+ a += 6;
+ a += 7;
+ a += 8;
+ a += 9;
+ a += 10;
+ barrier();
+ return a;
+}
+
+static __always_inline int lineinfo_inline_helper(void)
+{
+ return 99;
+}
+
+static noinline int lineinfo_inline_caller(void)
+{
+ barrier();
+ return lineinfo_inline_helper();
+}
+
+/* 10-deep call chain */
+static noinline int lineinfo_chain_10(void) { barrier(); return 10; }
+static noinline int lineinfo_chain_9(void) { barrier(); return lineinfo_chain_10(); }
+static noinline int lineinfo_chain_8(void) { barrier(); return lineinfo_chain_9(); }
+static noinline int lineinfo_chain_7(void) { barrier(); return lineinfo_chain_8(); }
+static noinline int lineinfo_chain_6(void) { barrier(); return lineinfo_chain_7(); }
+static noinline int lineinfo_chain_5(void) { barrier(); return lineinfo_chain_6(); }
+static noinline int lineinfo_chain_4(void) { barrier(); return lineinfo_chain_5(); }
+static noinline int lineinfo_chain_3(void) { barrier(); return lineinfo_chain_4(); }
+static noinline int lineinfo_chain_2(void) { barrier(); return lineinfo_chain_3(); }
+static noinline int lineinfo_chain_1(void) { barrier(); return lineinfo_chain_2(); }
+
+/* --------------- Group A: Basic lineinfo presence --------------- */
+
+static void test_normal_function(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+ sprint_symbol(buf, addr);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo in: %s", buf);
+ KUNIT_EXPECT_TRUE_MSG(test,
+ lineinfo_contains_file(buf, "lineinfo_kunit.c"),
+ "Wrong file in: %s", buf);
+}
+
+static void test_static_function(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_target_short;
+
+ sprint_symbol(buf, addr);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo in: %s", buf);
+}
+
+static void test_noinline_function(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_target_with_arg;
+
+ sprint_symbol(buf, addr);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo in: %s", buf);
+}
+
+static void test_inline_function(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_inline_caller;
+
+ sprint_symbol(buf, addr);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo for inline caller in: %s", buf);
+ KUNIT_EXPECT_TRUE_MSG(test,
+ lineinfo_contains_file(buf, "lineinfo_kunit.c"),
+ "Wrong file in: %s", buf);
+}
+
+static void test_short_function(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_target_short;
+
+ sprint_symbol(buf, addr);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo for short function in: %s", buf);
+}
+
+static void test_many_lines_function(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_target_many_lines;
+ unsigned int line;
+
+ sprint_symbol(buf, addr);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo in: %s", buf);
+ line = extract_line(buf);
+ KUNIT_EXPECT_GT_MSG(test, line, (unsigned int)0,
+ "Line number should be > 0 in: %s", buf);
+}
+
+/* --------------- Group B: Deep call chain --------------- */
+
+typedef int (*chain_fn_t)(void);
+
+static void test_deep_call_chain(struct kunit *test)
+{
+ static const chain_fn_t chain_fns[] = {
+ lineinfo_chain_1, lineinfo_chain_2,
+ lineinfo_chain_3, lineinfo_chain_4,
+ lineinfo_chain_5, lineinfo_chain_6,
+ lineinfo_chain_7, lineinfo_chain_8,
+ lineinfo_chain_9, lineinfo_chain_10,
+ };
+ char *buf = alloc_sym_buf(test);
+ int i, found = 0;
+
+ /* Call chain to prevent dead-code elimination */
+ KUNIT_ASSERT_EQ(test, lineinfo_chain_1(), 10);
+
+ for (i = 0; i < ARRAY_SIZE(chain_fns); i++) {
+ unsigned long addr = (unsigned long)chain_fns[i];
+
+ sprint_symbol(buf, addr);
+ if (has_lineinfo(buf))
+ found++;
+ }
+
+ /*
+ * Not every tiny function gets DWARF line info (compiler may
+ * omit it for very small stubs), but at least some should.
+ */
+ KUNIT_EXPECT_GT_MSG(test, found, 0,
+ "None of the 10 chain functions had lineinfo");
+}
+
+/* --------------- Group C: sprint_symbol API variants --------------- */
+
+static void test_sprint_symbol_format(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+ sprint_symbol(buf, addr);
+
+ /* Should contain +0x and /0x for offset/size */
+ KUNIT_EXPECT_NOT_NULL_MSG(test, strstr(buf, "+0x"),
+ "Missing offset in: %s", buf);
+ KUNIT_EXPECT_NOT_NULL_MSG(test, strstr(buf, "/0x"),
+ "Missing size in: %s", buf);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo in: %s", buf);
+}
+
+static void test_sprint_backtrace(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+ /* sprint_backtrace subtracts 1 internally to handle tail calls */
+ sprint_backtrace(buf, addr + 1);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo in backtrace: %s", buf);
+ KUNIT_EXPECT_TRUE_MSG(test,
+ lineinfo_contains_file(buf, "lineinfo_kunit.c"),
+ "Wrong file in backtrace: %s", buf);
+}
+
+static void test_sprint_backtrace_build_id(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+ sprint_backtrace_build_id(buf, addr + 1);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo in backtrace_build_id: %s", buf);
+}
+
+static void test_sprint_symbol_no_offset(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+ sprint_symbol_no_offset(buf, addr);
+ /* No "+0x" in output */
+ KUNIT_EXPECT_NULL_MSG(test, strstr(buf, "+0x"),
+ "Unexpected offset in no_offset: %s", buf);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo in no_offset: %s", buf);
+}
+
+/* --------------- Group D: printk format specifiers --------------- */
+
+static void test_pS_format(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ void *addr = lineinfo_target_normal;
+
+ snprintf(buf, KSYM_SYMBOL_LEN, "%pS", addr);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo in %%pS: %s", buf);
+}
+
+static void test_pBb_format(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ /*
+ * %pBb uses sprint_backtrace_build_id which subtracts 1 from the
+ * address, so pass addr+1 to resolve back to the function.
+ */
+ void *addr = (void *)((unsigned long)lineinfo_target_normal + 1);
+
+ snprintf(buf, KSYM_SYMBOL_LEN, "%pBb", addr);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo in %%pBb: %s", buf);
+}
+
+static void test_pSR_format(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ void *addr = lineinfo_target_normal;
+
+ snprintf(buf, KSYM_SYMBOL_LEN, "%pSR", addr);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo in %%pSR: %s", buf);
+}
+
+/* --------------- Group E: Address edge cases --------------- */
+
+static void test_symbol_start_addr(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+ sprint_symbol(buf, addr);
+ KUNIT_EXPECT_NOT_NULL_MSG(test, strstr(buf, "+0x0/"),
+ "Expected +0x0/ at function start: %s", buf);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo at function start: %s", buf);
+}
+
+static void test_symbol_nonzero_offset(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+ /*
+ * sprint_backtrace subtracts 1 internally.
+ * Passing addr+2 resolves to addr+1 which is inside the function
+ * at a non-zero offset.
+ */
+ sprint_backtrace(buf, addr + 2);
+ KUNIT_EXPECT_TRUE_MSG(test,
+ strnstr(buf, "lineinfo_target_normal",
+ KSYM_SYMBOL_LEN) != NULL,
+ "Didn't resolve to expected function: %s", buf);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo at non-zero offset: %s", buf);
+}
+
+static void test_unknown_address(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+
+ sprint_symbol(buf, 1UL);
+ /* Should be "0x1" with no lineinfo */
+ KUNIT_EXPECT_NOT_NULL_MSG(test, strstr(buf, "0x1"),
+ "Expected hex address for bogus addr: %s", buf);
+ KUNIT_EXPECT_FALSE_MSG(test, has_lineinfo(buf),
+ "Unexpected lineinfo for bogus addr: %s", buf);
+}
+
+static void test_kernel_function_lineinfo(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)sprint_symbol;
+
+ sprint_symbol(buf, addr);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo for sprint_symbol: %s", buf);
+ KUNIT_EXPECT_TRUE_MSG(test,
+ lineinfo_contains_file(buf, "kallsyms.c"),
+ "Expected kallsyms.c in: %s", buf);
+}
+
+static void test_assembly_no_lineinfo(struct kunit *test)
+{
+#if IS_BUILTIN(CONFIG_LINEINFO_KUNIT_TEST)
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)_text;
+
+ sprint_symbol(buf, addr);
+ /*
+ * _text is typically an asm entry point with no DWARF line info.
+ * If it has lineinfo, it's a C-based entry — skip in that case.
+ */
+ if (has_lineinfo(buf))
+ kunit_skip(test, "_text has lineinfo (C entry?): %s", buf);
+
+ KUNIT_EXPECT_FALSE_MSG(test, has_lineinfo(buf),
+ "Unexpected lineinfo for asm symbol: %s", buf);
+#else
+ kunit_skip(test, "_text not accessible from modules");
+#endif
+}
+
+/* --------------- Group F: Module path --------------- */
+
+static void test_module_function_lineinfo(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+ if (!IS_MODULE(CONFIG_LINEINFO_KUNIT_TEST)) {
+ kunit_skip(test, "Test only meaningful when built as module");
+ return;
+ }
+
+ sprint_symbol(buf, addr);
+ KUNIT_EXPECT_NOT_NULL_MSG(test,
+ strstr(buf, "[lineinfo_kunit"),
+ "Missing module name in: %s", buf);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo for module function: %s", buf);
+ KUNIT_EXPECT_TRUE_MSG(test,
+ lineinfo_contains_file(buf, "lineinfo_kunit.c"),
+ "Wrong file for module function: %s", buf);
+}
+
+/* --------------- Group G: Stress --------------- */
+
+struct lineinfo_stress_data {
+ unsigned long addr;
+ atomic_t failures;
+};
+
+static void lineinfo_stress_fn(void *info)
+{
+ struct lineinfo_stress_data *data = info;
+ char buf[KSYM_SYMBOL_LEN];
+ int i;
+
+ for (i = 0; i < 100; i++) {
+ sprint_symbol(buf, data->addr);
+ if (!has_lineinfo(buf))
+ atomic_inc(&data->failures);
+ }
+}
+
+static void test_concurrent_sprint_symbol(struct kunit *test)
+{
+ struct lineinfo_stress_data data;
+
+ data.addr = (unsigned long)lineinfo_target_normal;
+ atomic_set(&data.failures, 0);
+
+ on_each_cpu(lineinfo_stress_fn, &data, 1);
+
+ KUNIT_EXPECT_EQ_MSG(test, atomic_read(&data.failures), 0,
+ "Concurrent lineinfo failures detected");
+}
+
+static void test_rapid_sprint_symbol(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_target_normal;
+ int i, failures = 0;
+
+ for (i = 0; i < 1000; i++) {
+ sprint_symbol(buf, addr);
+ if (!has_lineinfo(buf))
+ failures++;
+ }
+
+ KUNIT_EXPECT_EQ_MSG(test, failures, 0,
+ "Rapid sprint_symbol failures: %d/1000", failures);
+}
+
+/* --------------- Group H: Safety and plausibility --------------- */
+
+static void test_line_number_plausible(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_target_normal;
+ unsigned int line;
+
+ sprint_symbol(buf, addr);
+ KUNIT_ASSERT_TRUE(test, has_lineinfo(buf));
+
+ line = extract_line(buf);
+ KUNIT_EXPECT_GT_MSG(test, line, (unsigned int)0,
+ "Line number should be > 0");
+ KUNIT_EXPECT_LT_MSG(test, line, (unsigned int)10000,
+ "Line number %u implausibly large for this file",
+ line);
+}
+
+static void test_buffer_no_overflow(struct kunit *test)
+{
+ const size_t canary_size = 16;
+ char *buf;
+ int i;
+
+ buf = kunit_kzalloc(test, KSYM_SYMBOL_LEN + canary_size, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, buf);
+
+ /* Fill canary area past KSYM_SYMBOL_LEN with 0xAA */
+ memset(buf + KSYM_SYMBOL_LEN, 0xAA, canary_size);
+
+ sprint_symbol(buf, (unsigned long)lineinfo_target_normal);
+
+ /* Verify canary bytes are untouched */
+ for (i = 0; i < canary_size; i++) {
+ KUNIT_EXPECT_EQ_MSG(test,
+ (unsigned char)buf[KSYM_SYMBOL_LEN + i],
+ (unsigned char)0xAA,
+ "Buffer overflow at offset %d past KSYM_SYMBOL_LEN",
+ i);
+ }
+}
+
+static void test_dump_stack_no_crash(struct kunit *test)
+{
+ /* Just verify dump_stack() completes without panic */
+ dump_stack();
+ KUNIT_SUCCEED(test);
+}
+
+static void test_sprint_symbol_build_id(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+ sprint_symbol_build_id(buf, addr);
+ KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+ "No lineinfo in sprint_symbol_build_id: %s", buf);
+}
+
+static void test_sleb128_edge_cases(struct kunit *test)
+{
+ u32 pos;
+ int32_t result;
+
+ /* Value 0: single byte 0x00 */
+ {
+ static const u8 data[] = { 0x00 };
+
+ pos = 0;
+ result = lineinfo_read_sleb128(data, &pos, sizeof(data));
+ KUNIT_EXPECT_EQ(test, result, (int32_t)0);
+ KUNIT_EXPECT_EQ(test, pos, (u32)1);
+ }
+
+ /* Value -1: single byte 0x7F */
+ {
+ static const u8 data[] = { 0x7f };
+
+ pos = 0;
+ result = lineinfo_read_sleb128(data, &pos, sizeof(data));
+ KUNIT_EXPECT_EQ(test, result, (int32_t)-1);
+ KUNIT_EXPECT_EQ(test, pos, (u32)1);
+ }
+
+ /* Value 1: single byte 0x01 */
+ {
+ static const u8 data[] = { 0x01 };
+
+ pos = 0;
+ result = lineinfo_read_sleb128(data, &pos, sizeof(data));
+ KUNIT_EXPECT_EQ(test, result, (int32_t)1);
+ KUNIT_EXPECT_EQ(test, pos, (u32)1);
+ }
+
+ /* Value -64: single byte 0x40 */
+ {
+ static const u8 data[] = { 0x40 };
+
+ pos = 0;
+ result = lineinfo_read_sleb128(data, &pos, sizeof(data));
+ KUNIT_EXPECT_EQ(test, result, (int32_t)-64);
+ KUNIT_EXPECT_EQ(test, pos, (u32)1);
+ }
+
+ /* Value 63: single byte 0x3F */
+ {
+ static const u8 data[] = { 0x3f };
+
+ pos = 0;
+ result = lineinfo_read_sleb128(data, &pos, sizeof(data));
+ KUNIT_EXPECT_EQ(test, result, (int32_t)63);
+ KUNIT_EXPECT_EQ(test, pos, (u32)1);
+ }
+
+ /* Value -128: two bytes 0x80 0x7F */
+ {
+ static const u8 data[] = { 0x80, 0x7f };
+
+ pos = 0;
+ result = lineinfo_read_sleb128(data, &pos, sizeof(data));
+ KUNIT_EXPECT_EQ(test, result, (int32_t)-128);
+ KUNIT_EXPECT_EQ(test, pos, (u32)2);
+ }
+}
+
+static void test_uleb128_edge_cases(struct kunit *test)
+{
+ u32 pos, result;
+
+ /* Value 0: single byte 0x00 */
+ {
+ static const u8 data[] = { 0x00 };
+
+ pos = 0;
+ result = lineinfo_read_uleb128(data, &pos, sizeof(data));
+ KUNIT_EXPECT_EQ(test, result, (u32)0);
+ KUNIT_EXPECT_EQ(test, pos, (u32)1);
+ }
+
+ /* Value 127: single byte 0x7F */
+ {
+ static const u8 data[] = { 0x7F };
+
+ pos = 0;
+ result = lineinfo_read_uleb128(data, &pos, sizeof(data));
+ KUNIT_EXPECT_EQ(test, result, (u32)127);
+ KUNIT_EXPECT_EQ(test, pos, (u32)1);
+ }
+
+ /* Value 128: two bytes 0x80 0x01 */
+ {
+ static const u8 data[] = { 0x80, 0x01 };
+
+ pos = 0;
+ result = lineinfo_read_uleb128(data, &pos, sizeof(data));
+ KUNIT_EXPECT_EQ(test, result, (u32)128);
+ KUNIT_EXPECT_EQ(test, pos, (u32)2);
+ }
+
+ /* Max u32 0xFFFFFFFF: 5 bytes */
+ {
+ static const u8 data[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0x0F };
+
+ pos = 0;
+ result = lineinfo_read_uleb128(data, &pos, sizeof(data));
+ KUNIT_EXPECT_EQ(test, result, (u32)0xFFFFFFFF);
+ KUNIT_EXPECT_EQ(test, pos, (u32)5);
+ }
+
+ /* Truncated input: pos >= end returns 0 */
+ {
+ static const u8 data[] = { 0x80 };
+
+ pos = 0;
+ result = lineinfo_read_uleb128(data, &pos, 0);
+ KUNIT_EXPECT_EQ_MSG(test, result, (u32)0,
+ "Expected 0 for empty input");
+ }
+
+ /* Truncated mid-varint: continuation byte but end reached */
+ {
+ static const u8 data[] = { 0x80 };
+
+ pos = 0;
+ result = lineinfo_read_uleb128(data, &pos, 1);
+ KUNIT_EXPECT_EQ_MSG(test, result, (u32)0,
+ "Expected 0 for truncated varint");
+ KUNIT_EXPECT_EQ(test, pos, (u32)1);
+ }
+}
+
+static void test_line_number_accuracy(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_target_normal;
+ unsigned int line;
+
+ sprint_symbol(buf, addr);
+ KUNIT_ASSERT_TRUE(test, has_lineinfo(buf));
+
+ line = extract_line(buf);
+
+ /*
+ * lineinfo_target_normal is defined around line 103-107.
+ * Allow wide range: KASAN instrumentation and module lineinfo
+ * address mapping can shift the reported line significantly.
+ */
+ KUNIT_EXPECT_GE_MSG(test, line, (unsigned int)50,
+ "Line %u too low for lineinfo_target_normal", line);
+ KUNIT_EXPECT_LE_MSG(test, line, (unsigned int)300,
+ "Line %u too high for lineinfo_target_normal", line);
+}
+
+static void test_many_lines_mid_function(struct kunit *test)
+{
+ char *buf = alloc_sym_buf(test);
+ unsigned long addr = (unsigned long)lineinfo_target_many_lines;
+ unsigned int line;
+ unsigned long mid_addr;
+
+ /* Get function size from sprint_symbol output */
+ sprint_symbol(buf, addr);
+ KUNIT_ASSERT_TRUE(test, has_lineinfo(buf));
+
+ /* Try an address 8 bytes into the function (past prologue) */
+ mid_addr = addr + 8;
+ sprint_symbol(buf, mid_addr);
+
+ /*
+ * Should still resolve to lineinfo_target_many_lines.
+ * Lineinfo should be present with a plausible line number.
+ */
+ KUNIT_EXPECT_TRUE_MSG(test,
+ strnstr(buf, "lineinfo_target_many_lines",
+ KSYM_SYMBOL_LEN) != NULL,
+ "Mid-function addr resolved to wrong symbol: %s",
+ buf);
+ if (has_lineinfo(buf)) {
+ line = extract_line(buf);
+ KUNIT_EXPECT_GE_MSG(test, line, (unsigned int)50,
+ "Line %u too low for mid-function", line);
+ KUNIT_EXPECT_LE_MSG(test, line, (unsigned int)700,
+ "Line %u too high for mid-function", line);
+ }
+}
+
+/* --------------- Suite registration --------------- */
+
+static struct kunit_case lineinfo_test_cases[] = {
+ /* Group A: Basic lineinfo presence */
+ KUNIT_CASE(test_normal_function),
+ KUNIT_CASE(test_static_function),
+ KUNIT_CASE(test_noinline_function),
+ KUNIT_CASE(test_inline_function),
+ KUNIT_CASE(test_short_function),
+ KUNIT_CASE(test_many_lines_function),
+ /* Group B: Deep call chain */
+ KUNIT_CASE(test_deep_call_chain),
+ /* Group C: sprint_symbol API variants */
+ KUNIT_CASE(test_sprint_symbol_format),
+ KUNIT_CASE(test_sprint_backtrace),
+ KUNIT_CASE(test_sprint_backtrace_build_id),
+ KUNIT_CASE(test_sprint_symbol_no_offset),
+ /* Group D: printk format specifiers */
+ KUNIT_CASE(test_pS_format),
+ KUNIT_CASE(test_pBb_format),
+ KUNIT_CASE(test_pSR_format),
+ /* Group E: Address edge cases */
+ KUNIT_CASE(test_symbol_start_addr),
+ KUNIT_CASE(test_symbol_nonzero_offset),
+ KUNIT_CASE(test_unknown_address),
+ KUNIT_CASE(test_kernel_function_lineinfo),
+ KUNIT_CASE(test_assembly_no_lineinfo),
+ /* Group F: Module path */
+ KUNIT_CASE(test_module_function_lineinfo),
+ /* Group G: Stress */
+ KUNIT_CASE_SLOW(test_concurrent_sprint_symbol),
+ KUNIT_CASE_SLOW(test_rapid_sprint_symbol),
+ /* Group H: Safety and plausibility */
+ KUNIT_CASE(test_line_number_plausible),
+ KUNIT_CASE(test_buffer_no_overflow),
+ KUNIT_CASE(test_dump_stack_no_crash),
+ KUNIT_CASE(test_sprint_symbol_build_id),
+ /* Group I: Encoding/decoding and accuracy */
+ KUNIT_CASE(test_sleb128_edge_cases),
+ KUNIT_CASE(test_uleb128_edge_cases),
+ KUNIT_CASE(test_line_number_accuracy),
+ KUNIT_CASE(test_many_lines_mid_function),
+ {}
+};
+
+static struct kunit_suite lineinfo_test_suite = {
+ .name = "lineinfo",
+ .test_cases = lineinfo_test_cases,
+};
+kunit_test_suites(&lineinfo_test_suite);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("KUnit tests for kallsyms lineinfo");
+MODULE_AUTHOR("Sasha Levin");
--
2.51.0
^ permalink raw reply related
* [PATCH v4 3/4] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction
From: Sasha Levin @ 2026-03-22 13:15 UTC (permalink / raw)
To: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
James Bottomley
Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, Helge Deller,
Randy Dunlap, Laurent Pinchart, Vivian Wang, linux-kernel,
linux-kbuild, linux-modules, linux-doc, Sasha Levin
In-Reply-To: <20260322131543.971079-1-sashal@kernel.org>
Replace the flat uncompressed parallel arrays (lineinfo_addrs[],
lineinfo_file_ids[], lineinfo_lines[]) with a block-indexed,
delta-encoded, ULEB128 varint compressed format.
The sorted address array has small deltas between consecutive entries
(typically 1-50 bytes), file IDs have high locality (delta often 0,
same file), and line numbers change slowly. Delta-encoding followed
by ULEB128 varint compression shrinks most values from 4 bytes to 1.
Entries are grouped into blocks of 64. A small uncompressed block
index (first addr + byte offset per block) enables O(log(N/64)) binary
search, followed by sequential decode of at most 64 varints within the
matching block. All decode state lives on the stack -- zero
allocations, still safe for NMI/panic context.
Measured on a defconfig+debug x86_64 build (3,017,154 entries, 4,822
source files, 47,144 blocks):
Before (flat arrays):
lineinfo_addrs[] 12,068,616 bytes (u32 x 3.0M)
lineinfo_file_ids[] 6,034,308 bytes (u16 x 3.0M)
lineinfo_lines[] 12,068,616 bytes (u32 x 3.0M)
Total: 30,171,540 bytes (28.8 MiB, 10.0 bytes/entry)
After (block-indexed delta + ULEB128):
lineinfo_block_addrs[] 188,576 bytes (184 KiB)
lineinfo_block_offsets[] 188,576 bytes (184 KiB)
lineinfo_data[] 10,926,128 bytes (10.4 MiB)
Total: 11,303,280 bytes (10.8 MiB, 3.7 bytes/entry)
Savings: 18.0 MiB (2.7x reduction)
Booted in QEMU and verified with SysRq-l that annotations still work:
default_idle+0x9/0x10 (arch/x86/kernel/process.c:767)
default_idle_call+0x6c/0xb0 (kernel/sched/idle.c:122)
do_idle+0x335/0x490 (kernel/sched/idle.c:191)
cpu_startup_entry+0x4e/0x60 (kernel/sched/idle.c:429)
rest_init+0x1aa/0x1b0 (init/main.c:760)
Suggested-by: Juergen Gross <jgross@suse.com>
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
.../admin-guide/kallsyms-lineinfo.rst | 7 +-
include/linux/mod_lineinfo.h | 227 ++++++++++++++++--
init/Kconfig | 8 +-
kernel/kallsyms.c | 46 ++--
kernel/kallsyms_internal.h | 8 +-
kernel/module/kallsyms.c | 85 +++----
scripts/empty_lineinfo.S | 20 +-
scripts/gen_lineinfo.c | 189 +++++++++------
scripts/kallsyms.c | 7 +-
9 files changed, 406 insertions(+), 191 deletions(-)
diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
index 5cae995eb118e..dd264830c8d5b 100644
--- a/Documentation/admin-guide/kallsyms-lineinfo.rst
+++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
@@ -76,10 +76,11 @@ Memory Overhead
===============
The vmlinux lineinfo tables are stored in ``.rodata`` and typically add
-approximately 44 MiB to the kernel image for a standard configuration
-(~4.6 million DWARF line entries, ~10 bytes per entry after deduplication).
+approximately 10-15 MiB to the kernel image for a standard configuration
+(~4.6 million DWARF line entries, ~2-3 bytes per entry after delta
+compression).
-Per-module lineinfo adds approximately 10 bytes per DWARF line entry to each
+Per-module lineinfo adds approximately 2-3 bytes per DWARF line entry to each
``.ko`` file.
Known Limitations
diff --git a/include/linux/mod_lineinfo.h b/include/linux/mod_lineinfo.h
index d62e9608f0f82..364e5d81fe5bb 100644
--- a/include/linux/mod_lineinfo.h
+++ b/include/linux/mod_lineinfo.h
@@ -8,13 +8,23 @@
*
* Section layout (all values in target-native endianness):
*
- * struct mod_lineinfo_header (16 bytes)
- * u32 addrs[num_entries] -- offsets from .text base, sorted
- * u16 file_ids[num_entries] -- parallel to addrs
- * <2-byte pad if num_entries is odd>
- * u32 lines[num_entries] -- parallel to addrs
+ * struct mod_lineinfo_header
+ * u32 block_addrs[num_blocks] -- first addr per block, for binary search
+ * u32 block_offsets[num_blocks] -- byte offset into compressed data stream
+ * u8 data[data_size] -- LEB128 delta-compressed entries
* u32 file_offsets[num_files] -- byte offset into filenames[]
* char filenames[filenames_size] -- concatenated NUL-terminated strings
+ *
+ * Each sub-array is located by an explicit (offset, size) pair in the
+ * header, similar to a flattened devicetree. This makes bounds checking
+ * straightforward: validate offset + size <= section_size for each array.
+ *
+ * Compressed stream format (per block of LINEINFO_BLOCK_ENTRIES entries):
+ * Entry 0: file_id (ULEB128), line (ULEB128)
+ * addr is in block_addrs[]
+ * Entry 1..N: addr_delta (ULEB128),
+ * file_id_delta (SLEB128),
+ * line_delta (SLEB128)
*/
#ifndef _LINUX_MOD_LINEINFO_H
#define _LINUX_MOD_LINEINFO_H
@@ -25,44 +35,209 @@
#include <stdint.h>
typedef uint32_t u32;
typedef uint16_t u16;
+typedef uint8_t u8;
#endif
+#define LINEINFO_BLOCK_ENTRIES 64
+
struct mod_lineinfo_header {
u32 num_entries;
+ u32 num_blocks;
u32 num_files;
- u32 filenames_size; /* total bytes of concatenated filenames */
- u32 reserved; /* padding, must be 0 */
+ u32 blocks_offset; /* offset to block_addrs[] from section start */
+ u32 blocks_size; /* bytes: num_blocks * 2 * sizeof(u32) */
+ u32 data_offset; /* offset to compressed stream */
+ u32 data_size; /* bytes of compressed data */
+ u32 files_offset; /* offset to file_offsets[] */
+ u32 files_size; /* bytes: num_files * sizeof(u32) */
+ u32 filenames_offset;
+ u32 filenames_size;
+ u32 reserved; /* must be 0 */
};
-/* Offset helpers: compute byte offset from start of section to each array */
+/*
+ * Descriptor for a lineinfo table, used by the shared lookup function.
+ * Callers populate this from either linker globals (vmlinux) or a
+ * validated mod_lineinfo_header (modules).
+ */
+struct lineinfo_table {
+ const u32 *blk_addrs;
+ const u32 *blk_offsets;
+ const u8 *data;
+ u32 data_size;
+ const u32 *file_offsets;
+ const char *filenames;
+ u32 num_entries;
+ u32 num_blocks;
+ u32 num_files;
+ u32 filenames_size;
+};
-static inline u32 mod_lineinfo_addrs_off(void)
+/*
+ * Read a ULEB128 varint from a byte stream.
+ * Returns the decoded value and advances *pos past the encoded bytes.
+ * If *pos would exceed 'end', returns 0 and sets *pos = end (safe for
+ * NMI/panic context: no crash, just a missed annotation).
+ */
+static inline u32 lineinfo_read_uleb128(const u8 *data, u32 *pos, u32 end)
{
- return sizeof(struct mod_lineinfo_header);
-}
+ u32 result = 0;
+ unsigned int shift = 0;
-static inline u32 mod_lineinfo_file_ids_off(u32 num_entries)
-{
- return mod_lineinfo_addrs_off() + num_entries * sizeof(u32);
+ while (*pos < end) {
+ u8 byte = data[*pos];
+ (*pos)++;
+ result |= (u32)(byte & 0x7f) << shift;
+ if (!(byte & 0x80))
+ return result;
+ shift += 7;
+ if (shift >= 32) {
+ /* Malformed: skip remaining continuation bytes */
+ while (*pos < end && (data[*pos] & 0x80))
+ (*pos)++;
+ if (*pos < end)
+ (*pos)++;
+ return result;
+ }
+ }
+ return result;
}
-static inline u32 mod_lineinfo_lines_off(u32 num_entries)
+/* Read an SLEB128 varint. Same safety guarantees as above. */
+static inline int32_t lineinfo_read_sleb128(const u8 *data, u32 *pos, u32 end)
{
- /* u16 file_ids[] may need 2-byte padding to align lines[] to 4 bytes */
- u32 off = mod_lineinfo_file_ids_off(num_entries) +
- num_entries * sizeof(u16);
- return (off + 3) & ~3u;
-}
+ int32_t result = 0;
+ unsigned int shift = 0;
+ u8 byte = 0;
-static inline u32 mod_lineinfo_file_offsets_off(u32 num_entries)
-{
- return mod_lineinfo_lines_off(num_entries) + num_entries * sizeof(u32);
+ while (*pos < end) {
+ byte = data[*pos];
+ (*pos)++;
+ result |= (int32_t)(byte & 0x7f) << shift;
+ shift += 7;
+ if (!(byte & 0x80))
+ break;
+ if (shift >= 32) {
+ while (*pos < end && (data[*pos] & 0x80))
+ (*pos)++;
+ if (*pos < end)
+ (*pos)++;
+ return result;
+ }
+ }
+
+ /* Sign-extend if the high bit of the last byte was set */
+ if (shift < 32 && (byte & 0x40))
+ result |= -(1 << shift);
+
+ return result;
}
-static inline u32 mod_lineinfo_filenames_off(u32 num_entries, u32 num_files)
+/*
+ * Search a lineinfo table for the source file and line corresponding to a
+ * given offset (from _text for vmlinux, from .text base for modules).
+ *
+ * Safe for NMI and panic context: no locks, no allocations, all state on stack.
+ * Returns true and sets @file and @line on success; false on any failure.
+ */
+static inline bool lineinfo_search(const struct lineinfo_table *tbl,
+ unsigned int offset,
+ const char **file, unsigned int *line)
{
- return mod_lineinfo_file_offsets_off(num_entries) +
- num_files * sizeof(u32);
+ unsigned int low, high, mid, block;
+ unsigned int cur_addr, cur_file_id, cur_line;
+ unsigned int best_file_id = 0, best_line = 0;
+ unsigned int block_entries, data_end;
+ bool found = false;
+ u32 pos;
+
+ if (!tbl->num_entries || !tbl->num_blocks)
+ return false;
+
+ /* Binary search on blk_addrs[] to find the right block */
+ low = 0;
+ high = tbl->num_blocks;
+ while (low < high) {
+ mid = low + (high - low) / 2;
+ if (tbl->blk_addrs[mid] <= offset)
+ low = mid + 1;
+ else
+ high = mid;
+ }
+
+ if (low == 0)
+ return false;
+ block = low - 1;
+
+ /* How many entries in this block? */
+ block_entries = LINEINFO_BLOCK_ENTRIES;
+ if (block == tbl->num_blocks - 1) {
+ unsigned int remaining = tbl->num_entries -
+ block * LINEINFO_BLOCK_ENTRIES;
+
+ if (remaining < block_entries)
+ block_entries = remaining;
+ }
+
+ /* Determine end of this block's data in the compressed stream */
+ if (block + 1 < tbl->num_blocks)
+ data_end = tbl->blk_offsets[block + 1];
+ else
+ data_end = tbl->data_size;
+
+ /* Clamp data_end to actual data size */
+ if (data_end > tbl->data_size)
+ data_end = tbl->data_size;
+
+ /* Decode entry 0: addr from blk_addrs, file_id and line from stream */
+ pos = tbl->blk_offsets[block];
+ if (pos >= data_end)
+ return false;
+
+ cur_addr = tbl->blk_addrs[block];
+ cur_file_id = lineinfo_read_uleb128(tbl->data, &pos, data_end);
+ cur_line = lineinfo_read_uleb128(tbl->data, &pos, data_end);
+
+ /* Check entry 0 */
+ if (cur_addr <= offset) {
+ best_file_id = cur_file_id;
+ best_line = cur_line;
+ found = true;
+ }
+
+ /* Decode entries 1..N */
+ for (unsigned int i = 1; i < block_entries; i++) {
+ unsigned int addr_delta;
+ int32_t file_delta, line_delta;
+
+ addr_delta = lineinfo_read_uleb128(tbl->data, &pos, data_end);
+ file_delta = lineinfo_read_sleb128(tbl->data, &pos, data_end);
+ line_delta = lineinfo_read_sleb128(tbl->data, &pos, data_end);
+
+ cur_addr += addr_delta;
+ cur_file_id = (unsigned int)((int32_t)cur_file_id + file_delta);
+ cur_line = (unsigned int)((int32_t)cur_line + line_delta);
+
+ if (cur_addr > offset)
+ break;
+
+ best_file_id = cur_file_id;
+ best_line = cur_line;
+ found = true;
+ }
+
+ if (!found)
+ return false;
+
+ if (best_file_id >= tbl->num_files)
+ return false;
+
+ if (tbl->file_offsets[best_file_id] >= tbl->filenames_size)
+ return false;
+
+ *file = &tbl->filenames[tbl->file_offsets[best_file_id]];
+ *line = best_line;
+ return true;
}
#endif /* _LINUX_MOD_LINEINFO_H */
diff --git a/init/Kconfig b/init/Kconfig
index bf53275bc405a..6e3795b3dbd62 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2065,8 +2065,9 @@ config KALLSYMS_LINEINFO
anon_vma_clone+0x2ed/0xcf0 (mm/rmap.c:412)
This requires elfutils (libdw-dev/elfutils-devel) on the build host.
- Adds approximately 44MB to a typical kernel image (10 bytes per
- DWARF line-table entry, ~4.6M entries for a typical config).
+ Adds approximately 10-15MB to a typical kernel image (~2-3 bytes
+ per entry after delta compression, ~4.6M entries for a typical
+ config).
If unsure, say N.
@@ -2079,7 +2080,8 @@ config KALLSYMS_LINEINFO_MODULES
so stack traces from module code include (file.c:123) annotations.
Requires elfutils (libdw-dev/elfutils-devel) on the build host.
- Increases .ko sizes by approximately 10 bytes per DWARF line entry.
+ Increases .ko sizes by approximately 2-3 bytes per DWARF line
+ entry after delta compression.
If unsure, say N.
diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
index 9df92b0fd9041..76e30cac3a277 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -467,13 +467,16 @@ static int append_buildid(char *buffer, const char *modname,
#endif /* CONFIG_STACKTRACE_BUILD_ID */
+#include <linux/mod_lineinfo.h>
+
bool kallsyms_lookup_lineinfo(unsigned long addr,
const char **file, unsigned int *line)
{
+ struct lineinfo_table tbl;
unsigned long long raw_offset;
- unsigned int offset, low, high, mid, file_id;
- if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) || !lineinfo_num_entries)
+ if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) ||
+ !lineinfo_num_entries || !lineinfo_num_blocks)
return false;
/* Compute offset from _text */
@@ -483,34 +486,19 @@ bool kallsyms_lookup_lineinfo(unsigned long addr,
raw_offset = addr - (unsigned long)_text;
if (raw_offset > UINT_MAX)
return false;
- offset = (unsigned int)raw_offset;
-
- /* Binary search for largest entry <= offset */
- low = 0;
- high = lineinfo_num_entries;
- while (low < high) {
- mid = low + (high - low) / 2;
- if (lineinfo_addrs[mid] <= offset)
- low = mid + 1;
- else
- high = mid;
- }
-
- if (low == 0)
- return false;
- low--;
-
- file_id = lineinfo_file_ids[low];
- *line = lineinfo_lines[low];
-
- if (file_id >= lineinfo_num_files)
- return false;
-
- if (lineinfo_file_offsets[file_id] >= lineinfo_filenames_size)
- return false;
- *file = &lineinfo_filenames[lineinfo_file_offsets[file_id]];
- return true;
+ tbl.blk_addrs = lineinfo_block_addrs;
+ tbl.blk_offsets = lineinfo_block_offsets;
+ tbl.data = lineinfo_data;
+ tbl.data_size = lineinfo_data_size;
+ tbl.file_offsets = lineinfo_file_offsets;
+ tbl.filenames = lineinfo_filenames;
+ tbl.num_entries = lineinfo_num_entries;
+ tbl.num_blocks = lineinfo_num_blocks;
+ tbl.num_files = lineinfo_num_files;
+ tbl.filenames_size = lineinfo_filenames_size;
+
+ return lineinfo_search(&tbl, (unsigned int)raw_offset, file, line);
}
/* Look up a kernel symbol and return it in a text buffer. */
diff --git a/kernel/kallsyms_internal.h b/kernel/kallsyms_internal.h
index d7374ce444d81..ffe4c658067ec 100644
--- a/kernel/kallsyms_internal.h
+++ b/kernel/kallsyms_internal.h
@@ -16,10 +16,12 @@ extern const unsigned int kallsyms_markers[];
extern const u8 kallsyms_seqs_of_names[];
extern const u32 lineinfo_num_entries;
-extern const u32 lineinfo_addrs[];
-extern const u16 lineinfo_file_ids[];
-extern const u32 lineinfo_lines[];
extern const u32 lineinfo_num_files;
+extern const u32 lineinfo_num_blocks;
+extern const u32 lineinfo_block_addrs[];
+extern const u32 lineinfo_block_offsets[];
+extern const u32 lineinfo_data_size;
+extern const u8 lineinfo_data[];
extern const u32 lineinfo_file_offsets[];
extern const u32 lineinfo_filenames_size;
extern const char lineinfo_filenames[];
diff --git a/kernel/module/kallsyms.c b/kernel/module/kallsyms.c
index 5b46293e957ab..8715a923ba536 100644
--- a/kernel/module/kallsyms.c
+++ b/kernel/module/kallsyms.c
@@ -509,16 +509,11 @@ bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
const char **file, unsigned int *line)
{
const struct mod_lineinfo_header *hdr;
+ struct lineinfo_table tbl;
const void *base;
- const u32 *addrs, *lines, *file_offsets;
- const u16 *file_ids;
- const char *filenames;
- u32 num_entries, num_files, filenames_size;
+ u32 section_size;
unsigned long text_base;
- unsigned int offset;
unsigned long long raw_offset;
- unsigned int low, high, mid;
- u16 file_id;
if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES))
return false;
@@ -527,61 +522,55 @@ bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
if (!base)
return false;
- if (mod->lineinfo_data_size < sizeof(*hdr))
+ section_size = mod->lineinfo_data_size;
+ if (section_size < sizeof(*hdr))
return false;
hdr = base;
- num_entries = hdr->num_entries;
- num_files = hdr->num_files;
- filenames_size = hdr->filenames_size;
- if (num_entries == 0)
+ if (hdr->num_entries == 0 || hdr->num_blocks == 0)
return false;
- /* Validate section is large enough for all arrays */
- if (mod->lineinfo_data_size <
- mod_lineinfo_filenames_off(num_entries, num_files) + filenames_size)
+ /* Validate each sub-array fits within the section */
+ if (hdr->blocks_offset + hdr->blocks_size > section_size)
return false;
-
- addrs = base + mod_lineinfo_addrs_off();
- file_ids = base + mod_lineinfo_file_ids_off(num_entries);
- lines = base + mod_lineinfo_lines_off(num_entries);
- file_offsets = base + mod_lineinfo_file_offsets_off(num_entries);
- filenames = base + mod_lineinfo_filenames_off(num_entries, num_files);
-
- /* Compute offset from module .text base */
- text_base = (unsigned long)mod->mem[MOD_TEXT].base;
- if (addr < text_base)
+ if (hdr->data_offset + hdr->data_size > section_size)
return false;
-
- raw_offset = addr - text_base;
- if (raw_offset > UINT_MAX)
+ if (hdr->files_offset + hdr->files_size > section_size)
+ return false;
+ if (hdr->filenames_offset + hdr->filenames_size > section_size)
return false;
- offset = (unsigned int)raw_offset;
-
- /* Binary search for largest entry <= offset */
- low = 0;
- high = num_entries;
- while (low < high) {
- mid = low + (high - low) / 2;
- if (addrs[mid] <= offset)
- low = mid + 1;
- else
- high = mid;
- }
- if (low == 0)
+ /* Validate array sizes match declared counts */
+ if (hdr->blocks_size < hdr->num_blocks * 2 * sizeof(u32))
+ return false;
+ if (hdr->files_size < hdr->num_files * sizeof(u32))
return false;
- low--;
- file_id = file_ids[low];
- if (file_id >= num_files)
+ /*
+ * Compute offset from module .text base.
+ * NOTE: This assumes .text is at the start of the MOD_TEXT segment.
+ * A proper fix would use ELF relocations to reference .text directly.
+ */
+ text_base = (unsigned long)mod->mem[MOD_TEXT].base;
+ if (addr < text_base)
return false;
- if (file_offsets[file_id] >= filenames_size)
+ raw_offset = addr - text_base;
+ if (raw_offset > U32_MAX)
return false;
- *file = &filenames[file_offsets[file_id]];
- *line = lines[low];
- return true;
+ tbl.blk_addrs = base + hdr->blocks_offset;
+ tbl.blk_offsets = base + hdr->blocks_offset +
+ hdr->num_blocks * sizeof(u32);
+ tbl.data = base + hdr->data_offset;
+ tbl.data_size = hdr->data_size;
+ tbl.file_offsets = base + hdr->files_offset;
+ tbl.filenames = base + hdr->filenames_offset;
+ tbl.num_entries = hdr->num_entries;
+ tbl.num_blocks = hdr->num_blocks;
+ tbl.num_files = hdr->num_files;
+ tbl.filenames_size = hdr->filenames_size;
+
+ return lineinfo_search(&tbl, (unsigned int)raw_offset, file, line);
}
diff --git a/scripts/empty_lineinfo.S b/scripts/empty_lineinfo.S
index e058c41137123..edd5b1092f050 100644
--- a/scripts/empty_lineinfo.S
+++ b/scripts/empty_lineinfo.S
@@ -14,12 +14,20 @@ lineinfo_num_entries:
.balign 4
lineinfo_num_files:
.long 0
- .globl lineinfo_addrs
-lineinfo_addrs:
- .globl lineinfo_file_ids
-lineinfo_file_ids:
- .globl lineinfo_lines
-lineinfo_lines:
+ .globl lineinfo_num_blocks
+ .balign 4
+lineinfo_num_blocks:
+ .long 0
+ .globl lineinfo_block_addrs
+lineinfo_block_addrs:
+ .globl lineinfo_block_offsets
+lineinfo_block_offsets:
+ .globl lineinfo_data_size
+ .balign 4
+lineinfo_data_size:
+ .long 0
+ .globl lineinfo_data
+lineinfo_data:
.globl lineinfo_file_offsets
lineinfo_file_offsets:
.globl lineinfo_filenames_size
diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c
index 7d06701549345..45b1c1081164d 100644
--- a/scripts/gen_lineinfo.c
+++ b/scripts/gen_lineinfo.c
@@ -548,6 +548,35 @@ static void deduplicate(void)
num_entries = j + 1;
}
+/*
+ * Emit the LEB128 delta-compressed data stream for one block.
+ * Uses .uleb128/.sleb128 assembler directives for encoding.
+ */
+static void emit_block_data(unsigned int block)
+{
+ unsigned int base = block * LINEINFO_BLOCK_ENTRIES;
+ unsigned int count = num_entries - base;
+
+ if (count > LINEINFO_BLOCK_ENTRIES)
+ count = LINEINFO_BLOCK_ENTRIES;
+
+ /* Entry 0: file_id, line (both unsigned) */
+ printf("\t.uleb128 %u\n", entries[base].file_id);
+ printf("\t.uleb128 %u\n", entries[base].line);
+
+ /* Entries 1..N: addr_delta (unsigned), file/line deltas (signed) */
+ for (unsigned int i = 1; i < count; i++) {
+ unsigned int idx = base + i;
+
+ printf("\t.uleb128 %u\n",
+ entries[idx].offset - entries[idx - 1].offset);
+ printf("\t.sleb128 %d\n",
+ (int)entries[idx].file_id - (int)entries[idx - 1].file_id);
+ printf("\t.sleb128 %d\n",
+ (int)entries[idx].line - (int)entries[idx - 1].line);
+ }
+}
+
static void compute_file_offsets(void)
{
unsigned int offset = 0;
@@ -571,6 +600,11 @@ static void print_escaped_asciz(const char *s)
static void output_assembly(void)
{
+ unsigned int num_blocks;
+
+ num_blocks = num_entries ?
+ (num_entries + LINEINFO_BLOCK_ENTRIES - 1) / LINEINFO_BLOCK_ENTRIES : 0;
+
printf("/* SPDX-License-Identifier: GPL-2.0 */\n");
printf("/*\n");
printf(" * Automatically generated by scripts/gen_lineinfo\n");
@@ -591,29 +625,40 @@ static void output_assembly(void)
printf("lineinfo_num_files:\n");
printf("\t.long %u\n\n", num_files);
- /* Sorted address offsets from _text */
- printf("\t.globl lineinfo_addrs\n");
+ /* Number of blocks */
+ printf("\t.globl lineinfo_num_blocks\n");
+ printf("\t.balign 4\n");
+ printf("lineinfo_num_blocks:\n");
+ printf("\t.long %u\n\n", num_blocks);
+
+ /* Block first-addresses for binary search */
+ printf("\t.globl lineinfo_block_addrs\n");
printf("\t.balign 4\n");
- printf("lineinfo_addrs:\n");
- for (unsigned int i = 0; i < num_entries; i++)
- printf("\t.long 0x%x\n", entries[i].offset);
- printf("\n");
-
- /* File IDs, parallel to addrs (u16 -- supports up to 65535 files) */
- printf("\t.globl lineinfo_file_ids\n");
- printf("\t.balign 2\n");
- printf("lineinfo_file_ids:\n");
- for (unsigned int i = 0; i < num_entries; i++)
- printf("\t.short %u\n", entries[i].file_id);
- printf("\n");
-
- /* Line numbers, parallel to addrs */
- printf("\t.globl lineinfo_lines\n");
+ printf("lineinfo_block_addrs:\n");
+ for (unsigned int i = 0; i < num_blocks; i++)
+ printf("\t.long 0x%x\n", entries[i * LINEINFO_BLOCK_ENTRIES].offset);
+
+ /* Block byte offsets into compressed stream */
+ printf("\t.globl lineinfo_block_offsets\n");
printf("\t.balign 4\n");
- printf("lineinfo_lines:\n");
- for (unsigned int i = 0; i < num_entries; i++)
- printf("\t.long %u\n", entries[i].line);
- printf("\n");
+ printf("lineinfo_block_offsets:\n");
+ for (unsigned int i = 0; i < num_blocks; i++)
+ printf("\t.long .Lblock_%u - lineinfo_data\n", i);
+
+ /* Compressed data size */
+ printf("\t.globl lineinfo_data_size\n");
+ printf("\t.balign 4\n");
+ printf("lineinfo_data_size:\n");
+ printf("\t.long .Ldata_end - lineinfo_data\n\n");
+
+ /* Compressed data stream */
+ printf("\t.globl lineinfo_data\n");
+ printf("lineinfo_data:\n");
+ for (unsigned int i = 0; i < num_blocks; i++) {
+ printf(".Lblock_%u:\n", i);
+ emit_block_data(i);
+ }
+ printf(".Ldata_end:\n\n");
/* File string offset table */
printf("\t.globl lineinfo_file_offsets\n");
@@ -621,34 +666,27 @@ static void output_assembly(void)
printf("lineinfo_file_offsets:\n");
for (unsigned int i = 0; i < num_files; i++)
printf("\t.long %u\n", files[i].str_offset);
- printf("\n");
/* Filenames size */
- {
- unsigned int fsize = 0;
-
- for (unsigned int i = 0; i < num_files; i++)
- fsize += strlen(files[i].name) + 1;
- printf("\t.globl lineinfo_filenames_size\n");
- printf("\t.balign 4\n");
- printf("lineinfo_filenames_size:\n");
- printf("\t.long %u\n\n", fsize);
- }
+ printf("\t.globl lineinfo_filenames_size\n");
+ printf("\t.balign 4\n");
+ printf("lineinfo_filenames_size:\n");
+ printf("\t.long .Lfilenames_end - lineinfo_filenames\n\n");
/* Concatenated NUL-terminated filenames */
printf("\t.globl lineinfo_filenames\n");
printf("lineinfo_filenames:\n");
for (unsigned int i = 0; i < num_files; i++)
print_escaped_asciz(files[i].name);
- printf("\n");
+ printf(".Lfilenames_end:\n");
}
static void output_module_assembly(void)
{
- unsigned int filenames_size = 0;
+ unsigned int num_blocks;
- for (unsigned int i = 0; i < num_files; i++)
- filenames_size += strlen(files[i].name) + 1;
+ num_blocks = num_entries ?
+ (num_entries + LINEINFO_BLOCK_ENTRIES - 1) / LINEINFO_BLOCK_ENTRIES : 0;
printf("/* SPDX-License-Identifier: GPL-2.0 */\n");
printf("/*\n");
@@ -658,46 +696,56 @@ static void output_module_assembly(void)
printf("\t.section .mod_lineinfo, \"a\"\n\n");
- /* Header: num_entries, num_files, filenames_size, reserved */
+ /*
+ * Header -- offsets and sizes are assembler expressions so the
+ * layout is self-describing without manual C arithmetic.
+ */
+ printf(".Lhdr:\n");
printf("\t.balign 4\n");
- printf("\t.long %u\n", num_entries);
- printf("\t.long %u\n", num_files);
- printf("\t.long %u\n", filenames_size);
- printf("\t.long 0\n\n");
-
- /* addrs[] */
- for (unsigned int i = 0; i < num_entries; i++)
- printf("\t.long 0x%x\n", entries[i].offset);
- if (num_entries)
- printf("\n");
-
- /* file_ids[] */
- for (unsigned int i = 0; i < num_entries; i++)
- printf("\t.short %u\n", entries[i].file_id);
-
- /* Padding to align lines[] to 4 bytes */
- if (num_entries & 1)
- printf("\t.short 0\n");
- if (num_entries)
- printf("\n");
-
- /* lines[] */
- for (unsigned int i = 0; i < num_entries; i++)
- printf("\t.long %u\n", entries[i].line);
- if (num_entries)
- printf("\n");
+ printf("\t.long %u\t\t\t\t/* num_entries */\n", num_entries);
+ printf("\t.long %u\t\t\t\t/* num_blocks */\n", num_blocks);
+ printf("\t.long %u\t\t\t\t/* num_files */\n", num_files);
+ printf("\t.long .Lblk_addrs - .Lhdr\t\t/* blocks_offset */\n");
+ printf("\t.long .Lblk_offsets_end - .Lblk_addrs\t/* blocks_size */\n");
+ printf("\t.long .Ldata - .Lhdr\t\t\t/* data_offset */\n");
+ printf("\t.long .Ldata_end - .Ldata\t\t/* data_size */\n");
+ printf("\t.long .Lfile_offsets - .Lhdr\t\t/* files_offset */\n");
+ printf("\t.long .Lfile_offsets_end - .Lfile_offsets /* files_size */\n");
+ printf("\t.long .Lfilenames - .Lhdr\t\t/* filenames_offset */\n");
+ printf("\t.long .Lfilenames_end - .Lfilenames\t/* filenames_size */\n");
+ printf("\t.long 0\t\t\t\t\t/* reserved */\n\n");
+
+ /* block_addrs[] */
+ printf(".Lblk_addrs:\n");
+ for (unsigned int i = 0; i < num_blocks; i++)
+ printf("\t.long 0x%x\n", entries[i * LINEINFO_BLOCK_ENTRIES].offset);
+
+ /* block_offsets[] */
+ printf(".Lblk_offsets:\n");
+ for (unsigned int i = 0; i < num_blocks; i++)
+ printf("\t.long .Lblock_%u - .Ldata\n", i);
+ printf(".Lblk_offsets_end:\n\n");
+
+ /* compressed data stream */
+ printf(".Ldata:\n");
+ for (unsigned int i = 0; i < num_blocks; i++) {
+ printf(".Lblock_%u:\n", i);
+ emit_block_data(i);
+ }
+ printf(".Ldata_end:\n");
/* file_offsets[] */
+ printf("\t.balign 4\n");
+ printf(".Lfile_offsets:\n");
for (unsigned int i = 0; i < num_files; i++)
printf("\t.long %u\n", files[i].str_offset);
- if (num_files)
- printf("\n");
+ printf(".Lfile_offsets_end:\n\n");
/* filenames[] */
+ printf(".Lfilenames:\n");
for (unsigned int i = 0; i < num_files; i++)
print_escaped_asciz(files[i].name);
- if (num_files)
- printf("\n");
+ printf(".Lfilenames_end:\n");
}
int main(int argc, char *argv[])
@@ -777,8 +825,10 @@ int main(int argc, char *argv[])
deduplicate();
compute_file_offsets();
- fprintf(stderr, "lineinfo: %u entries, %u files\n",
- num_entries, num_files);
+ fprintf(stderr, "lineinfo: %u entries, %u files, %u blocks\n",
+ num_entries, num_files,
+ num_entries ?
+ (num_entries + LINEINFO_BLOCK_ENTRIES - 1) / LINEINFO_BLOCK_ENTRIES : 0);
if (module_mode)
output_module_assembly();
@@ -794,6 +844,5 @@ int main(int argc, char *argv[])
for (unsigned int i = 0; i < num_files; i++)
free(files[i].name);
free(files);
-
return 0;
}
diff --git a/scripts/kallsyms.c b/scripts/kallsyms.c
index 42662c4fbc6c9..94fbdad3df7c6 100644
--- a/scripts/kallsyms.c
+++ b/scripts/kallsyms.c
@@ -80,11 +80,12 @@ static bool is_ignored_symbol(const char *name, char type)
{
/* Ignore lineinfo symbols for kallsyms pass stability */
static const char * const lineinfo_syms[] = {
- "lineinfo_addrs",
- "lineinfo_file_ids",
+ "lineinfo_block_addrs",
+ "lineinfo_block_offsets",
+ "lineinfo_data",
"lineinfo_file_offsets",
"lineinfo_filenames",
- "lineinfo_lines",
+ "lineinfo_num_blocks",
"lineinfo_num_entries",
"lineinfo_num_files",
};
--
2.51.0
^ permalink raw reply related
* [PATCH v4 2/4] kallsyms: extend lineinfo to loadable modules
From: Sasha Levin @ 2026-03-22 13:15 UTC (permalink / raw)
To: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
James Bottomley
Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, Helge Deller,
Randy Dunlap, Laurent Pinchart, Vivian Wang, linux-kernel,
linux-kbuild, linux-modules, linux-doc, Sasha Levin
In-Reply-To: <20260322131543.971079-1-sashal@kernel.org>
Add CONFIG_KALLSYMS_LINEINFO_MODULES, which extends the
CONFIG_KALLSYMS_LINEINFO feature to loadable kernel modules.
At build time, each .ko is post-processed by scripts/gen-mod-lineinfo.sh
(modeled on gen-btf.sh) which runs scripts/gen_lineinfo --module on the
.ko, generates a .mod_lineinfo section containing a compact binary table
of .text-relative offsets, file IDs, line numbers, and filenames, and
embeds it back into the .ko via objcopy.
At runtime, module_lookup_lineinfo() performs a binary search on the
module's .mod_lineinfo section, and __sprint_symbol() calls it for
addresses that fall within a module. The lookup is NMI/panic-safe
(no locks, no allocations) — the data lives in read-only module memory
and is freed automatically when the module is unloaded.
The gen_lineinfo tool gains --module mode which:
- Uses .text section address as base (ET_REL files have no _text symbol)
- Filters entries to .text-only (excludes .init.text/.exit.text)
- Handles libdw's ET_REL path-doubling quirk in make_relative()
- Outputs a flat binary-format section instead of named global symbols
Per-module overhead is approximately 10 bytes per DWARF line entry.
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
.../admin-guide/kallsyms-lineinfo.rst | 40 +-
MAINTAINERS | 2 +
include/linux/mod_lineinfo.h | 68 ++++
include/linux/module.h | 5 +
init/Kconfig | 13 +
kernel/kallsyms.c | 18 +-
kernel/module/kallsyms.c | 91 +++++
kernel/module/main.c | 3 +
scripts/Makefile.modfinal | 6 +
scripts/gen-mod-lineinfo.sh | 48 +++
scripts/gen_lineinfo.c | 347 ++++++++++++++++--
11 files changed, 601 insertions(+), 40 deletions(-)
create mode 100644 include/linux/mod_lineinfo.h
create mode 100755 scripts/gen-mod-lineinfo.sh
diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
index c8ec124394354..5cae995eb118e 100644
--- a/Documentation/admin-guide/kallsyms-lineinfo.rst
+++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
@@ -51,22 +51,46 @@ With ``CONFIG_KALLSYMS_LINEINFO``::
Note that assembly routines (such as ``entry_SYSCALL_64_after_hwframe``) are
not annotated because they lack DWARF debug information.
+Module Support
+==============
+
+``CONFIG_KALLSYMS_LINEINFO_MODULES`` extends the feature to loadable kernel
+modules. When enabled, each ``.ko`` is post-processed at build time to embed
+a ``.mod_lineinfo`` section containing the same kind of address-to-source
+mapping.
+
+Enable in addition to the base options::
+
+ CONFIG_MODULES=y
+ CONFIG_KALLSYMS_LINEINFO_MODULES=y
+
+Stack traces from module code will then include annotations::
+
+ my_driver_func+0x30/0x100 [my_driver] (drivers/foo/bar.c:123)
+
+The ``.mod_lineinfo`` section is loaded into read-only module memory alongside
+the module text. No additional runtime memory allocation is required; the data
+is freed when the module is unloaded.
+
Memory Overhead
===============
-The lineinfo tables are stored in ``.rodata`` and typically add approximately
-44 MiB to the kernel image for a standard configuration (~4.6 million DWARF
-line entries, ~10 bytes per entry after deduplication).
+The vmlinux lineinfo tables are stored in ``.rodata`` and typically add
+approximately 44 MiB to the kernel image for a standard configuration
+(~4.6 million DWARF line entries, ~10 bytes per entry after deduplication).
+
+Per-module lineinfo adds approximately 10 bytes per DWARF line entry to each
+``.ko`` file.
Known Limitations
=================
-- **vmlinux only**: Only symbols in the core kernel image are annotated.
- Module symbols are not covered.
-- **4 GiB offset limit**: Address offsets from ``_text`` are stored as 32-bit
- values. Entries beyond 4 GiB from ``_text`` are skipped at build time with
- a warning.
+- **4 GiB offset limit**: Address offsets from ``_text`` (vmlinux) or
+ ``.text`` base (modules) are stored as 32-bit values. Entries beyond
+ 4 GiB are skipped at build time with a warning.
- **65535 file limit**: Source file IDs are stored as 16-bit values. Builds
with more than 65535 unique source files will fail with an error.
- **No assembly annotations**: Functions implemented in assembly that lack
DWARF ``.debug_line`` data are not annotated.
+- **No init text**: For modules, functions in ``.init.text`` are not annotated
+ because that memory is freed after module initialization.
diff --git a/MAINTAINERS b/MAINTAINERS
index f061e69b6e32a..535e992ca5a20 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13732,6 +13732,8 @@ KALLSYMS LINEINFO
M: Sasha Levin <sashal@kernel.org>
S: Maintained
F: Documentation/admin-guide/kallsyms-lineinfo.rst
+F: include/linux/mod_lineinfo.h
+F: scripts/gen-mod-lineinfo.sh
F: scripts/gen_lineinfo.c
KASAN
diff --git a/include/linux/mod_lineinfo.h b/include/linux/mod_lineinfo.h
new file mode 100644
index 0000000000000..d62e9608f0f82
--- /dev/null
+++ b/include/linux/mod_lineinfo.h
@@ -0,0 +1,68 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * mod_lineinfo.h - Binary format for per-module source line information
+ *
+ * This header defines the layout of the .mod_lineinfo section embedded
+ * in loadable kernel modules. It is dual-use: included from both the
+ * kernel and the userspace gen_lineinfo tool.
+ *
+ * Section layout (all values in target-native endianness):
+ *
+ * struct mod_lineinfo_header (16 bytes)
+ * u32 addrs[num_entries] -- offsets from .text base, sorted
+ * u16 file_ids[num_entries] -- parallel to addrs
+ * <2-byte pad if num_entries is odd>
+ * u32 lines[num_entries] -- parallel to addrs
+ * u32 file_offsets[num_files] -- byte offset into filenames[]
+ * char filenames[filenames_size] -- concatenated NUL-terminated strings
+ */
+#ifndef _LINUX_MOD_LINEINFO_H
+#define _LINUX_MOD_LINEINFO_H
+
+#ifdef __KERNEL__
+#include <linux/types.h>
+#else
+#include <stdint.h>
+typedef uint32_t u32;
+typedef uint16_t u16;
+#endif
+
+struct mod_lineinfo_header {
+ u32 num_entries;
+ u32 num_files;
+ u32 filenames_size; /* total bytes of concatenated filenames */
+ u32 reserved; /* padding, must be 0 */
+};
+
+/* Offset helpers: compute byte offset from start of section to each array */
+
+static inline u32 mod_lineinfo_addrs_off(void)
+{
+ return sizeof(struct mod_lineinfo_header);
+}
+
+static inline u32 mod_lineinfo_file_ids_off(u32 num_entries)
+{
+ return mod_lineinfo_addrs_off() + num_entries * sizeof(u32);
+}
+
+static inline u32 mod_lineinfo_lines_off(u32 num_entries)
+{
+ /* u16 file_ids[] may need 2-byte padding to align lines[] to 4 bytes */
+ u32 off = mod_lineinfo_file_ids_off(num_entries) +
+ num_entries * sizeof(u16);
+ return (off + 3) & ~3u;
+}
+
+static inline u32 mod_lineinfo_file_offsets_off(u32 num_entries)
+{
+ return mod_lineinfo_lines_off(num_entries) + num_entries * sizeof(u32);
+}
+
+static inline u32 mod_lineinfo_filenames_off(u32 num_entries, u32 num_files)
+{
+ return mod_lineinfo_file_offsets_off(num_entries) +
+ num_files * sizeof(u32);
+}
+
+#endif /* _LINUX_MOD_LINEINFO_H */
diff --git a/include/linux/module.h b/include/linux/module.h
index 14f391b186c6d..d23e0cd9c7210 100644
--- a/include/linux/module.h
+++ b/include/linux/module.h
@@ -508,6 +508,8 @@ struct module {
void *btf_data;
void *btf_base_data;
#endif
+ void *lineinfo_data; /* .mod_lineinfo section in MOD_RODATA */
+ unsigned int lineinfo_data_size;
#ifdef CONFIG_JUMP_LABEL
struct jump_entry *jump_entries;
unsigned int num_jump_entries;
@@ -1021,6 +1023,9 @@ static inline unsigned long find_kallsyms_symbol_value(struct module *mod,
#endif /* CONFIG_MODULES && CONFIG_KALLSYMS */
+bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
+ const char **file, unsigned int *line);
+
/* Define __free(module_put) macro for struct module *. */
DEFINE_FREE(module_put, struct module *, if (_T) module_put(_T))
diff --git a/init/Kconfig b/init/Kconfig
index c39f27e6393a8..bf53275bc405a 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2070,6 +2070,19 @@ config KALLSYMS_LINEINFO
If unsure, say N.
+config KALLSYMS_LINEINFO_MODULES
+ bool "Embed source file:line information in module stack traces"
+ depends on KALLSYMS_LINEINFO && MODULES
+ help
+ Extends KALLSYMS_LINEINFO to loadable kernel modules. Each .ko
+ gets a lineinfo table generated from its DWARF data at build time,
+ so stack traces from module code include (file.c:123) annotations.
+
+ Requires elfutils (libdw-dev/elfutils-devel) on the build host.
+ Increases .ko sizes by approximately 10 bytes per DWARF line entry.
+
+ If unsure, say N.
+
# end of the "standard kernel features (expert users)" menu
config ARCH_HAS_MEMBARRIER_CALLBACKS
diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
index d0a9cd9c6dace..9df92b0fd9041 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -543,12 +543,24 @@ static int __sprint_symbol(char *buffer, unsigned long address,
len += sprintf(buffer + len, "]");
}
- if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) && !modname) {
+ if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO)) {
const char *li_file;
unsigned int li_line;
+ bool found = false;
+
+ if (!modname)
+ found = kallsyms_lookup_lineinfo(address,
+ &li_file, &li_line);
+ else if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES)) {
+ struct module *mod = __module_address(address);
+
+ if (mod)
+ found = module_lookup_lineinfo(mod, address,
+ &li_file,
+ &li_line);
+ }
- if (kallsyms_lookup_lineinfo(address,
- &li_file, &li_line))
+ if (found)
len += snprintf(buffer + len, KSYM_SYMBOL_LEN - len,
" (%s:%u)", li_file, li_line);
}
diff --git a/kernel/module/kallsyms.c b/kernel/module/kallsyms.c
index 0fc11e45df9b9..5b46293e957ab 100644
--- a/kernel/module/kallsyms.c
+++ b/kernel/module/kallsyms.c
@@ -494,3 +494,94 @@ int module_kallsyms_on_each_symbol(const char *modname,
mutex_unlock(&module_mutex);
return ret;
}
+
+#include <linux/mod_lineinfo.h>
+
+/*
+ * Look up source file:line for an address within a loaded module.
+ * Uses the .mod_lineinfo section embedded in the .ko at build time.
+ *
+ * Safe in NMI/panic context: no locks, no allocations.
+ * Caller must hold RCU read lock (or be in a context where the module
+ * cannot be unloaded).
+ */
+bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
+ const char **file, unsigned int *line)
+{
+ const struct mod_lineinfo_header *hdr;
+ const void *base;
+ const u32 *addrs, *lines, *file_offsets;
+ const u16 *file_ids;
+ const char *filenames;
+ u32 num_entries, num_files, filenames_size;
+ unsigned long text_base;
+ unsigned int offset;
+ unsigned long long raw_offset;
+ unsigned int low, high, mid;
+ u16 file_id;
+
+ if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES))
+ return false;
+
+ base = mod->lineinfo_data;
+ if (!base)
+ return false;
+
+ if (mod->lineinfo_data_size < sizeof(*hdr))
+ return false;
+
+ hdr = base;
+ num_entries = hdr->num_entries;
+ num_files = hdr->num_files;
+ filenames_size = hdr->filenames_size;
+
+ if (num_entries == 0)
+ return false;
+
+ /* Validate section is large enough for all arrays */
+ if (mod->lineinfo_data_size <
+ mod_lineinfo_filenames_off(num_entries, num_files) + filenames_size)
+ return false;
+
+ addrs = base + mod_lineinfo_addrs_off();
+ file_ids = base + mod_lineinfo_file_ids_off(num_entries);
+ lines = base + mod_lineinfo_lines_off(num_entries);
+ file_offsets = base + mod_lineinfo_file_offsets_off(num_entries);
+ filenames = base + mod_lineinfo_filenames_off(num_entries, num_files);
+
+ /* Compute offset from module .text base */
+ text_base = (unsigned long)mod->mem[MOD_TEXT].base;
+ if (addr < text_base)
+ return false;
+
+ raw_offset = addr - text_base;
+ if (raw_offset > UINT_MAX)
+ return false;
+ offset = (unsigned int)raw_offset;
+
+ /* Binary search for largest entry <= offset */
+ low = 0;
+ high = num_entries;
+ while (low < high) {
+ mid = low + (high - low) / 2;
+ if (addrs[mid] <= offset)
+ low = mid + 1;
+ else
+ high = mid;
+ }
+
+ if (low == 0)
+ return false;
+ low--;
+
+ file_id = file_ids[low];
+ if (file_id >= num_files)
+ return false;
+
+ if (file_offsets[file_id] >= filenames_size)
+ return false;
+
+ *file = &filenames[file_offsets[file_id]];
+ *line = lines[low];
+ return true;
+}
diff --git a/kernel/module/main.c b/kernel/module/main.c
index 2bac4c7cd019a..d11646b02730a 100644
--- a/kernel/module/main.c
+++ b/kernel/module/main.c
@@ -2648,6 +2648,9 @@ static int find_module_sections(struct module *mod, struct load_info *info)
mod->btf_base_data = any_section_objs(info, ".BTF.base", 1,
&mod->btf_base_data_size);
#endif
+ if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES))
+ mod->lineinfo_data = any_section_objs(info, ".mod_lineinfo", 1,
+ &mod->lineinfo_data_size);
#ifdef CONFIG_JUMP_LABEL
mod->jump_entries = section_objs(info, "__jump_table",
sizeof(*mod->jump_entries),
diff --git a/scripts/Makefile.modfinal b/scripts/Makefile.modfinal
index adcbcde16a071..3941cf624526b 100644
--- a/scripts/Makefile.modfinal
+++ b/scripts/Makefile.modfinal
@@ -46,6 +46,9 @@ quiet_cmd_btf_ko = BTF [M] $@
$(CONFIG_SHELL) $(srctree)/scripts/gen-btf.sh --btf_base $(objtree)/vmlinux $@; \
fi;
+quiet_cmd_lineinfo_ko = LINEINFO [M] $@
+ cmd_lineinfo_ko = $(CONFIG_SHELL) $(srctree)/scripts/gen-mod-lineinfo.sh $@
+
# Same as newer-prereqs, but allows to exclude specified extra dependencies
newer_prereqs_except = $(filter-out $(PHONY) $(1),$?)
@@ -59,6 +62,9 @@ if_changed_except = $(if $(call newer_prereqs_except,$(2))$(cmd-check), \
+$(call if_changed_except,ld_ko_o,$(objtree)/vmlinux)
ifdef CONFIG_DEBUG_INFO_BTF_MODULES
+$(if $(newer-prereqs),$(call cmd,btf_ko))
+endif
+ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+ +$(if $(newer-prereqs),$(call cmd,lineinfo_ko))
endif
+$(call cmd,check_tracepoint)
diff --git a/scripts/gen-mod-lineinfo.sh b/scripts/gen-mod-lineinfo.sh
new file mode 100755
index 0000000000000..d0663b862d31b
--- /dev/null
+++ b/scripts/gen-mod-lineinfo.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+#
+# gen-mod-lineinfo.sh - Embed source line info into a kernel module (.ko)
+#
+# Reads DWARF from the .ko, generates a .mod_lineinfo section, and
+# embeds it back into the .ko. Modeled on scripts/gen-btf.sh.
+
+set -e
+
+if [ $# -ne 1 ]; then
+ echo "Usage: $0 <module.ko>" >&2
+ exit 1
+fi
+
+KO="$1"
+
+cleanup() {
+ rm -f "${KO}.lineinfo.S" "${KO}.lineinfo.o" "${KO}.lineinfo.bin"
+}
+trap cleanup EXIT
+
+case "${KBUILD_VERBOSE}" in
+*1*)
+ set -x
+ ;;
+esac
+
+# Generate assembly from DWARF -- if it fails (no DWARF), silently skip
+if ! ${objtree}/scripts/gen_lineinfo --module "${KO}" > "${KO}.lineinfo.S"; then
+ exit 0
+fi
+
+# Compile assembly to object file
+${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \
+ ${KBUILD_AFLAGS} ${KBUILD_AFLAGS_MODULE} \
+ -c -o "${KO}.lineinfo.o" "${KO}.lineinfo.S"
+
+# Extract raw section content
+${OBJCOPY} -O binary --only-section=.mod_lineinfo \
+ "${KO}.lineinfo.o" "${KO}.lineinfo.bin"
+
+# Embed into the .ko with alloc,readonly flags
+${OBJCOPY} --add-section ".mod_lineinfo=${KO}.lineinfo.bin" \
+ --set-section-flags .mod_lineinfo=alloc,readonly \
+ "${KO}"
+
+exit 0
diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c
index 37d5e84971be4..7d06701549345 100644
--- a/scripts/gen_lineinfo.c
+++ b/scripts/gen_lineinfo.c
@@ -23,8 +23,16 @@
#include <gelf.h>
#include <limits.h>
+#include "../include/linux/mod_lineinfo.h"
+
+static int module_mode;
+
static unsigned int skipped_overflow;
+/* .text range for module mode (keep only runtime code) */
+static unsigned long long text_section_start;
+static unsigned long long text_section_end;
+
struct line_entry {
unsigned int offset; /* offset from _text */
unsigned int file_id;
@@ -148,27 +156,25 @@ static const char *make_relative(const char *path, const char *comp_dir)
{
const char *p;
- /* If already relative, use as-is */
- if (path[0] != '/')
- return path;
-
- /* comp_dir from DWARF is the most reliable method */
- if (comp_dir) {
- size_t len = strlen(comp_dir);
-
- if (!strncmp(path, comp_dir, len) && path[len] == '/') {
- const char *rel = path + len + 1;
-
- /*
- * If comp_dir pointed to a subdirectory
- * (e.g. arch/parisc/kernel) rather than
- * the tree root, stripping it leaves a
- * bare filename. Fall through to the
- * kernel_dirs scan so we recover the full
- * relative path instead.
- */
- if (strchr(rel, '/'))
- return rel;
+ if (path[0] == '/') {
+ /* Try comp_dir prefix from DWARF */
+ if (comp_dir) {
+ size_t len = strlen(comp_dir);
+
+ if (!strncmp(path, comp_dir, len) && path[len] == '/') {
+ const char *rel = path + len + 1;
+
+ /*
+ * If comp_dir pointed to a subdirectory
+ * (e.g. arch/parisc/kernel) rather than
+ * the tree root, stripping it leaves a
+ * bare filename. Fall through to the
+ * kernel_dirs scan so we recover the full
+ * relative path instead.
+ */
+ if (strchr(rel, '/'))
+ return rel;
+ }
}
/*
@@ -194,9 +200,42 @@ static const char *make_relative(const char *path, const char *comp_dir)
return p ? p + 1 : path;
}
- /* Fall back to basename */
- p = strrchr(path, '/');
- return p ? p + 1 : path;
+ /*
+ * Relative path — check for duplicated-path quirk from libdw
+ * on ET_REL files (e.g., "a/b.c/a/b.c" → "a/b.c").
+ */
+ {
+ size_t len = strlen(path);
+ size_t mid = len / 2;
+
+ if (len > 1 && path[mid] == '/' &&
+ !memcmp(path, path + mid + 1, mid))
+ return path + mid + 1;
+ }
+
+ /*
+ * Bare filename with no directory component — try to recover the
+ * relative path using comp_dir. Some toolchains/elfutils combos
+ * produce bare filenames where comp_dir holds the source directory.
+ * Construct the absolute path and run the kernel_dirs scan.
+ */
+ if (!strchr(path, '/') && comp_dir && comp_dir[0] == '/') {
+ static char buf[PATH_MAX];
+
+ snprintf(buf, sizeof(buf), "%s/%s", comp_dir, path);
+ for (p = buf + 1; *p; p++) {
+ if (*(p - 1) == '/') {
+ for (unsigned int i = 0; i < sizeof(kernel_dirs) /
+ sizeof(kernel_dirs[0]); i++) {
+ if (!strncmp(p, kernel_dirs[i],
+ strlen(kernel_dirs[i])))
+ return p;
+ }
+ }
+ }
+ }
+
+ return path;
}
static int compare_entries(const void *a, const void *b)
@@ -248,6 +287,159 @@ static unsigned long long find_text_addr(Elf *elf)
exit(1);
}
+static void find_text_section_range(Elf *elf)
+{
+ Elf_Scn *scn = NULL;
+ GElf_Shdr shdr;
+ size_t shstrndx;
+
+ if (elf_getshdrstrndx(elf, &shstrndx) != 0)
+ return;
+
+ while ((scn = elf_nextscn(elf, scn)) != NULL) {
+ const char *name;
+
+ if (!gelf_getshdr(scn, &shdr))
+ continue;
+ name = elf_strptr(elf, shstrndx, shdr.sh_name);
+ if (name && !strcmp(name, ".text")) {
+ text_section_start = shdr.sh_addr;
+ text_section_end = shdr.sh_addr + shdr.sh_size;
+ return;
+ }
+ }
+}
+
+/*
+ * Apply .rela.debug_line relocations to a mutable copy of .debug_line data.
+ *
+ * elfutils libdw (through at least 0.194) does NOT apply relocations for
+ * ET_REL files when using dwarf_begin_elf(). The internal libdwfl layer
+ * does this via __libdwfl_relocate(), but that API is not public.
+ *
+ * For DWARF5, the .debug_line file name table uses DW_FORM_line_strp
+ * references into .debug_line_str. Without relocation, all these offsets
+ * resolve to 0 (or garbage), causing dwarf_linesrc()/dwarf_filesrc() to
+ * return wrong filenames (typically the comp_dir for every file).
+ *
+ * This function applies the relocations manually so that the patched
+ * .debug_line data can be fed to dwarf_begin_elf() and produce correct
+ * results.
+ *
+ * See elfutils bug https://sourceware.org/bugzilla/show_bug.cgi?id=31447
+ * A fix (dwelf_elf_apply_relocs) was proposed but not yet merged as of
+ * elfutils 0.194: https://sourceware.org/pipermail/elfutils-devel/2024q3/007388.html
+ */
+/*
+ * Determine the relocation type for a 32-bit absolute reference
+ * on the given architecture. Returns 0 if unknown.
+ */
+static unsigned int r_type_abs32(unsigned int e_machine)
+{
+ switch (e_machine) {
+ case EM_X86_64: return R_X86_64_32;
+ case EM_386: return R_386_32;
+ case EM_AARCH64: return R_AARCH64_ABS32;
+ case EM_ARM: return R_ARM_ABS32;
+ case EM_RISCV: return R_RISCV_32;
+ case EM_S390: return R_390_32;
+ case EM_MIPS: return R_MIPS_32;
+ case EM_PPC64: return R_PPC64_ADDR32;
+ case EM_PPC: return R_PPC_ADDR32;
+ case EM_LOONGARCH: return R_LARCH_32;
+ case EM_PARISC: return R_PARISC_DIR32;
+ default: return 0;
+ }
+}
+
+static void apply_debug_line_relocations(Elf *elf)
+{
+ Elf_Scn *scn = NULL;
+ Elf_Scn *debug_line_scn = NULL;
+ Elf_Scn *rela_debug_line_scn = NULL;
+ Elf_Scn *symtab_scn = NULL;
+ GElf_Shdr shdr;
+ GElf_Ehdr ehdr;
+ unsigned int abs32_type;
+ size_t shstrndx;
+ Elf_Data *dl_data, *rela_data, *sym_data;
+ GElf_Shdr rela_shdr, sym_shdr;
+ size_t nrels, i;
+
+ if (gelf_getehdr(elf, &ehdr) == NULL)
+ return;
+
+ abs32_type = r_type_abs32(ehdr.e_machine);
+ if (!abs32_type)
+ return;
+
+ if (elf_getshdrstrndx(elf, &shstrndx) != 0)
+ return;
+
+ /* Find the relevant sections */
+ while ((scn = elf_nextscn(elf, scn)) != NULL) {
+ const char *name;
+
+ if (!gelf_getshdr(scn, &shdr))
+ continue;
+ name = elf_strptr(elf, shstrndx, shdr.sh_name);
+ if (!name)
+ continue;
+
+ if (!strcmp(name, ".debug_line"))
+ debug_line_scn = scn;
+ else if (!strcmp(name, ".rela.debug_line"))
+ rela_debug_line_scn = scn;
+ else if (shdr.sh_type == SHT_SYMTAB)
+ symtab_scn = scn;
+ }
+
+ if (!debug_line_scn || !rela_debug_line_scn || !symtab_scn)
+ return;
+
+ dl_data = elf_getdata(debug_line_scn, NULL);
+ rela_data = elf_getdata(rela_debug_line_scn, NULL);
+ sym_data = elf_getdata(symtab_scn, NULL);
+ if (!dl_data || !rela_data || !sym_data)
+ return;
+
+ if (!gelf_getshdr(rela_debug_line_scn, &rela_shdr))
+ return;
+ if (!gelf_getshdr(symtab_scn, &sym_shdr))
+ return;
+
+ nrels = rela_shdr.sh_size / rela_shdr.sh_entsize;
+
+ for (i = 0; i < nrels; i++) {
+ GElf_Rela rela;
+ GElf_Sym sym;
+ unsigned int r_type;
+ size_t r_sym;
+ uint32_t value;
+
+ if (!gelf_getrela(rela_data, i, &rela))
+ continue;
+
+ r_type = GELF_R_TYPE(rela.r_info);
+ r_sym = GELF_R_SYM(rela.r_info);
+
+ /* Only handle the 32-bit absolute reloc for this arch */
+ if (r_type != abs32_type)
+ continue;
+
+ if (!gelf_getsym(sym_data, r_sym, &sym))
+ continue;
+
+ /* Relocated value = sym.st_value + addend */
+ value = (uint32_t)(sym.st_value + rela.r_addend);
+
+ /* Patch the .debug_line data at the relocation offset */
+ if (rela.r_offset + 4 <= dl_data->d_size)
+ memcpy((char *)dl_data->d_buf + rela.r_offset,
+ &value, sizeof(value));
+ }
+}
+
static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
{
Dwarf_Off off = 0, next_off;
@@ -295,6 +487,17 @@ static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
if (addr < text_addr)
continue;
+ /*
+ * In module mode, keep only .text addresses.
+ * In ET_REL .ko files, .text, .init.text and
+ * .exit.text all have sh_addr == 0 and therefore
+ * overlapping address ranges. Explicitly check
+ * against the .text bounds.
+ */
+ if (module_mode && text_section_end > text_section_start &&
+ (addr < text_section_start || addr >= text_section_end))
+ continue;
+
{
unsigned long long raw_offset = addr - text_addr;
@@ -440,6 +643,63 @@ static void output_assembly(void)
printf("\n");
}
+static void output_module_assembly(void)
+{
+ unsigned int filenames_size = 0;
+
+ for (unsigned int i = 0; i < num_files; i++)
+ filenames_size += strlen(files[i].name) + 1;
+
+ printf("/* SPDX-License-Identifier: GPL-2.0 */\n");
+ printf("/*\n");
+ printf(" * Automatically generated by scripts/gen_lineinfo --module\n");
+ printf(" * Do not edit.\n");
+ printf(" */\n\n");
+
+ printf("\t.section .mod_lineinfo, \"a\"\n\n");
+
+ /* Header: num_entries, num_files, filenames_size, reserved */
+ printf("\t.balign 4\n");
+ printf("\t.long %u\n", num_entries);
+ printf("\t.long %u\n", num_files);
+ printf("\t.long %u\n", filenames_size);
+ printf("\t.long 0\n\n");
+
+ /* addrs[] */
+ for (unsigned int i = 0; i < num_entries; i++)
+ printf("\t.long 0x%x\n", entries[i].offset);
+ if (num_entries)
+ printf("\n");
+
+ /* file_ids[] */
+ for (unsigned int i = 0; i < num_entries; i++)
+ printf("\t.short %u\n", entries[i].file_id);
+
+ /* Padding to align lines[] to 4 bytes */
+ if (num_entries & 1)
+ printf("\t.short 0\n");
+ if (num_entries)
+ printf("\n");
+
+ /* lines[] */
+ for (unsigned int i = 0; i < num_entries; i++)
+ printf("\t.long %u\n", entries[i].line);
+ if (num_entries)
+ printf("\n");
+
+ /* file_offsets[] */
+ for (unsigned int i = 0; i < num_files; i++)
+ printf("\t.long %u\n", files[i].str_offset);
+ if (num_files)
+ printf("\n");
+
+ /* filenames[] */
+ for (unsigned int i = 0; i < num_files; i++)
+ print_escaped_asciz(files[i].name);
+ if (num_files)
+ printf("\n");
+}
+
int main(int argc, char *argv[])
{
int fd;
@@ -447,12 +707,23 @@ int main(int argc, char *argv[])
Dwarf *dwarf;
unsigned long long text_addr;
+ if (argc >= 2 && !strcmp(argv[1], "--module")) {
+ module_mode = 1;
+ argv++;
+ argc--;
+ }
+
if (argc != 2) {
- fprintf(stderr, "Usage: %s <vmlinux>\n", argv[0]);
+ fprintf(stderr, "Usage: %s [--module] <ELF file>\n", argv[0]);
return 1;
}
- fd = open(argv[1], O_RDONLY);
+ /*
+ * For module mode, open O_RDWR so we can apply debug section
+ * relocations to the in-memory ELF data. The modifications
+ * are NOT written back to disk (no elf_update() call).
+ */
+ fd = open(argv[1], module_mode ? O_RDWR : O_RDONLY);
if (fd < 0) {
fprintf(stderr, "Cannot open %s: %s\n", argv[1],
strerror(errno));
@@ -460,7 +731,7 @@ int main(int argc, char *argv[])
}
elf_version(EV_CURRENT);
- elf = elf_begin(fd, ELF_C_READ, NULL);
+ elf = elf_begin(fd, module_mode ? ELF_C_RDWR : ELF_C_READ, NULL);
if (!elf) {
fprintf(stderr, "elf_begin failed: %s\n",
elf_errmsg(elf_errno()));
@@ -468,7 +739,22 @@ int main(int argc, char *argv[])
return 1;
}
- text_addr = find_text_addr(elf);
+ if (module_mode) {
+ /*
+ * .ko files are ET_REL after ld -r. libdw does NOT apply
+ * relocations for ET_REL files, so DW_FORM_line_strp
+ * references in .debug_line are not resolved. Apply them
+ * ourselves so that dwarf_linesrc() returns correct paths.
+ *
+ * DWARF addresses include the .text sh_addr. Use .text
+ * sh_addr as the base so offsets are .text-relative.
+ */
+ apply_debug_line_relocations(elf);
+ find_text_section_range(elf);
+ text_addr = text_section_start;
+ } else {
+ text_addr = find_text_addr(elf);
+ }
dwarf = dwarf_begin_elf(elf, DWARF_C_READ, NULL);
if (!dwarf) {
@@ -494,7 +780,10 @@ int main(int argc, char *argv[])
fprintf(stderr, "lineinfo: %u entries, %u files\n",
num_entries, num_files);
- output_assembly();
+ if (module_mode)
+ output_module_assembly();
+ else
+ output_assembly();
dwarf_end(dwarf);
elf_end(elf);
--
2.51.0
^ permalink raw reply related
* [PATCH v4 1/4] kallsyms: embed source file:line info in kernel stack traces
From: Sasha Levin @ 2026-03-22 13:15 UTC (permalink / raw)
To: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
James Bottomley
Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, Helge Deller,
Randy Dunlap, Laurent Pinchart, Vivian Wang, linux-kernel,
linux-kbuild, linux-modules, linux-doc, Sasha Levin
In-Reply-To: <20260322131543.971079-1-sashal@kernel.org>
Add CONFIG_KALLSYMS_LINEINFO, which embeds a compact address-to-line
lookup table in the kernel image so stack traces directly print source
file and line number information:
root@localhost:~# echo c > /proc/sysrq-trigger
[ 11.201987] sysrq: Trigger a crash
[ 11.202831] Kernel panic - not syncing: sysrq triggered crash
[ 11.206218] Call Trace:
[ 11.206501] <TASK>
[ 11.206749] dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:94)
[ 11.207403] vpanic+0x36e/0x620 (kernel/panic.c:650)
[ 11.208565] ? __lock_acquire+0x465/0x2240 (kernel/locking/lockdep.c:4674)
[ 11.209324] panic+0xc9/0xd0 (kernel/panic.c:787)
[ 11.211873] ? find_held_lock+0x2b/0x80 (kernel/locking/lockdep.c:5350)
[ 11.212597] ? lock_release+0xd3/0x300 (kernel/locking/lockdep.c:5535)
[ 11.213312] sysrq_handle_crash+0x1a/0x20 (drivers/tty/sysrq.c:154)
[ 11.214005] __handle_sysrq.cold+0x66/0x256 (drivers/tty/sysrq.c:611)
[ 11.214712] write_sysrq_trigger+0x65/0x80 (drivers/tty/sysrq.c:1221)
[ 11.215424] proc_reg_write+0x1bd/0x3c0 (fs/proc/inode.c:330)
[ 11.216061] vfs_write+0x1c6/0xff0 (fs/read_write.c:686)
[ 11.218848] ksys_write+0xfa/0x200 (fs/read_write.c:740)
[ 11.222394] do_syscall_64+0xf3/0x690 (arch/x86/entry/syscall_64.c:63)
[ 11.223942] entry_SYSCALL_64_after_hwframe+0x77/0x7f (arch/x86/entry/entry_64.S:121)
At build time, a new host tool (scripts/gen_lineinfo) reads DWARF
.debug_line from vmlinux using libdw (elfutils), extracts all
address-to-file:line mappings, and generates an assembly file with
sorted parallel arrays (offsets from _text, file IDs, and line
numbers). These are linked into vmlinux as .rodata.
At runtime, kallsyms_lookup_lineinfo() does a binary search on the
table and __sprint_symbol() appends "(file:line)" to each stack frame.
The lookup uses offsets from _text so it works with KASLR, requires no
locks or allocations, and is safe in any context including panic.
The feature requires CONFIG_DEBUG_INFO (for DWARF data) and
elfutils (libdw-dev) on the build host.
Memory footprint measured with a 1852-option x86_64 config:
Table: 4,597,583 entries from 4,841 source files
lineinfo_addrs[] 4,597,583 x u32 = 17.5 MiB
lineinfo_file_ids[] 4,597,583 x u16 = 8.8 MiB
lineinfo_lines[] 4,597,583 x u32 = 17.5 MiB
file_offsets + filenames ~ 0.1 MiB
Total .rodata increase: ~ 44.0 MiB
vmlinux (stripped): 529 MiB -> 573 MiB (+44 MiB / +8.3%)
Note: this probably won't be something we roll into "production", but
it might be useful for the average user given the relatively low memory
footprint, in canary deployments for hyperscalers, or by default for
folks who run tests/fuzzing/etc.
Disclaimer: this was vibe coded over an afternoon with an AI coding
assistant.
The .config used for testing is a simple KVM guest configuration for
local development and testing.
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
Documentation/admin-guide/index.rst | 1 +
.../admin-guide/kallsyms-lineinfo.rst | 72 +++
MAINTAINERS | 6 +
include/linux/kallsyms.h | 17 +-
init/Kconfig | 20 +
kernel/kallsyms.c | 56 ++
kernel/kallsyms_internal.h | 9 +
scripts/.gitignore | 1 +
scripts/Makefile | 3 +
scripts/empty_lineinfo.S | 30 ++
scripts/gen_lineinfo.c | 510 ++++++++++++++++++
scripts/kallsyms.c | 16 +
scripts/link-vmlinux.sh | 43 +-
13 files changed, 780 insertions(+), 4 deletions(-)
create mode 100644 Documentation/admin-guide/kallsyms-lineinfo.rst
create mode 100644 scripts/empty_lineinfo.S
create mode 100644 scripts/gen_lineinfo.c
diff --git a/Documentation/admin-guide/index.rst b/Documentation/admin-guide/index.rst
index b734f8a2a2c48..1801b9880aeb7 100644
--- a/Documentation/admin-guide/index.rst
+++ b/Documentation/admin-guide/index.rst
@@ -73,6 +73,7 @@ problems and bugs in particular.
ramoops
dynamic-debug-howto
init
+ kallsyms-lineinfo
kdump/index
perf/index
pstore-blk
diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
new file mode 100644
index 0000000000000..c8ec124394354
--- /dev/null
+++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
@@ -0,0 +1,72 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+====================================
+Kallsyms Source Line Info (LINEINFO)
+====================================
+
+Overview
+========
+
+``CONFIG_KALLSYMS_LINEINFO`` embeds DWARF-derived source file and line number
+mappings into the kernel image so that stack traces include
+``(file.c:123)`` annotations next to each symbol. This makes it significantly
+easier to pinpoint the exact source location during debugging, without needing
+to manually cross-reference addresses with ``addr2line``.
+
+Enabling the Feature
+====================
+
+Enable the following kernel configuration options::
+
+ CONFIG_KALLSYMS=y
+ CONFIG_DEBUG_INFO=y
+ CONFIG_KALLSYMS_LINEINFO=y
+
+Build dependency: the host tool ``scripts/gen_lineinfo`` requires ``libdw``
+from elfutils. Install the development package:
+
+- Debian/Ubuntu: ``apt install libdw-dev``
+- Fedora/RHEL: ``dnf install elfutils-devel``
+- Arch Linux: ``pacman -S elfutils``
+
+Example Output
+==============
+
+Without ``CONFIG_KALLSYMS_LINEINFO``::
+
+ Call Trace:
+ <TASK>
+ dump_stack_lvl+0x5d/0x80
+ do_syscall_64+0x82/0x190
+ entry_SYSCALL_64_after_hwframe+0x76/0x7e
+
+With ``CONFIG_KALLSYMS_LINEINFO``::
+
+ Call Trace:
+ <TASK>
+ dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:123)
+ do_syscall_64+0x82/0x190 (arch/x86/entry/common.c:52)
+ entry_SYSCALL_64_after_hwframe+0x76/0x7e
+
+Note that assembly routines (such as ``entry_SYSCALL_64_after_hwframe``) are
+not annotated because they lack DWARF debug information.
+
+Memory Overhead
+===============
+
+The lineinfo tables are stored in ``.rodata`` and typically add approximately
+44 MiB to the kernel image for a standard configuration (~4.6 million DWARF
+line entries, ~10 bytes per entry after deduplication).
+
+Known Limitations
+=================
+
+- **vmlinux only**: Only symbols in the core kernel image are annotated.
+ Module symbols are not covered.
+- **4 GiB offset limit**: Address offsets from ``_text`` are stored as 32-bit
+ values. Entries beyond 4 GiB from ``_text`` are skipped at build time with
+ a warning.
+- **65535 file limit**: Source file IDs are stored as 16-bit values. Builds
+ with more than 65535 unique source files will fail with an error.
+- **No assembly annotations**: Functions implemented in assembly that lack
+ DWARF ``.debug_line`` data are not annotated.
diff --git a/MAINTAINERS b/MAINTAINERS
index 61bf550fd37c2..f061e69b6e32a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13728,6 +13728,12 @@ S: Maintained
F: Documentation/hwmon/k8temp.rst
F: drivers/hwmon/k8temp.c
+KALLSYMS LINEINFO
+M: Sasha Levin <sashal@kernel.org>
+S: Maintained
+F: Documentation/admin-guide/kallsyms-lineinfo.rst
+F: scripts/gen_lineinfo.c
+
KASAN
M: Andrey Ryabinin <ryabinin.a.a@gmail.com>
R: Alexander Potapenko <glider@google.com>
diff --git a/include/linux/kallsyms.h b/include/linux/kallsyms.h
index d5dd54c53ace6..7d4c9dca06c87 100644
--- a/include/linux/kallsyms.h
+++ b/include/linux/kallsyms.h
@@ -16,10 +16,15 @@
#include <asm/sections.h>
#define KSYM_NAME_LEN 512
+
+/* Extra space for " (path/to/file.c:12345)" suffix when lineinfo is enabled */
+#define KSYM_LINEINFO_LEN (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) ? 128 : 0)
+
#define KSYM_SYMBOL_LEN (sizeof("%s+%#lx/%#lx [%s %s]") + \
(KSYM_NAME_LEN - 1) + \
2*(BITS_PER_LONG*3/10) + (MODULE_NAME_LEN - 1) + \
- (BUILD_ID_SIZE_MAX * 2) + 1)
+ (BUILD_ID_SIZE_MAX * 2) + 1 + \
+ KSYM_LINEINFO_LEN)
struct cred;
struct module;
@@ -96,6 +101,9 @@ extern int sprint_backtrace_build_id(char *buffer, unsigned long address);
int lookup_symbol_name(unsigned long addr, char *symname);
+bool kallsyms_lookup_lineinfo(unsigned long addr,
+ const char **file, unsigned int *line);
+
#else /* !CONFIG_KALLSYMS */
static inline unsigned long kallsyms_lookup_name(const char *name)
@@ -164,6 +172,13 @@ static inline int kallsyms_on_each_match_symbol(int (*fn)(void *, unsigned long)
{
return -EOPNOTSUPP;
}
+
+static inline bool kallsyms_lookup_lineinfo(unsigned long addr,
+ const char **file,
+ unsigned int *line)
+{
+ return false;
+}
#endif /*CONFIG_KALLSYMS*/
static inline void print_ip_sym(const char *loglvl, unsigned long ip)
diff --git a/init/Kconfig b/init/Kconfig
index b55deae9256c7..c39f27e6393a8 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2050,6 +2050,26 @@ config KALLSYMS_ALL
Say N unless you really need all symbols, or kernel live patching.
+config KALLSYMS_LINEINFO
+ bool "Embed source file:line information in stack traces"
+ depends on KALLSYMS && DEBUG_INFO
+ help
+ Embeds an address-to-source-line mapping table in the kernel
+ image so that stack traces directly include file:line information,
+ similar to what scripts/decode_stacktrace.sh provides but without
+ needing external tools or a vmlinux with debug info at runtime.
+
+ When enabled, stack traces will look like:
+
+ kmem_cache_alloc_noprof+0x60/0x630 (mm/slub.c:3456)
+ anon_vma_clone+0x2ed/0xcf0 (mm/rmap.c:412)
+
+ This requires elfutils (libdw-dev/elfutils-devel) on the build host.
+ Adds approximately 44MB to a typical kernel image (10 bytes per
+ DWARF line-table entry, ~4.6M entries for a typical config).
+
+ If unsure, say N.
+
# end of the "standard kernel features (expert users)" menu
config ARCH_HAS_MEMBARRIER_CALLBACKS
diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
index aec2f06858afd..d0a9cd9c6dace 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -467,6 +467,52 @@ static int append_buildid(char *buffer, const char *modname,
#endif /* CONFIG_STACKTRACE_BUILD_ID */
+bool kallsyms_lookup_lineinfo(unsigned long addr,
+ const char **file, unsigned int *line)
+{
+ unsigned long long raw_offset;
+ unsigned int offset, low, high, mid, file_id;
+
+ if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) || !lineinfo_num_entries)
+ return false;
+
+ /* Compute offset from _text */
+ if (addr < (unsigned long)_text)
+ return false;
+
+ raw_offset = addr - (unsigned long)_text;
+ if (raw_offset > UINT_MAX)
+ return false;
+ offset = (unsigned int)raw_offset;
+
+ /* Binary search for largest entry <= offset */
+ low = 0;
+ high = lineinfo_num_entries;
+ while (low < high) {
+ mid = low + (high - low) / 2;
+ if (lineinfo_addrs[mid] <= offset)
+ low = mid + 1;
+ else
+ high = mid;
+ }
+
+ if (low == 0)
+ return false;
+ low--;
+
+ file_id = lineinfo_file_ids[low];
+ *line = lineinfo_lines[low];
+
+ if (file_id >= lineinfo_num_files)
+ return false;
+
+ if (lineinfo_file_offsets[file_id] >= lineinfo_filenames_size)
+ return false;
+
+ *file = &lineinfo_filenames[lineinfo_file_offsets[file_id]];
+ return true;
+}
+
/* Look up a kernel symbol and return it in a text buffer. */
static int __sprint_symbol(char *buffer, unsigned long address,
int symbol_offset, int add_offset, int add_buildid)
@@ -497,6 +543,16 @@ static int __sprint_symbol(char *buffer, unsigned long address,
len += sprintf(buffer + len, "]");
}
+ if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) && !modname) {
+ const char *li_file;
+ unsigned int li_line;
+
+ if (kallsyms_lookup_lineinfo(address,
+ &li_file, &li_line))
+ len += snprintf(buffer + len, KSYM_SYMBOL_LEN - len,
+ " (%s:%u)", li_file, li_line);
+ }
+
return len;
}
diff --git a/kernel/kallsyms_internal.h b/kernel/kallsyms_internal.h
index 81a867dbe57d4..d7374ce444d81 100644
--- a/kernel/kallsyms_internal.h
+++ b/kernel/kallsyms_internal.h
@@ -15,4 +15,13 @@ extern const u16 kallsyms_token_index[];
extern const unsigned int kallsyms_markers[];
extern const u8 kallsyms_seqs_of_names[];
+extern const u32 lineinfo_num_entries;
+extern const u32 lineinfo_addrs[];
+extern const u16 lineinfo_file_ids[];
+extern const u32 lineinfo_lines[];
+extern const u32 lineinfo_num_files;
+extern const u32 lineinfo_file_offsets[];
+extern const u32 lineinfo_filenames_size;
+extern const char lineinfo_filenames[];
+
#endif // LINUX_KALLSYMS_INTERNAL_H_
diff --git a/scripts/.gitignore b/scripts/.gitignore
index 4215c2208f7e4..e175714c18b61 100644
--- a/scripts/.gitignore
+++ b/scripts/.gitignore
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: GPL-2.0-only
/asn1_compiler
+/gen_lineinfo
/gen_packed_field_checks
/generate_rust_target
/insert-sys-cert
diff --git a/scripts/Makefile b/scripts/Makefile
index 0941e5ce7b575..ffe89875b3295 100644
--- a/scripts/Makefile
+++ b/scripts/Makefile
@@ -4,6 +4,7 @@
# the kernel for the build process.
hostprogs-always-$(CONFIG_KALLSYMS) += kallsyms
+hostprogs-always-$(CONFIG_KALLSYMS_LINEINFO) += gen_lineinfo
hostprogs-always-$(BUILD_C_RECORDMCOUNT) += recordmcount
hostprogs-always-$(CONFIG_BUILDTIME_TABLE_SORT) += sorttable
hostprogs-always-$(CONFIG_ASN1) += asn1_compiler
@@ -36,6 +37,8 @@ HOSTLDLIBS_sorttable = -lpthread
HOSTCFLAGS_asn1_compiler.o = -I$(srctree)/include
HOSTCFLAGS_sign-file.o = $(shell $(HOSTPKG_CONFIG) --cflags libcrypto 2> /dev/null)
HOSTLDLIBS_sign-file = $(shell $(HOSTPKG_CONFIG) --libs libcrypto 2> /dev/null || echo -lcrypto)
+HOSTCFLAGS_gen_lineinfo.o = $(shell $(HOSTPKG_CONFIG) --cflags libdw 2> /dev/null)
+HOSTLDLIBS_gen_lineinfo = $(shell $(HOSTPKG_CONFIG) --libs libdw 2> /dev/null || echo -ldw -lelf -lz)
ifdef CONFIG_UNWINDER_ORC
ifeq ($(ARCH),x86_64)
diff --git a/scripts/empty_lineinfo.S b/scripts/empty_lineinfo.S
new file mode 100644
index 0000000000000..e058c41137123
--- /dev/null
+++ b/scripts/empty_lineinfo.S
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Empty lineinfo stub for the initial vmlinux link.
+ * The real lineinfo is generated from .tmp_vmlinux1 by gen_lineinfo.
+ */
+ .section .rodata, "a"
+ .globl lineinfo_num_entries
+ .balign 4
+lineinfo_num_entries:
+ .long 0
+ .globl lineinfo_num_files
+ .balign 4
+lineinfo_num_files:
+ .long 0
+ .globl lineinfo_addrs
+lineinfo_addrs:
+ .globl lineinfo_file_ids
+lineinfo_file_ids:
+ .globl lineinfo_lines
+lineinfo_lines:
+ .globl lineinfo_file_offsets
+lineinfo_file_offsets:
+ .globl lineinfo_filenames_size
+ .balign 4
+lineinfo_filenames_size:
+ .long 0
+ .globl lineinfo_filenames
+lineinfo_filenames:
diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c
new file mode 100644
index 0000000000000..37d5e84971be4
--- /dev/null
+++ b/scripts/gen_lineinfo.c
@@ -0,0 +1,510 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * gen_lineinfo.c - Generate address-to-source-line lookup tables from DWARF
+ *
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Reads DWARF .debug_line from a vmlinux ELF file and outputs an assembly
+ * file containing sorted lookup tables that the kernel uses to annotate
+ * stack traces with source file:line information.
+ *
+ * Requires libdw from elfutils.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <elfutils/libdw.h>
+#include <dwarf.h>
+#include <elf.h>
+#include <gelf.h>
+#include <limits.h>
+
+static unsigned int skipped_overflow;
+
+struct line_entry {
+ unsigned int offset; /* offset from _text */
+ unsigned int file_id;
+ unsigned int line;
+};
+
+struct file_entry {
+ char *name;
+ unsigned int id;
+ unsigned int str_offset;
+};
+
+static struct line_entry *entries;
+static unsigned int num_entries;
+static unsigned int entries_capacity;
+
+static struct file_entry *files;
+static unsigned int num_files;
+static unsigned int files_capacity;
+
+#define FILE_HASH_BITS 13
+#define FILE_HASH_SIZE (1 << FILE_HASH_BITS)
+
+struct file_hash_entry {
+ const char *name;
+ unsigned int id;
+};
+
+static struct file_hash_entry file_hash[FILE_HASH_SIZE];
+
+static unsigned int hash_str(const char *s)
+{
+ unsigned int h = 5381;
+
+ for (; *s; s++)
+ h = h * 33 + (unsigned char)*s;
+ return h & (FILE_HASH_SIZE - 1);
+}
+
+static void add_entry(unsigned int offset, unsigned int file_id,
+ unsigned int line)
+{
+ if (num_entries >= entries_capacity) {
+ entries_capacity = entries_capacity ? entries_capacity * 2 : 65536;
+ entries = realloc(entries, entries_capacity * sizeof(*entries));
+ if (!entries) {
+ fprintf(stderr, "out of memory\n");
+ exit(1);
+ }
+ }
+ entries[num_entries].offset = offset;
+ entries[num_entries].file_id = file_id;
+ entries[num_entries].line = line;
+ num_entries++;
+}
+
+static unsigned int find_or_add_file(const char *name)
+{
+ unsigned int h = hash_str(name);
+
+ /* Open-addressing lookup with linear probing */
+ while (file_hash[h].name) {
+ if (!strcmp(file_hash[h].name, name))
+ return file_hash[h].id;
+ h = (h + 1) & (FILE_HASH_SIZE - 1);
+ }
+
+ if (num_files >= 65535) {
+ fprintf(stderr,
+ "gen_lineinfo: too many source files (%u > 65535)\n",
+ num_files);
+ exit(1);
+ }
+
+ if (num_files >= files_capacity) {
+ files_capacity = files_capacity ? files_capacity * 2 : 4096;
+ files = realloc(files, files_capacity * sizeof(*files));
+ if (!files) {
+ fprintf(stderr, "out of memory\n");
+ exit(1);
+ }
+ }
+ files[num_files].name = strdup(name);
+ files[num_files].id = num_files;
+
+ /* Insert into hash table (points to files[] entry) */
+ file_hash[h].name = files[num_files].name;
+ file_hash[h].id = num_files;
+
+ num_files++;
+ return num_files - 1;
+}
+
+/*
+ * Well-known top-level directories in the kernel source tree.
+ * Used as a fallback to recover relative paths from absolute DWARF paths
+ * when comp_dir doesn't match (e.g. O= out-of-tree builds where comp_dir
+ * is the build directory but source paths point into the source tree).
+ */
+static const char * const kernel_dirs[] = {
+ "arch/", "block/", "certs/", "crypto/", "drivers/", "fs/",
+ "include/", "init/", "io_uring/", "ipc/", "kernel/", "lib/",
+ "mm/", "net/", "rust/", "samples/", "scripts/", "security/",
+ "sound/", "tools/", "usr/", "virt/",
+};
+
+/*
+ * Strip a filename to a kernel-relative path.
+ *
+ * For absolute paths, strip the comp_dir prefix (from DWARF) to get
+ * a kernel-tree-relative path. When that fails (e.g. O= builds where
+ * comp_dir is the build directory), scan for a well-known kernel
+ * top-level directory name in the path to recover the relative path.
+ * Fall back to the basename as a last resort.
+ *
+ * For relative paths (common in modules), libdw may produce a bogus
+ * doubled path like "net/foo/bar.c/net/foo/bar.c" due to ET_REL DWARF
+ * quirks. Detect and strip such duplicates.
+ */
+static const char *make_relative(const char *path, const char *comp_dir)
+{
+ const char *p;
+
+ /* If already relative, use as-is */
+ if (path[0] != '/')
+ return path;
+
+ /* comp_dir from DWARF is the most reliable method */
+ if (comp_dir) {
+ size_t len = strlen(comp_dir);
+
+ if (!strncmp(path, comp_dir, len) && path[len] == '/') {
+ const char *rel = path + len + 1;
+
+ /*
+ * If comp_dir pointed to a subdirectory
+ * (e.g. arch/parisc/kernel) rather than
+ * the tree root, stripping it leaves a
+ * bare filename. Fall through to the
+ * kernel_dirs scan so we recover the full
+ * relative path instead.
+ */
+ if (strchr(rel, '/'))
+ return rel;
+ }
+
+ /*
+ * comp_dir prefix didn't help — either it didn't match
+ * or it was too specific and left a bare filename.
+ * Scan for a known kernel top-level directory component
+ * to find where the relative path starts. This handles
+ * O= builds and arches where comp_dir is a subdirectory.
+ */
+ for (p = path + 1; *p; p++) {
+ if (*(p - 1) == '/') {
+ for (unsigned int i = 0; i < sizeof(kernel_dirs) /
+ sizeof(kernel_dirs[0]); i++) {
+ if (!strncmp(p, kernel_dirs[i],
+ strlen(kernel_dirs[i])))
+ return p;
+ }
+ }
+ }
+
+ /* Fall back to basename */
+ p = strrchr(path, '/');
+ return p ? p + 1 : path;
+ }
+
+ /* Fall back to basename */
+ p = strrchr(path, '/');
+ return p ? p + 1 : path;
+}
+
+static int compare_entries(const void *a, const void *b)
+{
+ const struct line_entry *ea = a;
+ const struct line_entry *eb = b;
+
+ if (ea->offset != eb->offset)
+ return ea->offset < eb->offset ? -1 : 1;
+ if (ea->file_id != eb->file_id)
+ return ea->file_id < eb->file_id ? -1 : 1;
+ if (ea->line != eb->line)
+ return ea->line < eb->line ? -1 : 1;
+ return 0;
+}
+
+static unsigned long long find_text_addr(Elf *elf)
+{
+ size_t nsyms, i;
+ Elf_Scn *scn = NULL;
+ GElf_Shdr shdr;
+
+ while ((scn = elf_nextscn(elf, scn)) != NULL) {
+ Elf_Data *data;
+
+ if (!gelf_getshdr(scn, &shdr))
+ continue;
+ if (shdr.sh_type != SHT_SYMTAB)
+ continue;
+
+ data = elf_getdata(scn, NULL);
+ if (!data)
+ continue;
+
+ nsyms = shdr.sh_size / shdr.sh_entsize;
+ for (i = 0; i < nsyms; i++) {
+ GElf_Sym sym;
+ const char *name;
+
+ if (!gelf_getsym(data, i, &sym))
+ continue;
+ name = elf_strptr(elf, shdr.sh_link, sym.st_name);
+ if (name && !strcmp(name, "_text"))
+ return sym.st_value;
+ }
+ }
+
+ fprintf(stderr, "Cannot find _text symbol\n");
+ exit(1);
+}
+
+static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
+{
+ Dwarf_Off off = 0, next_off;
+ size_t hdr_size;
+
+ while (dwarf_nextcu(dwarf, off, &next_off, &hdr_size,
+ NULL, NULL, NULL) == 0) {
+ Dwarf_Die cudie;
+ Dwarf_Lines *lines;
+ size_t nlines;
+ Dwarf_Attribute attr;
+ const char *comp_dir = NULL;
+
+ if (!dwarf_offdie(dwarf, off + hdr_size, &cudie))
+ goto next;
+
+ if (dwarf_attr(&cudie, DW_AT_comp_dir, &attr))
+ comp_dir = dwarf_formstring(&attr);
+
+ if (dwarf_getsrclines(&cudie, &lines, &nlines) != 0)
+ goto next;
+
+ for (size_t i = 0; i < nlines; i++) {
+ Dwarf_Line *line = dwarf_onesrcline(lines, i);
+ Dwarf_Addr addr;
+ const char *src;
+ const char *rel;
+ unsigned int file_id, loffset;
+ int lineno;
+
+ if (!line)
+ continue;
+
+ if (dwarf_lineaddr(line, &addr) != 0)
+ continue;
+ if (dwarf_lineno(line, &lineno) != 0)
+ continue;
+ if (lineno == 0)
+ continue;
+
+ src = dwarf_linesrc(line, NULL, NULL);
+ if (!src)
+ continue;
+
+ if (addr < text_addr)
+ continue;
+
+ {
+ unsigned long long raw_offset = addr - text_addr;
+
+ if (raw_offset > UINT_MAX) {
+ skipped_overflow++;
+ continue;
+ }
+ loffset = (unsigned int)raw_offset;
+ }
+
+ rel = make_relative(src, comp_dir);
+ file_id = find_or_add_file(rel);
+
+ add_entry(loffset, file_id, (unsigned int)lineno);
+ }
+next:
+ off = next_off;
+ }
+}
+
+static void deduplicate(void)
+{
+ unsigned int i, j;
+
+ if (num_entries < 2)
+ return;
+
+ /* Sort by offset, then file_id, then line for stability */
+ qsort(entries, num_entries, sizeof(*entries), compare_entries);
+
+ /*
+ * Remove duplicate entries:
+ * - Same offset: keep first (deterministic from stable sort keys)
+ * - Same file:line as previous kept entry: redundant for binary
+ * search -- any address between them resolves to the earlier one
+ */
+ j = 0;
+ for (i = 1; i < num_entries; i++) {
+ if (entries[i].offset == entries[j].offset)
+ continue;
+ if (entries[i].file_id == entries[j].file_id &&
+ entries[i].line == entries[j].line)
+ continue;
+ j++;
+ if (j != i)
+ entries[j] = entries[i];
+ }
+ num_entries = j + 1;
+}
+
+static void compute_file_offsets(void)
+{
+ unsigned int offset = 0;
+
+ for (unsigned int i = 0; i < num_files; i++) {
+ files[i].str_offset = offset;
+ offset += strlen(files[i].name) + 1;
+ }
+}
+
+static void print_escaped_asciz(const char *s)
+{
+ printf("\t.asciz \"");
+ for (; *s; s++) {
+ if (*s == '"' || *s == '\\')
+ putchar('\\');
+ putchar(*s);
+ }
+ printf("\"\n");
+}
+
+static void output_assembly(void)
+{
+ printf("/* SPDX-License-Identifier: GPL-2.0 */\n");
+ printf("/*\n");
+ printf(" * Automatically generated by scripts/gen_lineinfo\n");
+ printf(" * Do not edit.\n");
+ printf(" */\n\n");
+
+ printf("\t.section .rodata, \"a\"\n\n");
+
+ /* Number of entries */
+ printf("\t.globl lineinfo_num_entries\n");
+ printf("\t.balign 4\n");
+ printf("lineinfo_num_entries:\n");
+ printf("\t.long %u\n\n", num_entries);
+
+ /* Number of files */
+ printf("\t.globl lineinfo_num_files\n");
+ printf("\t.balign 4\n");
+ printf("lineinfo_num_files:\n");
+ printf("\t.long %u\n\n", num_files);
+
+ /* Sorted address offsets from _text */
+ printf("\t.globl lineinfo_addrs\n");
+ printf("\t.balign 4\n");
+ printf("lineinfo_addrs:\n");
+ for (unsigned int i = 0; i < num_entries; i++)
+ printf("\t.long 0x%x\n", entries[i].offset);
+ printf("\n");
+
+ /* File IDs, parallel to addrs (u16 -- supports up to 65535 files) */
+ printf("\t.globl lineinfo_file_ids\n");
+ printf("\t.balign 2\n");
+ printf("lineinfo_file_ids:\n");
+ for (unsigned int i = 0; i < num_entries; i++)
+ printf("\t.short %u\n", entries[i].file_id);
+ printf("\n");
+
+ /* Line numbers, parallel to addrs */
+ printf("\t.globl lineinfo_lines\n");
+ printf("\t.balign 4\n");
+ printf("lineinfo_lines:\n");
+ for (unsigned int i = 0; i < num_entries; i++)
+ printf("\t.long %u\n", entries[i].line);
+ printf("\n");
+
+ /* File string offset table */
+ printf("\t.globl lineinfo_file_offsets\n");
+ printf("\t.balign 4\n");
+ printf("lineinfo_file_offsets:\n");
+ for (unsigned int i = 0; i < num_files; i++)
+ printf("\t.long %u\n", files[i].str_offset);
+ printf("\n");
+
+ /* Filenames size */
+ {
+ unsigned int fsize = 0;
+
+ for (unsigned int i = 0; i < num_files; i++)
+ fsize += strlen(files[i].name) + 1;
+ printf("\t.globl lineinfo_filenames_size\n");
+ printf("\t.balign 4\n");
+ printf("lineinfo_filenames_size:\n");
+ printf("\t.long %u\n\n", fsize);
+ }
+
+ /* Concatenated NUL-terminated filenames */
+ printf("\t.globl lineinfo_filenames\n");
+ printf("lineinfo_filenames:\n");
+ for (unsigned int i = 0; i < num_files; i++)
+ print_escaped_asciz(files[i].name);
+ printf("\n");
+}
+
+int main(int argc, char *argv[])
+{
+ int fd;
+ Elf *elf;
+ Dwarf *dwarf;
+ unsigned long long text_addr;
+
+ if (argc != 2) {
+ fprintf(stderr, "Usage: %s <vmlinux>\n", argv[0]);
+ return 1;
+ }
+
+ fd = open(argv[1], O_RDONLY);
+ if (fd < 0) {
+ fprintf(stderr, "Cannot open %s: %s\n", argv[1],
+ strerror(errno));
+ return 1;
+ }
+
+ elf_version(EV_CURRENT);
+ elf = elf_begin(fd, ELF_C_READ, NULL);
+ if (!elf) {
+ fprintf(stderr, "elf_begin failed: %s\n",
+ elf_errmsg(elf_errno()));
+ close(fd);
+ return 1;
+ }
+
+ text_addr = find_text_addr(elf);
+
+ dwarf = dwarf_begin_elf(elf, DWARF_C_READ, NULL);
+ if (!dwarf) {
+ fprintf(stderr, "dwarf_begin_elf failed: %s\n",
+ dwarf_errmsg(dwarf_errno()));
+ fprintf(stderr, "Is %s built with CONFIG_DEBUG_INFO?\n",
+ argv[1]);
+ elf_end(elf);
+ close(fd);
+ return 1;
+ }
+
+ process_dwarf(dwarf, text_addr);
+
+ if (skipped_overflow)
+ fprintf(stderr,
+ "lineinfo: warning: %u entries skipped (offset > 4 GiB from _text)\n",
+ skipped_overflow);
+
+ deduplicate();
+ compute_file_offsets();
+
+ fprintf(stderr, "lineinfo: %u entries, %u files\n",
+ num_entries, num_files);
+
+ output_assembly();
+
+ dwarf_end(dwarf);
+ elf_end(elf);
+ close(fd);
+
+ /* Cleanup */
+ free(entries);
+ for (unsigned int i = 0; i < num_files; i++)
+ free(files[i].name);
+ free(files);
+
+ return 0;
+}
diff --git a/scripts/kallsyms.c b/scripts/kallsyms.c
index 37d5c095ad22a..42662c4fbc6c9 100644
--- a/scripts/kallsyms.c
+++ b/scripts/kallsyms.c
@@ -78,6 +78,17 @@ static char *sym_name(const struct sym_entry *s)
static bool is_ignored_symbol(const char *name, char type)
{
+ /* Ignore lineinfo symbols for kallsyms pass stability */
+ static const char * const lineinfo_syms[] = {
+ "lineinfo_addrs",
+ "lineinfo_file_ids",
+ "lineinfo_file_offsets",
+ "lineinfo_filenames",
+ "lineinfo_lines",
+ "lineinfo_num_entries",
+ "lineinfo_num_files",
+ };
+
if (type == 'u' || type == 'n')
return true;
@@ -90,6 +101,11 @@ static bool is_ignored_symbol(const char *name, char type)
return true;
}
+ for (size_t i = 0; i < ARRAY_SIZE(lineinfo_syms); i++) {
+ if (!strcmp(name, lineinfo_syms[i]))
+ return true;
+ }
+
return false;
}
diff --git a/scripts/link-vmlinux.sh b/scripts/link-vmlinux.sh
index f99e196abeea4..39ca44fbb259b 100755
--- a/scripts/link-vmlinux.sh
+++ b/scripts/link-vmlinux.sh
@@ -103,7 +103,7 @@ vmlinux_link()
${ld} ${ldflags} -o ${output} \
${wl}--whole-archive ${objs} ${wl}--no-whole-archive \
${wl}--start-group ${libs} ${wl}--end-group \
- ${kallsymso} ${btf_vmlinux_bin_o} ${arch_vmlinux_o} ${ldlibs}
+ ${kallsymso} ${lineinfo_o} ${btf_vmlinux_bin_o} ${arch_vmlinux_o} ${ldlibs}
}
# Create ${2}.o file with all symbols from the ${1} object file
@@ -129,6 +129,26 @@ kallsyms()
kallsymso=${2}.o
}
+# Generate lineinfo tables from DWARF debug info in a temporary vmlinux.
+# ${1} - temporary vmlinux with debug info
+# Output: sets lineinfo_o to the generated .o file
+gen_lineinfo()
+{
+ info LINEINFO .tmp_lineinfo.S
+ if ! scripts/gen_lineinfo "${1}" > .tmp_lineinfo.S; then
+ echo >&2 "Failed to generate lineinfo from ${1}"
+ echo >&2 "Try to disable CONFIG_KALLSYMS_LINEINFO"
+ exit 1
+ fi
+
+ info AS .tmp_lineinfo.o
+ ${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \
+ ${KBUILD_AFLAGS} ${KBUILD_AFLAGS_KERNEL} \
+ -c -o .tmp_lineinfo.o .tmp_lineinfo.S
+
+ lineinfo_o=.tmp_lineinfo.o
+}
+
# Perform kallsyms for the given temporary vmlinux.
sysmap_and_kallsyms()
{
@@ -155,6 +175,7 @@ sorttable()
cleanup()
{
rm -f .btf.*
+ rm -f .tmp_lineinfo.*
rm -f .tmp_vmlinux.nm-sort
rm -f System.map
rm -f vmlinux
@@ -183,6 +204,7 @@ fi
btf_vmlinux_bin_o=
btfids_vmlinux=
kallsymso=
+lineinfo_o=
strip_debug=
generate_map=
@@ -198,10 +220,21 @@ if is_enabled CONFIG_KALLSYMS; then
kallsyms .tmp_vmlinux0.syms .tmp_vmlinux0.kallsyms
fi
+if is_enabled CONFIG_KALLSYMS_LINEINFO; then
+ # Assemble an empty lineinfo stub for the initial link.
+ # The real lineinfo is generated from .tmp_vmlinux1 by gen_lineinfo.
+ ${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \
+ ${KBUILD_AFLAGS} ${KBUILD_AFLAGS_KERNEL} \
+ -c -o .tmp_lineinfo.o "${srctree}/scripts/empty_lineinfo.S"
+ lineinfo_o=.tmp_lineinfo.o
+fi
+
if is_enabled CONFIG_KALLSYMS || is_enabled CONFIG_DEBUG_INFO_BTF; then
- # The kallsyms linking does not need debug symbols, but the BTF does.
- if ! is_enabled CONFIG_DEBUG_INFO_BTF; then
+ # The kallsyms linking does not need debug symbols, but BTF and
+ # lineinfo generation do.
+ if ! is_enabled CONFIG_DEBUG_INFO_BTF &&
+ ! is_enabled CONFIG_KALLSYMS_LINEINFO; then
strip_debug=1
fi
@@ -219,6 +252,10 @@ if is_enabled CONFIG_DEBUG_INFO_BTF; then
btfids_vmlinux=.tmp_vmlinux1.BTF_ids
fi
+if is_enabled CONFIG_KALLSYMS_LINEINFO; then
+ gen_lineinfo .tmp_vmlinux1
+fi
+
if is_enabled CONFIG_KALLSYMS; then
# kallsyms support
--
2.51.0
^ permalink raw reply related
* [PATCH v4 0/4] kallsyms: embed source file:line info in kernel stack traces
From: Sasha Levin @ 2026-03-22 13:15 UTC (permalink / raw)
To: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
James Bottomley
Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, Helge Deller,
Randy Dunlap, Laurent Pinchart, Vivian Wang, linux-kernel,
linux-kbuild, linux-modules, linux-doc, Sasha Levin
This series adds CONFIG_KALLSYMS_LINEINFO, which embeds source file:line
information directly in the kernel image so that stack traces annotate
every frame with the originating source location - no external tools, no
debug symbols at runtime, and safe to use in NMI/panic context.
Motivation
==========
The recent "slowly decommission bugzilla?" thread surfaced a recurring
problem: when users encounter kernel crashes they see stack traces like
`func+0x1ec/0x240` but have no way to identify which subsystem or
maintainer to contact. Richard Weinberger proposed building a database
mapping symbols to source files using nm/DWARF. Linus pointed to
scripts/decode_stacktrace.sh as the existing solution. But as the
discussion progressed, it became clear that decode_stacktrace.sh has
significant practical barriers that prevent it from being useful in the
common case.
Problems with scripts/decode_stacktrace.sh
==========================================
- Requires debug symbols: the script needs vmlinux with DWARF debug
info. Many distros don't retain debug symbols for older or security
kernels, and even when available, asking users to obtain matching
debuginfo packages is a significant hurdle.
- Requires toolchain: users need addr2line and nm installed.
- Version-matching requirement: debug symbols must exactly match the
running kernel binary.
What this series does
=====================
Patch 1: CONFIG_KALLSYMS_LINEINFO
At build time, a host tool (scripts/gen_lineinfo) reads DWARF
.debug_line from vmlinux, extracts address-to-file:line mappings, and
embeds them as sorted lookup tables in .rodata. At runtime,
kallsyms_lookup_lineinfo() binary-searches the table and
__sprint_symbol() appends "(file:line)" to each stack frame. NMI/panic-
safe (no locks, no allocations), KASLR-compatible.
Patch 2: CONFIG_KALLSYMS_LINEINFO_MODULES
Extends lineinfo to loadable modules. Each .ko gets a .mod_lineinfo
section embedded at build time. The module loader picks it up at load
time. Same zero-allocation, NMI-safe lookup.
Patch 3: delta compression
Block-indexed delta-encoding with LEB128 varints, implementing the
approach suggested by Juergen Gross in the RFC review. Reduces overhead
from ~44 MiB to ~11 MiB (~3.7 bytes/entry), addressing the primary size
concern from the RFC.
Patch 4: KUnit tests
30 KUnit tests covering the lineinfo lookup paths, delta-decode logic,
boundary conditions, and integration with the backtrace formatting APIs.
Example output
==============
[ 11.206749] dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:94)
[ 11.207403] vpanic+0x36e/0x620 (kernel/panic.c:650)
[ 11.209324] panic+0xc9/0xd0 (kernel/panic.c:787)
[ 11.213312] sysrq_handle_crash+0x1a/0x20 (drivers/tty/sysrq.c:154)
[ 11.214005] __handle_sysrq.cold+0x66/0x256 (drivers/tty/sysrq.c:611)
[ 11.214712] write_sysrq_trigger+0x65/0x80 (drivers/tty/sysrq.c:1221)
[ 11.215424] proc_reg_write+0x1bd/0x3c0 (fs/proc/inode.c:330)
[ 11.216061] vfs_write+0x1c6/0xff0 (fs/read_write.c:686)
[ 11.218848] ksys_write+0xfa/0x200 (fs/read_write.c:740)
[ 11.222394] do_syscall_64+0xf3/0x690 (arch/x86/entry/syscall_64.c:63)
Size impact
===========
Measured with a Debian kernel config:
- bzImage: +3.6 MiB (14 MiB -> 18 MiB, +26%)
- Runtime memory: +5.9 MiB (text+data+bss)
- Code overhead: +5.0 KiB (.text, lookup functions only)
- Data overhead: +5.9 MiB (.data, lineinfo tables)
Lineinfo data breakdown:
- lineinfo_data (delta-compressed): 5,728 KiB (97%)
- lineinfo_block_addrs: 99 KiB
- lineinfo_block_offsets: 99 KiB
- lineinfo_filenames: 111 KiB
- lineinfo_file_offsets: 17 KiB
The ~5.9 MiB is after 2.7x delta compression; uncompressed would be
~16 MiB. This is a fraction of the cost of shipping full DWARF debug
info (hundreds of MiB), which distros must store and serve for every
kernel version.
For distros, maintaining debug symbol repositories is expensive: storage,
mirrors, and CDN bandwidth for hundreds of MiB per kernel build add up
quickly. A ~5.9 MiB increase in the kernel image itself is a modest cost
that eliminates the need for users to find, download, and version-match
debuginfo packages just to make a crash report useful.
For developers, the file:line annotations appear immediately in crash
traces - no post-processing with decode_stacktrace.sh needed.
Changes since v3
=================
- Remove redundant gen_lineinfo entry in scripts/Makefile for
CONFIG_KALLSYMS_LINEINFO_MODULES (depends on CONFIG_KALLSYMS_LINEINFO
which already builds it). (Reported by Petr Pavlu)
- Use R_* constants from <elf.h> instead of hardcoded relocation type
values in r_type_abs32(). (Reported by Petr Pavlu)
- Simplify duplicated-path detection in make_relative(): replace loop
over every '/' with a direct midpoint check, since true path
duplication always splits at len/2. (Suggested by Petr Pavlu)
- Fix comment in process_dwarf(): sections in ET_REL objects have
sh_addr == 0 and therefore overlapping address ranges; this is
expected behavior, not a "may" situation. (Reported by Petr Pavlu)
- Use U32_MAX instead of UINT_MAX for the module raw_offset bounds
check, matching the u32 type of the addrs array.
(Reported by Petr Pavlu)
- Document the assumption that .text is at the start of the MOD_TEXT
segment in module_lookup_lineinfo(). A proper fix using ELF
relocations is planned for a future series.
(Reported by Petr Pavlu)
- Wrap -fno-inline-functions-called-once in $(call cc-option,...) for
clang compatibility. Clang does not support this GCC-specific flag;
the noinline attribute is sufficient.
Changes since v2
=================
- Replace #ifdef CONFIG_KALLSYMS_LINEINFO with IS_ENABLED() throughout,
so the compiler checks the code for syntax errors regardless of
configuration. (Suggested by Helge Deller)
- Replace zigzag + ULEB128 encoding of signed deltas with native SLEB128,
removing the unnecessary zigzag transform layer.
(Suggested by Vivian Wang)
- Deduplicate the binary search and delta-decode logic: extract shared
struct lineinfo_table and lineinfo_search() into mod_lineinfo.h
instead of maintaining near-identical copies in kernel/kallsyms.c and
kernel/module/kallsyms.c. (Suggested by Vivian Wang)
- Use .uleb128 / .sleb128 assembler directives in gen_lineinfo output
instead of encoding varints in C and emitting .byte hex literals.
(Suggested by Vivian Wang)
- Redesign module mod_lineinfo_header to use explicit (offset, size)
pairs for each sub-array, similar to flattened devicetree layout.
This makes bounds validation straightforward: offset + size <=
section_size. (Suggested by Vivian Wang)
- Remove dead sym_start parameter from kallsyms_lookup_lineinfo() and
module_lookup_lineinfo().
Changes since v1
=================
- Fix path stripping regression on architectures where DWARF comp_dir is
a subdirectory (e.g. arch/parisc/kernel) rather than the source tree
root: paths now correctly show "kernel/traps.c:212" instead of bare
"traps.c:212". Added kernel_dirs[] fallback scan and bare-filename
recovery via comp_dir. (Reported by Helge Deller)
- Fix RST heading: overline/underline must be at least as long as the
heading text in kallsyms-lineinfo.rst. (Reported by Randy Dunlap)
- Fix MAINTAINERS alphabetical ordering: move KALLSYMS LINEINFO entry
before KASAN. (Reported by Randy Dunlap)
- Fix arch-portability of .debug_line relocation handling: replace
hardcoded R_X86_64_32 with r_type_abs32() supporting x86, arm, arm64,
riscv, s390, mips, ppc, loongarch, and parisc.
- Fix vmlinux compressed-path data_end for the last block: use
lineinfo_data_size instead of UINT_MAX.
- Add file_offsets[] and filenames_size bounds checks in vmlinux lookup
path (the module path already had them).
- Add alignment padding for file_offsets[] in module .mod_lineinfo
binary format (data[] is variable-length u8, followed by u32[]).
- Remove sym_start cross-validation check that incorrectly rejected
valid lineinfo entries for assembly-adjacent functions.
- Add KUnit test suite (new patch 4/4): 30 tests covering vmlinux
lookup, module lookup, delta decode, boundary conditions, and
backtrace formatting integration.
Changes since RFC
==================
- Added module support (patch 2)
- Added delta compression (patch 3), reducing size from ~44 MiB to
~11 MiB, addressing the primary concern from RFC review
- Added documentation (Documentation/admin-guide/kallsyms-lineinfo.rst)
- Added MAINTAINERS entry
Sasha Levin (4):
kallsyms: embed source file:line info in kernel stack traces
kallsyms: extend lineinfo to loadable modules
kallsyms: delta-compress lineinfo tables for ~2.7x size reduction
kallsyms: add KUnit tests for lineinfo feature
Documentation/admin-guide/index.rst | 1 +
.../admin-guide/kallsyms-lineinfo.rst | 97 ++
MAINTAINERS | 9 +
include/linux/kallsyms.h | 17 +-
include/linux/mod_lineinfo.h | 243 +++++
include/linux/module.h | 5 +
init/Kconfig | 35 +
kernel/kallsyms.c | 58 ++
kernel/kallsyms_internal.h | 11 +
kernel/module/kallsyms.c | 80 ++
kernel/module/main.c | 3 +
lib/Kconfig.debug | 10 +
lib/tests/Makefile | 3 +
lib/tests/lineinfo_kunit.c | 813 +++++++++++++++++
scripts/.gitignore | 1 +
scripts/Makefile | 3 +
scripts/Makefile.modfinal | 6 +
scripts/empty_lineinfo.S | 38 +
scripts/gen-mod-lineinfo.sh | 48 +
scripts/gen_lineinfo.c | 848 ++++++++++++++++++
scripts/kallsyms.c | 17 +
scripts/link-vmlinux.sh | 43 +-
22 files changed, 2385 insertions(+), 4 deletions(-)
create mode 100644 Documentation/admin-guide/kallsyms-lineinfo.rst
create mode 100644 include/linux/mod_lineinfo.h
create mode 100644 lib/tests/lineinfo_kunit.c
create mode 100644 scripts/empty_lineinfo.S
create mode 100755 scripts/gen-mod-lineinfo.sh
create mode 100644 scripts/gen_lineinfo.c
--
2.51.0
^ permalink raw reply
* Re: [PATCH 0/1] docs: examples of pages affected by heading overflow
From: Rito Rhymes @ 2026-03-22 12:14 UTC (permalink / raw)
To: Randy Dunlap, Rito Rhymes, Jonathan Corbet, linux-doc
Cc: Shuah Khan, linux-kernel
In-Reply-To: <f81edf19-9b33-4935-904e-636afa4dad79@infradead.org>
Thanks for testing things out and for asking about the logic behind the
implementation choices.
Is there consistency in the solutions chosen?
Those 4 patches are not a single series for a reason. They are targeted
fixes for distinct contexts and use cases where one solution may be
preferable to the other, i.e. wrapping vs. contained horizontal scroll
overflow. They are logically consistent in that the reasons for the
chosen solution do not directly contradict each other across these
contexts, but they should not all be treated as requiring the same
generic behavior, such as wrapping everywhere or contained horizontal
scrolling everywhere. I also did not submit them as a single patch
series because I did not want each patch to have to justify itself
against every other patch as a single thematic unit, because the cases
are very different even if they share an underlying symptom (page-wide
horizontal scroll overflow).
> docs: allow long unbroken headings to wrap and prevent overflow
Why prefer wrapping?
You found an unfortunate edge case, but the core regression this fix
addresses is headings pushing past the right page margin and creating
page-wide horizontal scroll overflow that breaks the page layout
instead of wrapping.
The edge case being:
GPIO_V2_LINE_GET_VALUES_IOCTL
Modern browsers do not treat underscores as natural word-break points.
That whole string is a long unbroken token without a natural breakpoint,
so without the added wrapping behavior it will create page-wide
horizontal overflow on small enough screen sizes, as you can test on the
live site.
So semantically, it looks like a regression to see
GPIO_V2_LINE_GET_VALUES_IOCT
L
instead of:
GPIO_V2_LINE_GET_VALUES_
IOCTL
But that is still the expected browser behavior. It only looks
semantically incorrect because the string has internal structure that
the browser does not understand for wrapping purposes. It is equivalent
to taking a long unbroken string like:
GPIOV2LINEGETVALUESIOCTL
and getting:
GPIOV2LINEGETVALUESIOCT
L
There may be semantically parseable words in there, but regular browser
wrapping does not know that.
So this solution does come with a trade-off.
Long unbroken strings that have semantic shape but no natural
breakpoint, including code-style names, will wrap unnaturally.
But for the majority of headings, wrapping produces clean shapes where
the semantic structure is preserved because there is spacing or a `-`
somewhere in the heading.
For headings, having everything immediately visible without requiring
horizontal scrolling is the preferred trade-off. It is tolerable for
headings to wrap differently across screen sizes, whether that means 1
line, 2 lines, or 3 lines. In most cases, however many lines the
heading wraps to, it remains readable and semantically preserved. I do
not think it would be a reasonable trade-off to introduce contained
horizontal scrolling for headings just to avoid the edge cases
involving long unbroken code-style strings.
Thus, wrapping is the appropriate choice here.
With that said, we could go a step further and add JavaScript to help
treat underscores as wrap points, but I do not think that is ideal. It
would be client-side post-processing, and I do not see similar
non-Sphinx-native JavaScript being used here for this kind of text
shaping. I am open to that if people think it is worth it, but it would
be a new precedent here and not a standard web approach.
> docs: contain horizontal overflow in C API descriptions
Why prefer contained horizontal scroll overflow?
For this patch, contained horizontal scrolling is preferred because
code fidelity is the priority. The code is isolated from the rest of the
page in a dedicated block and presented in a form that is intended to
remain representative of the code itself. Wrapping distorts spacing and
line structure, which affects fidelity, creates misleading renderings,
and reduces readability. Therefore, wrapping is not a good trade-off
here.
> docs: allow inline literals in paragraphs to wrap to prevent overflow
Why prefer wrapping?
If preserving code fidelity were the priority here, the code would not
be embedded inside explanatory prose. The purpose in this context is to
read a paragraph that includes code as part of the explanation, and it
would be distracting and hurt readability if the user needed to scroll
horizontally mid-paragraph just to continue reading. In that context, we
treat it like any other `<span>` inside of a `<p>`.
> docs: wrap generated tables to contain small-screen overflow
Why prefer contained horizontal scroll overflow?
Wrapping is generally not practical here.
Many of these tables, often under a `*/features.html*`, exceed at least
double the viewport width of a mobile screen size. Wrapping the cell
content enough to fit within a small viewport would often create
unreadable vertical stacks of wrapped text. Having the rows or columns
reorganize themselves in some way to fit the viewport would
fundamentally change the table structure and how it is intended to be
navigated, which defeats the simplicity of keeping a single table
structure for many purposes.
The simplest solution is to respect the width the generated table
creates and allow horizontal scrolling within the table container,
without allowing the entire page width to expand.
^ permalink raw reply
* Re: [PATCH v2] Docs: iio: ad7191 Correct clock configuration
From: Jonathan Cameron @ 2026-03-22 12:13 UTC (permalink / raw)
To: Andy Shevchenko
Cc: Ammar Mustafa, Alisa-Dariana Roman, David Lechner, Nuno Sá,
Andy Shevchenko, Jonathan Corbet, Shuah Khan, linux-iio,
linux-doc, linux-kernel
In-Reply-To: <aaLIhgJjrNlp3oTy@ashevche-desk.local>
On Sat, 28 Feb 2026 12:50:46 +0200
Andy Shevchenko <andriy.shevchenko@intel.com> wrote:
> On Fri, Feb 27, 2026 at 02:08:33PM -0500, Ammar Mustafa wrote:
> > Correct the ad7191 documentation to match the datasheet:
> > - Fix inverted CLKSEL pin logic: device uses external clock when pin is
> > inactive, and internal CMOS/crystal when high.
>
> high --> active
>
> Thanks, this part looks good in the below documentation update.
>
> > - Correct CMOS-compatible clock pin from MCLK2 to MCLK1.
>
> I haven't checked driver yet, but is it only for a single component?
> Can you double check that _all_ supported by the driver have the same
> in their datasheet(s)?
>
> ...
Hi Ammar,
Just a quick note to say I'm going to mark this one in patchwork
as needing a new version given Andy's questions have been here a while.
Thanks,
Jonathan
>
> > +- When CLKSEL pin is ACTIVE: Uses internal 4.92MHz clock (no clock property
> > needed)
> > -- When CLKSEL pin is tied HIGH: Requires external clock source
> > +- When CLKSEL pin is INACTIVE: Requires external clock source
> > - Can be a crystal between MCLK1 and MCLK2 pins
> > - - Or a CMOS-compatible clock driving MCLK2 pin
> > + - Or a CMOS-compatible clock driving MCLK1 pin and MCLK2 left unconnected
> > - Must specify the "clocks" property in device tree when using external clock
>
^ permalink raw reply
* [PATCH v2 9/9] kernel/api: add runtime verification selftest
From: Sasha Levin @ 2026-03-22 12:10 UTC (permalink / raw)
To: linux-api, linux-kernel
Cc: linux-doc, linux-fsdevel, linux-kbuild, linux-kselftest,
workflows, tools, x86, Thomas Gleixner, Paul E . McKenney,
Greg Kroah-Hartman, Jonathan Corbet, Dmitry Vyukov, Randy Dunlap,
Cyril Hrubis, Kees Cook, Jake Edge, David Laight, Askar Safin,
Gabriele Paoloni, Mauro Carvalho Chehab, Christian Brauner,
Alexander Viro, Andrew Morton, Masahiro Yamada, Shuah Khan,
Ingo Molnar, Arnd Bergmann, Sasha Levin
In-Reply-To: <20260322121026.869758-1-sashal@kernel.org>
Add a selftest for CONFIG_KAPI_RUNTIME_CHECKS that exercises
sys_open/sys_read/sys_write/sys_close through raw syscall() and
verifies KAPI pre-validation catches invalid parameters while
allowing valid operations through.
Test cases (TAP output):
1-4: Valid open/read/write/close succeed
5-7: Invalid flags, mode bits, NULL path rejected with EINVAL
8: dmesg contains expected KAPI warning strings
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
MAINTAINERS | 1 +
tools/testing/selftests/kapi/Makefile | 7 +
tools/testing/selftests/kapi/kapi_test_util.h | 33 +
tools/testing/selftests/kapi/test_kapi.c | 1023 +++++++++++++++++
4 files changed, 1064 insertions(+)
create mode 100644 tools/testing/selftests/kapi/Makefile
create mode 100644 tools/testing/selftests/kapi/kapi_test_util.h
create mode 100644 tools/testing/selftests/kapi/test_kapi.c
diff --git a/MAINTAINERS b/MAINTAINERS
index db2ba9a596eef..430a23994b314 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13814,6 +13814,7 @@ F: include/linux/syscall_api_spec.h
F: kernel/api/
F: tools/kapi/
F: tools/lib/python/kdoc/kdoc_apispec.py
+F: tools/testing/selftests/kapi/
KERNEL AUTOMOUNTER
M: Ian Kent <raven@themaw.net>
diff --git a/tools/testing/selftests/kapi/Makefile b/tools/testing/selftests/kapi/Makefile
new file mode 100644
index 0000000000000..5f3fdeddcae41
--- /dev/null
+++ b/tools/testing/selftests/kapi/Makefile
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0
+
+TEST_GEN_PROGS := test_kapi
+
+CFLAGS += -static -Wall -O2 $(KHDR_INCLUDES)
+
+include ../lib.mk
diff --git a/tools/testing/selftests/kapi/kapi_test_util.h b/tools/testing/selftests/kapi/kapi_test_util.h
new file mode 100644
index 0000000000000..e097c370542ad
--- /dev/null
+++ b/tools/testing/selftests/kapi/kapi_test_util.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Compatibility helpers for KAPI selftests.
+ *
+ * __NR_open is not defined on aarch64 and riscv64 (only __NR_openat exists).
+ * Provide a wrapper that uses __NR_openat with AT_FDCWD to achieve the same
+ * behavior as __NR_open on architectures that lack it.
+ */
+#ifndef KAPI_TEST_UTIL_H
+#define KAPI_TEST_UTIL_H
+
+#include <fcntl.h>
+#include <sys/syscall.h>
+
+#ifndef __NR_open
+/*
+ * On architectures without __NR_open (e.g., aarch64, riscv64),
+ * use openat(AT_FDCWD, ...) which is equivalent.
+ */
+static inline long kapi_sys_open(const char *pathname, int flags, int mode)
+{
+ return syscall(__NR_openat, AT_FDCWD, pathname, flags, mode);
+}
+#else
+static inline long kapi_sys_open(const char *pathname, int flags, int mode)
+{
+ return syscall(__NR_open, pathname, flags, mode);
+}
+#endif
+
+#endif /* KAPI_TEST_UTIL_H */
diff --git a/tools/testing/selftests/kapi/test_kapi.c b/tools/testing/selftests/kapi/test_kapi.c
new file mode 100644
index 0000000000000..3d9a3a05186d2
--- /dev/null
+++ b/tools/testing/selftests/kapi/test_kapi.c
@@ -0,0 +1,1023 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Userspace selftest for KAPI runtime verification of syscall parameters.
+ *
+ * Exercises sys_open, sys_read, sys_write, and sys_close through raw
+ * syscall() to ensure KAPI pre-validation wrappers interact correctly
+ * with normal kernel error handling.
+ *
+ * Requires CONFIG_KAPI_RUNTIME_CHECKS=y for full coverage; many tests
+ * also pass without it.
+ *
+ * TAP output format.
+ */
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <signal.h>
+#include <sys/syscall.h>
+#include <sys/stat.h>
+#include <linux/limits.h>
+#include "kapi_test_util.h"
+
+#define NUM_TESTS 29
+
+static int test_num;
+static int failures;
+static volatile sig_atomic_t got_sigpipe;
+
+static void tap_ok(const char *desc)
+{
+ printf("ok %d - %s\n", ++test_num, desc);
+}
+
+static void tap_fail(const char *desc, const char *reason)
+{
+ printf("not ok %d - %s # %s\n", ++test_num, desc, reason);
+ failures++;
+}
+
+static void sigpipe_handler(int sig)
+{
+ (void)sig;
+ got_sigpipe = 1;
+}
+
+/* ---- Valid operation tests ---- */
+
+/*
+ * Test 1: open a readable file
+ * Returns fd on success.
+ */
+static int test_open_valid(void)
+{
+ errno = 0;
+ long fd = kapi_sys_open("/etc/hostname", O_RDONLY, 0);
+
+ if (fd >= 0) {
+ tap_ok("open valid file");
+ } else {
+ /* /etc/hostname might not exist; try /etc/passwd */
+ errno = 0;
+ fd = kapi_sys_open("/etc/passwd", O_RDONLY, 0);
+ if (fd >= 0)
+ tap_ok("open valid file (fallback /etc/passwd)");
+ else
+ tap_fail("open valid file", strerror(errno));
+ }
+ return (int)fd;
+}
+
+/*
+ * Test 2: read from fd
+ */
+static void test_read_valid(int fd)
+{
+ char buf[256];
+
+ errno = 0;
+ long ret = syscall(__NR_read, fd, buf, sizeof(buf));
+
+ if (ret > 0)
+ tap_ok("read from valid fd");
+ else if (ret == 0)
+ tap_ok("read from valid fd (EOF)");
+ else
+ tap_fail("read from valid fd", strerror(errno));
+}
+
+/*
+ * Test 3: write to /dev/null
+ */
+static void test_write_valid(void)
+{
+ errno = 0;
+ long devnull = kapi_sys_open("/dev/null", O_WRONLY, 0);
+
+ if (devnull < 0) {
+ tap_fail("write to /dev/null (open failed)", strerror(errno));
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_write, (int)devnull, "hello", 5);
+
+ if (ret == 5)
+ tap_ok("write to /dev/null");
+ else
+ tap_fail("write to /dev/null",
+ ret < 0 ? strerror(errno) : "short write");
+
+ syscall(__NR_close, (int)devnull);
+}
+
+/*
+ * Test 4: close fd
+ */
+static void test_close_valid(int fd)
+{
+ errno = 0;
+ long ret = syscall(__NR_close, fd);
+
+ if (ret == 0)
+ tap_ok("close valid fd");
+ else
+ tap_fail("close valid fd", strerror(errno));
+}
+
+/* ---- KAPI parameter rejection tests ---- */
+
+/*
+ * Test 5: open with invalid flag bits
+ * 0x10000000 is outside the valid O_* mask, KAPI should reject.
+ */
+static void test_open_invalid_flags(void)
+{
+ errno = 0;
+ long ret = kapi_sys_open("/tmp/kapi_test", 0x10000000, 0);
+
+ if (ret == -1 && errno == EINVAL) {
+ tap_ok("open with invalid flags returns EINVAL");
+ } else if (ret >= 0) {
+ tap_fail("open with invalid flags", "expected EINVAL, got success");
+ syscall(__NR_close, (int)ret);
+ } else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EINVAL, got %s",
+ strerror(errno));
+ tap_fail("open with invalid flags", msg);
+ }
+}
+
+/*
+ * Test 6: open with invalid mode bits
+ * 0xFFFF has bits outside S_IALLUGO (07777), KAPI should reject.
+ */
+static void test_open_invalid_mode(void)
+{
+ errno = 0;
+ long ret = kapi_sys_open("/tmp/kapi_test_mode",
+ O_CREAT | O_WRONLY, 0xFFFF);
+
+ if (ret == -1 && errno == EINVAL) {
+ tap_ok("open with invalid mode returns EINVAL");
+ } else if (ret >= 0) {
+ tap_fail("open with invalid mode", "expected EINVAL, got success");
+ syscall(__NR_close, (int)ret);
+ unlink("/tmp/kapi_test_mode");
+ } else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EINVAL, got %s",
+ strerror(errno));
+ tap_fail("open with invalid mode", msg);
+ }
+}
+
+/*
+ * Test 7: open with NULL path
+ * KAPI USER_PATH constraint should reject NULL.
+ */
+static void test_open_null_path(void)
+{
+ errno = 0;
+ long ret = kapi_sys_open(NULL, O_RDONLY, 0);
+
+ if (ret == -1 && errno == EINVAL) {
+ tap_ok("open with NULL path returns EINVAL");
+ } else if (ret == -1 && errno == EFAULT) {
+ /* Kernel may catch this as EFAULT before KAPI */
+ tap_ok("open with NULL path returns EFAULT (acceptable)");
+ } else if (ret >= 0) {
+ tap_fail("open with NULL path", "expected error, got success");
+ syscall(__NR_close, (int)ret);
+ } else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "got %s", strerror(errno));
+ tap_fail("open with NULL path", msg);
+ }
+}
+
+/*
+ * Test 8: open with flag bit 30 set (0x40000000)
+ * This bit is outside the valid O_* mask, KAPI should reject with EINVAL.
+ */
+static void test_open_flag_bit30(void)
+{
+ errno = 0;
+ long ret = kapi_sys_open("/dev/null", 0x40000000, 0);
+
+ if (ret == -1 && errno == EINVAL)
+ tap_ok("open with flag bit 30 (0x40000000) returns EINVAL");
+ else if (ret >= 0) {
+ tap_fail("open with flag bit 30 (0x40000000) returns EINVAL",
+ "expected EINVAL, got success");
+ syscall(__NR_close, (int)ret);
+ } else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EINVAL, got %s",
+ strerror(errno));
+ tap_fail("open with flag bit 30 (0x40000000) returns EINVAL",
+ msg);
+ }
+}
+
+/* ---- Boundary condition and error path tests ---- */
+
+/*
+ * Test 9: read with fd=-1 should return an error.
+ * With CONFIG_KAPI_RUNTIME_CHECKS=y, KAPI validates the fd first and
+ * rejects negative fds (other than AT_FDCWD) with EINVAL. Without
+ * KAPI, the kernel returns EBADF. Accept either.
+ */
+static void test_read_bad_fd(void)
+{
+ char buf[16];
+
+ errno = 0;
+ long ret = syscall(__NR_read, -1, buf, sizeof(buf));
+
+ if (ret == -1 && (errno == EBADF || errno == EINVAL))
+ tap_ok("read with fd=-1 returns error");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EBADF/EINVAL, got %s",
+ ret >= 0 ? "success" : strerror(errno));
+ tap_fail("read with fd=-1 returns error", msg);
+ }
+}
+
+/*
+ * Test 10: read with count=0 should return 0
+ */
+static void test_read_zero_count(void)
+{
+ char buf[1];
+ long fd;
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+ if (fd < 0) {
+ tap_fail("read with count=0 returns 0",
+ "cannot open /dev/null");
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_read, (int)fd, buf, 0);
+
+ if (ret == 0)
+ tap_ok("read with count=0 returns 0");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+ ret, strerror(errno));
+ tap_fail("read with count=0 returns 0", msg);
+ }
+
+ syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 11: write with count=0 should return 0
+ */
+static void test_write_zero_count(void)
+{
+ long fd;
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+ if (fd < 0) {
+ tap_fail("write with count=0 returns 0",
+ "cannot open /dev/null");
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_write, (int)fd, "x", 0);
+
+ if (ret == 0)
+ tap_ok("write with count=0 returns 0");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+ ret, strerror(errno));
+ tap_fail("write with count=0 returns 0", msg);
+ }
+
+ syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 12: open with a path longer than PATH_MAX should fail
+ * Expect ENAMETOOLONG or EINVAL.
+ */
+static void test_open_long_path(void)
+{
+ char *longpath;
+ size_t len = PATH_MAX + 256;
+
+ longpath = malloc(len);
+ if (!longpath) {
+ tap_fail("open with path > PATH_MAX", "malloc failed");
+ return;
+ }
+
+ memset(longpath, 'A', len - 1);
+ longpath[0] = '/';
+ longpath[len - 1] = '\0';
+
+ errno = 0;
+ long ret = kapi_sys_open(longpath, O_RDONLY, 0);
+
+ if (ret == -1 && (errno == ENAMETOOLONG || errno == EINVAL))
+ tap_ok("open with path > PATH_MAX returns ENAMETOOLONG/EINVAL");
+ else if (ret >= 0) {
+ tap_fail("open with path > PATH_MAX",
+ "expected error, got success");
+ syscall(__NR_close, (int)ret);
+ } else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg),
+ "expected ENAMETOOLONG/EINVAL, got %s",
+ strerror(errno));
+ tap_fail("open with path > PATH_MAX", msg);
+ }
+
+ free(longpath);
+}
+
+/*
+ * Test 13: read with unmapped user pointer should return EFAULT or EINVAL.
+ * Use a pipe with data so the kernel actually tries to copy to the buffer.
+ */
+static void test_read_unmapped_buf(void)
+{
+ int pipefd[2];
+
+ if (pipe(pipefd) < 0) {
+ tap_fail("read with unmapped buffer returns EFAULT/EINVAL",
+ "pipe() failed");
+ return;
+ }
+
+ /* Write some data so read has something to copy */
+ (void)write(pipefd[1], "hello", 5);
+
+ errno = 0;
+ long ret = syscall(__NR_read, pipefd[0], (void *)0xDEAD0000, 16);
+
+ if (ret == -1 && (errno == EFAULT || errno == EINVAL))
+ tap_ok("read with unmapped buffer returns EFAULT/EINVAL");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg),
+ "expected EFAULT/EINVAL, got %s",
+ ret >= 0 ? "success" : strerror(errno));
+ tap_fail("read with unmapped buffer returns EFAULT/EINVAL",
+ msg);
+ }
+
+ close(pipefd[0]);
+ close(pipefd[1]);
+}
+
+/*
+ * Test 14: write with unmapped user pointer should return EFAULT or EINVAL.
+ * Use a pipe so the kernel actually tries to copy from the buffer.
+ */
+static void test_write_unmapped_buf(void)
+{
+ int pipefd[2];
+
+ if (pipe(pipefd) < 0) {
+ tap_fail("write with unmapped buffer returns EFAULT/EINVAL",
+ "pipe() failed");
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_write, pipefd[1], (void *)0xDEAD0000, 16);
+
+ if (ret == -1 && (errno == EFAULT || errno == EINVAL))
+ tap_ok("write with unmapped buffer returns EFAULT/EINVAL");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg),
+ "expected EFAULT/EINVAL, got %s",
+ ret >= 0 ? "success" : strerror(errno));
+ tap_fail("write with unmapped buffer returns EFAULT/EINVAL",
+ msg);
+ }
+
+ close(pipefd[0]);
+ close(pipefd[1]);
+}
+
+/*
+ * Test 15: close an already-closed fd should return EBADF
+ */
+static void test_close_already_closed(void)
+{
+ long fd;
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+ if (fd < 0) {
+ tap_fail("close already-closed fd returns EBADF",
+ "cannot open /dev/null");
+ return;
+ }
+
+ /* Close it once - should succeed */
+ syscall(__NR_close, (int)fd);
+
+ /* Close it again - should fail with EBADF */
+ errno = 0;
+ long ret = syscall(__NR_close, (int)fd);
+
+ if (ret == -1 && errno == EBADF)
+ tap_ok("close already-closed fd returns EBADF");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+ ret == 0 ? "success" : strerror(errno));
+ tap_fail("close already-closed fd returns EBADF", msg);
+ }
+}
+
+/*
+ * Test 16: open /dev/null with O_RDONLY|O_CLOEXEC should succeed
+ */
+static void test_open_valid_cloexec(void)
+{
+ errno = 0;
+ long fd = kapi_sys_open("/dev/null", O_RDONLY | O_CLOEXEC, 0);
+
+ if (fd >= 0) {
+ tap_ok("open /dev/null with O_RDONLY|O_CLOEXEC succeeds");
+ syscall(__NR_close, (int)fd);
+ } else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected success, got %s",
+ strerror(errno));
+ tap_fail("open /dev/null with O_RDONLY|O_CLOEXEC succeeds",
+ msg);
+ }
+}
+
+/*
+ * Test 17: write 0 bytes to /dev/null should return 0
+ */
+static void test_write_zero_devnull(void)
+{
+ long fd;
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+ if (fd < 0) {
+ tap_fail("write 0 bytes to /dev/null returns 0",
+ "cannot open /dev/null");
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_write, (int)fd, "", 0);
+
+ if (ret == 0)
+ tap_ok("write 0 bytes to /dev/null returns 0");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+ ret, strerror(errno));
+ tap_fail("write 0 bytes to /dev/null returns 0", msg);
+ }
+
+ syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 18: read from a write-only fd should return EBADF
+ */
+static void test_read_writeonly_fd(void)
+{
+ long fd;
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+ if (fd < 0) {
+ tap_fail("read from write-only fd returns EBADF",
+ "cannot open /dev/null");
+ return;
+ }
+
+ char buf[16];
+
+ errno = 0;
+ long ret = syscall(__NR_read, (int)fd, buf, sizeof(buf));
+
+ if (ret == -1 && errno == EBADF)
+ tap_ok("read from write-only fd returns EBADF");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+ ret >= 0 ? "success" : strerror(errno));
+ tap_fail("read from write-only fd returns EBADF", msg);
+ }
+
+ syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 19: write to a read-only fd should return EBADF
+ */
+static void test_write_readonly_fd(void)
+{
+ long fd;
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+ if (fd < 0) {
+ tap_fail("write to read-only fd returns EBADF",
+ "cannot open /dev/null");
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_write, (int)fd, "hello", 5);
+
+ if (ret == -1 && errno == EBADF)
+ tap_ok("write to read-only fd returns EBADF");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+ ret >= 0 ? "success" : strerror(errno));
+ tap_fail("write to read-only fd returns EBADF", msg);
+ }
+
+ syscall(__NR_close, (int)fd);
+}
+
+/*
+ * Test 20: close fd 9999 (likely invalid) should return EBADF
+ */
+static void test_close_fd_9999(void)
+{
+ errno = 0;
+ long ret = syscall(__NR_close, 9999);
+
+ if (ret == -1 && errno == EBADF)
+ tap_ok("close fd 9999 returns EBADF");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+ ret == 0 ? "success" : strerror(errno));
+ tap_fail("close fd 9999 returns EBADF", msg);
+ }
+}
+
+/*
+ * Test 21: read from pipe after write end is closed returns 0 (EOF)
+ */
+static void test_read_closed_pipe(void)
+{
+ int pipefd[2];
+
+ if (pipe(pipefd) < 0) {
+ tap_fail("read from closed pipe returns 0 (EOF)",
+ "pipe() failed");
+ return;
+ }
+
+ /* Close write end */
+ close(pipefd[1]);
+
+ char buf[16];
+
+ errno = 0;
+ long ret = syscall(__NR_read, pipefd[0], buf, sizeof(buf));
+
+ if (ret == 0)
+ tap_ok("read from closed pipe returns 0 (EOF)");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected 0, got %ld (errno=%s)",
+ ret, ret < 0 ? strerror(errno) : "n/a");
+ tap_fail("read from closed pipe returns 0 (EOF)", msg);
+ }
+
+ close(pipefd[0]);
+}
+
+/*
+ * Test 22: write to pipe after read end is closed returns EPIPE + SIGPIPE
+ */
+static void test_write_closed_pipe(void)
+{
+ int pipefd[2];
+ struct sigaction sa, old_sa;
+
+ if (pipe(pipefd) < 0) {
+ tap_fail("write to closed pipe returns EPIPE + SIGPIPE",
+ "pipe() failed");
+ return;
+ }
+
+ /* Install SIGPIPE handler */
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = sigpipe_handler;
+ sigemptyset(&sa.sa_mask);
+ sigaction(SIGPIPE, &sa, &old_sa);
+
+ got_sigpipe = 0;
+
+ /* Close read end */
+ close(pipefd[0]);
+
+ errno = 0;
+ long ret = syscall(__NR_write, pipefd[1], "hello", 5);
+
+ if (ret == -1 && errno == EPIPE && got_sigpipe)
+ tap_ok("write to closed pipe returns EPIPE + SIGPIPE");
+ else if (ret == -1 && errno == EPIPE)
+ tap_ok("write to closed pipe returns EPIPE (SIGPIPE not caught)");
+ else {
+ char msg[128];
+
+ snprintf(msg, sizeof(msg),
+ "expected EPIPE, got %s (sigpipe=%d)",
+ ret >= 0 ? "success" : strerror(errno),
+ got_sigpipe);
+ tap_fail("write to closed pipe returns EPIPE + SIGPIPE", msg);
+ }
+
+ /* Restore SIGPIPE handler */
+ sigaction(SIGPIPE, &old_sa, NULL);
+ close(pipefd[1]);
+}
+
+/*
+ * Test 23: open with O_DIRECTORY on a regular file returns ENOTDIR
+ */
+static void test_open_directory_on_file(void)
+{
+ errno = 0;
+ long ret = kapi_sys_open("/dev/null", O_RDONLY | O_DIRECTORY, 0);
+
+ if (ret == -1 && errno == ENOTDIR)
+ tap_ok("open O_DIRECTORY on regular file returns ENOTDIR");
+ else if (ret >= 0) {
+ tap_fail("open O_DIRECTORY on regular file",
+ "expected ENOTDIR, got success");
+ syscall(__NR_close, (int)ret);
+ } else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected ENOTDIR, got %s",
+ strerror(errno));
+ tap_fail("open O_DIRECTORY on regular file", msg);
+ }
+}
+
+/*
+ * Test 24: open nonexistent file without O_CREAT returns ENOENT
+ */
+static void test_open_nonexistent(void)
+{
+ errno = 0;
+ long ret = kapi_sys_open(
+ "/tmp/kapi_nonexistent_file_12345", O_RDONLY, 0);
+
+ if (ret == -1 && errno == ENOENT)
+ tap_ok("open nonexistent file without O_CREAT returns ENOENT");
+ else if (ret >= 0) {
+ tap_fail("open nonexistent file",
+ "expected ENOENT, got success (file exists?)");
+ syscall(__NR_close, (int)ret);
+ } else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected ENOENT, got %s",
+ strerror(errno));
+ tap_fail("open nonexistent file", msg);
+ }
+}
+
+/*
+ * Test 25: close stdin (fd 0) should succeed
+ * We dup it first so we can restore it.
+ */
+static void test_close_stdin(void)
+{
+ int saved_stdin = dup(0);
+
+ if (saved_stdin < 0) {
+ tap_fail("close stdin succeeds", "cannot dup stdin");
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_close, 0);
+
+ if (ret == 0)
+ tap_ok("close stdin (fd 0) succeeds");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected success, got %s",
+ strerror(errno));
+ tap_fail("close stdin (fd 0) succeeds", msg);
+ }
+
+ /* Restore stdin */
+ dup2(saved_stdin, 0);
+ close(saved_stdin);
+}
+
+/*
+ * Test 26: read after close returns EBADF
+ */
+static void test_read_after_close(void)
+{
+ long fd;
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_RDONLY, 0);
+ if (fd < 0) {
+ tap_fail("read after close returns EBADF",
+ "cannot open /dev/null");
+ return;
+ }
+
+ syscall(__NR_close, (int)fd);
+
+ char buf[16];
+
+ errno = 0;
+ long ret = syscall(__NR_read, (int)fd, buf, sizeof(buf));
+
+ if (ret == -1 && errno == EBADF)
+ tap_ok("read after close returns EBADF");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected EBADF, got %s",
+ ret >= 0 ? "success" : strerror(errno));
+ tap_fail("read after close returns EBADF", msg);
+ }
+}
+
+/*
+ * Test 27: write with large count
+ * Without KAPI: the kernel clamps count to MAX_RW_COUNT and succeeds.
+ * With KAPI: KAPI validates the buffer against the count and may
+ * return EFAULT/EINVAL since the buffer is smaller than count.
+ * Accept either success or EFAULT/EINVAL.
+ */
+static void test_write_large_count(void)
+{
+ long fd;
+ char buf[64] = "test data";
+
+ errno = 0;
+ fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+ if (fd < 0) {
+ tap_fail("write with large count handled correctly",
+ "cannot open /dev/null");
+ return;
+ }
+
+ errno = 0;
+ long ret = syscall(__NR_write, (int)fd, buf, (size_t)0x7ffff000UL);
+
+ if (ret > 0)
+ tap_ok("write with large count succeeds (clamped, no KAPI)");
+ else if (ret == -1 && (errno == EFAULT || errno == EINVAL))
+ tap_ok("write with large count returns EFAULT/EINVAL (KAPI validates buffer)");
+ else {
+ char msg[64];
+
+ snprintf(msg, sizeof(msg), "expected success or EFAULT, got %s",
+ ret == 0 ? "zero" : strerror(errno));
+ tap_fail("write with large count handled correctly", msg);
+ }
+
+ syscall(__NR_close, (int)fd);
+}
+
+/* ---- Integration tests ---- */
+
+/*
+ * Test 28: full normal syscall path - open, read, write, close
+ * Verify KAPI does not interfere with normal operations.
+ */
+static void test_normal_path(void)
+{
+ long rd_fd, wr_fd;
+ char buf[128];
+ int ok = 1;
+ char reason[128] = "";
+
+ /* Open a readable file */
+ errno = 0;
+ rd_fd = kapi_sys_open("/etc/hostname", O_RDONLY, 0);
+ if (rd_fd < 0) {
+ errno = 0;
+ rd_fd = kapi_sys_open("/etc/passwd", O_RDONLY, 0);
+ }
+ if (rd_fd < 0) {
+ snprintf(reason, sizeof(reason), "open readable file: %s",
+ strerror(errno));
+ ok = 0;
+ }
+
+ /* Read from it */
+ if (ok) {
+ errno = 0;
+ long n = syscall(__NR_read, (int)rd_fd, buf, sizeof(buf));
+
+ if (n < 0) {
+ snprintf(reason, sizeof(reason), "read: %s",
+ strerror(errno));
+ ok = 0;
+ }
+ }
+
+ /* Open /dev/null for writing */
+ wr_fd = -1;
+ if (ok) {
+ errno = 0;
+ wr_fd = kapi_sys_open("/dev/null", O_WRONLY, 0);
+ if (wr_fd < 0) {
+ snprintf(reason, sizeof(reason),
+ "open /dev/null: %s", strerror(errno));
+ ok = 0;
+ }
+ }
+
+ /* Write to /dev/null */
+ if (ok) {
+ errno = 0;
+ long n = syscall(__NR_write, (int)wr_fd, "test", 4);
+
+ if (n != 4) {
+ snprintf(reason, sizeof(reason), "write: %s",
+ n < 0 ? strerror(errno) : "short write");
+ ok = 0;
+ }
+ }
+
+ /* Close both fds */
+ if (rd_fd >= 0) {
+ errno = 0;
+ if (syscall(__NR_close, (int)rd_fd) != 0 && ok) {
+ snprintf(reason, sizeof(reason), "close read fd: %s",
+ strerror(errno));
+ ok = 0;
+ }
+ }
+
+ if (wr_fd >= 0) {
+ errno = 0;
+ if (syscall(__NR_close, (int)wr_fd) != 0 && ok) {
+ snprintf(reason, sizeof(reason), "close write fd: %s",
+ strerror(errno));
+ ok = 0;
+ }
+ }
+
+ if (ok)
+ tap_ok("normal syscall path (open/read/write/close) works");
+ else
+ tap_fail("normal syscall path (open/read/write/close) works",
+ reason);
+}
+
+/*
+ * Test 29: verify dmesg contains KAPI warnings for the invalid tests
+ */
+static void test_dmesg_warnings(void)
+{
+ int kmsg_fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+
+ if (kmsg_fd < 0) {
+ tap_ok("dmesg check skipped (cannot open /dev/kmsg)");
+ return;
+ }
+
+ /* Read all available kmsg messages from the start */
+ lseek(kmsg_fd, 0, SEEK_DATA);
+
+ char line[4096];
+ int found_invalid_bits = 0;
+ int found_null = 0;
+ ssize_t n;
+
+ while ((n = read(kmsg_fd, line, sizeof(line) - 1)) > 0) {
+ line[n] = '\0';
+ if (strstr(line, "contains invalid bits"))
+ found_invalid_bits++;
+ if (strstr(line, "NULL") && strstr(line, "not allowed"))
+ found_null++;
+ }
+
+ close(kmsg_fd);
+
+ if (found_invalid_bits >= 2 && found_null >= 1)
+ tap_ok("dmesg contains expected KAPI warnings");
+ else if (found_invalid_bits >= 1 || found_null >= 1) {
+ char msg[128];
+
+ snprintf(msg, sizeof(msg),
+ "partial: invalid_bits=%d null=%d",
+ found_invalid_bits, found_null);
+ tap_ok(msg);
+ } else {
+ tap_fail("dmesg KAPI warnings",
+ "no KAPI warnings found in dmesg");
+ }
+}
+
+int main(void)
+{
+ printf("TAP version 13\n");
+ printf("1..%d\n", NUM_TESTS);
+
+ /* Valid operations (1-4) */
+ int fd = test_open_valid();
+
+ if (fd >= 0)
+ test_read_valid(fd);
+ else
+ tap_fail("read from valid fd", "no fd from open");
+
+ test_write_valid();
+
+ if (fd >= 0)
+ test_close_valid(fd);
+ else
+ tap_fail("close valid fd", "no fd from open");
+
+ /* KAPI parameter rejection (5-8) */
+ test_open_invalid_flags();
+ test_open_invalid_mode();
+ test_open_null_path();
+ test_open_flag_bit30();
+
+ /* Boundary conditions and error paths (9-20) */
+ test_read_bad_fd();
+ test_read_zero_count();
+ test_write_zero_count();
+ test_open_long_path();
+ test_read_unmapped_buf();
+ test_write_unmapped_buf();
+ test_close_already_closed();
+ test_open_valid_cloexec();
+ test_write_zero_devnull();
+ test_read_writeonly_fd();
+ test_write_readonly_fd();
+ test_close_fd_9999();
+
+ /* Pipe and lifecycle tests (21-27) */
+ test_read_closed_pipe();
+ test_write_closed_pipe();
+ test_open_directory_on_file();
+ test_open_nonexistent();
+ test_close_stdin();
+ test_read_after_close();
+ test_write_large_count();
+
+ /* Integration (28-29) */
+ test_normal_path();
+ test_dmesg_warnings();
+
+ if (failures)
+ fprintf(stderr, "# %d test(s) failed\n", failures);
+ else
+ fprintf(stderr, "# All tests passed\n");
+
+ return failures ? 1 : 0;
+}
--
2.51.0
^ permalink raw reply related
* [PATCH v2 8/9] kernel/api: add API specification for sys_write
From: Sasha Levin @ 2026-03-22 12:10 UTC (permalink / raw)
To: linux-api, linux-kernel
Cc: linux-doc, linux-fsdevel, linux-kbuild, linux-kselftest,
workflows, tools, x86, Thomas Gleixner, Paul E . McKenney,
Greg Kroah-Hartman, Jonathan Corbet, Dmitry Vyukov, Randy Dunlap,
Cyril Hrubis, Kees Cook, Jake Edge, David Laight, Askar Safin,
Gabriele Paoloni, Mauro Carvalho Chehab, Christian Brauner,
Alexander Viro, Andrew Morton, Masahiro Yamada, Shuah Khan,
Ingo Molnar, Arnd Bergmann, Sasha Levin
In-Reply-To: <20260322121026.869758-1-sashal@kernel.org>
Add KAPI-annotated kerneldoc for the sys_write system call in
fs/read_write.c.
The specification documents parameter constraints (fd, user buffer,
count), error conditions, locking requirements, signal handling
behavior, and short write semantics.
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
fs/read_write.c | 380 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 380 insertions(+)
diff --git a/fs/read_write.c b/fs/read_write.c
index bee19b7ddadcf..302031516275b 100644
--- a/fs/read_write.c
+++ b/fs/read_write.c
@@ -1043,6 +1043,386 @@ ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
return ret;
}
+/**
+ * sys_write - Write data to a file descriptor
+ * @fd: File descriptor to write to
+ * @buf: User-space buffer containing data to write
+ * @count: Maximum number of bytes to write
+ *
+ * long-desc: Attempts to write up to count bytes from the buffer starting at
+ * buf to the file referred to by the file descriptor fd. For seekable files
+ * (regular files, block devices), the write begins at the current file offset,
+ * and the file offset is advanced by the number of bytes written. If the file
+ * was opened with O_APPEND, the file offset is first set to the end of the
+ * file before writing. For non-seekable files (pipes, FIFOs, sockets, character
+ * devices), the file offset is not used and writing occurs at the current
+ * position as defined by the device.
+ *
+ * The number of bytes written may be less than count if, for example, there is
+ * insufficient space on the underlying physical medium, or the RLIMIT_FSIZE
+ * resource limit is encountered, or the call was interrupted by a signal
+ * handler after having written less than count bytes. In the event of a
+ * successful partial write, the caller should make another write() call to
+ * transfer the remaining bytes. This behavior is called a "short write."
+ *
+ * On Linux, write() transfers at most MAX_RW_COUNT (0x7ffff000, approximately
+ * 2GB minus one page) bytes per call, regardless of whether the file or
+ * filesystem would allow more. This prevents signed arithmetic overflow.
+ *
+ * For regular files, a successful write() does not guarantee that data has been
+ * committed to disk. Use fsync(2) or fdatasync(2) if durability is required.
+ * For O_SYNC or O_DSYNC files, the kernel automatically syncs data on write.
+ *
+ * POSIX permits writes that are interrupted after partial writes to either
+ * return -1 with errno=EINTR, or to return the count of bytes already written.
+ * Linux implements the latter behavior: if some data has been written before
+ * a signal arrives, write() returns the number of bytes written rather than
+ * failing with EINTR.
+ *
+ * context-flags: KAPI_CTX_PROCESS | KAPI_CTX_SLEEPABLE
+ *
+ * param: fd
+ * type: KAPI_TYPE_FD
+ * flags: KAPI_PARAM_IN
+ * constraint-type: KAPI_CONSTRAINT_RANGE
+ * range: 0, INT_MAX
+ * cdesc: Must be a valid, open file descriptor with write permission.
+ * The file must have been opened with O_WRONLY or O_RDWR. File descriptors
+ * opened with O_RDONLY, O_PATH, or that have been closed return EBADF.
+ * Standard file descriptors 0 (stdin), 1 (stdout), 2 (stderr) are valid if
+ * open and writable. AT_FDCWD and other special values are not valid.
+ *
+ * param: buf
+ * type: KAPI_TYPE_USER_PTR
+ * flags: KAPI_PARAM_IN | KAPI_PARAM_USER
+ * constraint-type: KAPI_CONSTRAINT_BUFFER
+ * size-param: 2
+ * cdesc: Must point to a valid, readable user-space memory region of at
+ * least count bytes. The buffer is validated via access_ok() before any
+ * write operation. NULL is invalid and returns EFAULT. For O_DIRECT writes,
+ * the buffer may need to be aligned to the filesystem's block size (varies
+ * by filesystem; query with statx() using STATX_DIOALIGN on Linux 6.1+).
+ *
+ * param: count
+ * type: KAPI_TYPE_UINT
+ * flags: KAPI_PARAM_IN
+ * constraint-type: KAPI_CONSTRAINT_RANGE
+ * range: 0, SIZE_MAX
+ * cdesc: Maximum number of bytes to write. Clamped internally to
+ * MAX_RW_COUNT (INT_MAX & PAGE_MASK, approximately 0x7ffff000 bytes) to
+ * prevent signed overflow. A count of 0 returns 0 immediately without any
+ * file operations. Cast to ssize_t must not be negative.
+ *
+ * return:
+ * type: KAPI_TYPE_INT
+ * check-type: KAPI_RETURN_RANGE
+ * success: >= 0
+ * desc: On success, returns the number of bytes written (non-negative). Zero
+ * indicates that nothing was written (count was 0, or no space available
+ * for non-blocking writes). The return value may be less than count due to
+ * resource limits, signal interruption, or device constraints (short write).
+ * On error, returns a negative error code.
+ *
+ * error: EBADF, Bad file descriptor
+ * desc: fd is not a valid file descriptor, or fd was not opened for writing.
+ * This includes file descriptors opened with O_RDONLY, O_PATH, or file
+ * descriptors that have been closed. Also returned if the file structure
+ * does not have FMODE_WRITE or FMODE_CAN_WRITE set.
+ *
+ * error: EFAULT, Bad address
+ * desc: buf points outside the accessible address space. The buffer address
+ * failed access_ok() validation. Can also occur if a fault happens during
+ * copy_from_user() when reading data from user space.
+ *
+ * error: EINVAL, Invalid argument
+ * desc: Returned in several cases: (1) The file descriptor refers to an
+ * object that is not suitable for writing (no write or write_iter method).
+ * (2) The file was opened with O_DIRECT and the buffer alignment, offset,
+ * or count does not meet the filesystem's alignment requirements. (3) The
+ * count argument, when cast to ssize_t, is negative. (4) For IOCB_NOWAIT
+ * operations on non-O_DIRECT files that don't support WASYNC.
+ *
+ * error: EAGAIN, Resource temporarily unavailable
+ * desc: fd refers to a file (pipe, socket, device) that is marked non-blocking
+ * (O_NONBLOCK) and the write would block because the buffer is full. Also
+ * returned with IOCB_NOWAIT when data cannot be written immediately.
+ * Equivalent to EWOULDBLOCK. The application should retry later or use
+ * select/poll/epoll to wait for writability.
+ *
+ * error: EINTR, Interrupted system call
+ * desc: The call was interrupted by a signal before any data was written. This
+ * only occurs if no data has been transferred; if some data was written
+ * before the signal, the call returns the number of bytes written. The
+ * caller should typically restart the write.
+ *
+ * error: EPIPE, Broken pipe
+ * desc: fd refers to a pipe or socket whose reading end has been closed.
+ * When this condition occurs, the calling process also receives a SIGPIPE
+ * signal. If the signal is caught or ignored, EPIPE is still returned.
+ * For sockets, MSG_NOSIGNAL (via send()) suppresses the signal. For
+ * pwritev2(), the RWF_NOSIGNAL flag suppresses it.
+ *
+ * error: EFBIG, File too large
+ * desc: An attempt was made to write a file that exceeds the implementation-
+ * defined maximum file size or the file size limit (RLIMIT_FSIZE) of the
+ * process. When RLIMIT_FSIZE is exceeded, the process also receives SIGXFSZ.
+ * For files not opened with O_LARGEFILE on 32-bit systems, the limit is 2GB.
+ *
+ * error: ENOSPC, No space left on device
+ * desc: The device containing the file has no room for the data. This can
+ * occur mid-write resulting in a short write followed by ENOSPC on retry.
+ *
+ * error: EDQUOT, Disk quota exceeded
+ * desc: The user's quota of disk blocks on the filesystem has been exhausted.
+ * Like ENOSPC, this can result in a short write.
+ *
+ * error: EIO, Input/output error
+ * desc: A low-level I/O error occurred while modifying the inode or writing
+ * data. This typically indicates hardware failure, filesystem corruption,
+ * or network filesystem timeout. Some data may have been written.
+ *
+ * error: EPERM, Operation not permitted
+ * desc: The operation was prevented: (1) by a file seal (F_SEAL_WRITE or
+ * F_SEAL_FUTURE_WRITE on memfd/shmem), (2) writing to an immutable inode
+ * (IS_IMMUTABLE), (3) by an LSM hook denying the operation, or (4) by a
+ * fanotify permission event denying the write.
+ *
+ * error: EOVERFLOW, Value too large for defined data type
+ * desc: The file position plus count would exceed LLONG_MAX. Also returned
+ * when the offset would exceed filesystem limits after the write.
+ *
+ * error: EDESTADDRREQ, Destination address required
+ * desc: fd is a datagram socket for which no peer address has been set using
+ * connect(2). Use sendto(2) to specify the destination address.
+ *
+ * error: ETXTBSY, Text file busy
+ * desc: The file is being used as a swap file (IS_SWAPFILE). Note: unlike
+ * the traditional Unix meaning, Linux does not return ETXTBSY when writing
+ * to an executing binary; that only blocks open() with O_WRONLY/O_RDWR.
+ *
+ * error: EXDEV, Cross-device link
+ * desc: When writing to a pipe that has been configured as a watch queue
+ * (CONFIG_WATCH_QUEUE), direct write() calls are not supported.
+ *
+ * error: ENOMEM, Out of memory
+ * desc: Insufficient kernel memory was available for the write operation.
+ * For pipes, this occurs when allocating pages for the pipe buffer.
+ *
+ * error: ERESTARTSYS, Restart system call (internal)
+ * desc: Internal error code indicating the syscall should be restarted. This
+ * is converted to EINTR if SA_RESTART is not set on the signal handler, or
+ * the syscall is transparently restarted if SA_RESTART is set. User space
+ * should not see this error code directly.
+ *
+ * error: EACCES, Permission denied
+ * desc: The security subsystem (LSM such as SELinux or AppArmor) denied the
+ * write operation via security_file_permission(). This can occur even if
+ * the file was successfully opened.
+ *
+ * lock: file->f_pos_lock
+ * type: KAPI_LOCK_MUTEX
+ * acquired: conditional
+ * released: true
+ * desc: For regular files that require atomic position updates (FMODE_ATOMIC_POS),
+ * the f_pos_lock mutex is acquired by fdget_pos() at syscall entry and released
+ * by fdput_pos() at syscall exit. This serializes concurrent writes sharing
+ * the same file description. Not acquired for stream files (FMODE_STREAM like
+ * pipes and sockets) or when the file is not shared.
+ *
+ * lock: sb->s_writers (freeze protection)
+ * type: KAPI_LOCK_CUSTOM
+ * acquired: conditional
+ * released: true
+ * desc: For regular files, file_start_write() acquires freeze protection on
+ * the superblock via sb_start_write() before the write, and file_end_write()
+ * releases it after. This prevents writes during filesystem freeze. Not
+ * acquired for non-regular files (pipes, sockets, devices).
+ *
+ * lock: inode->i_rwsem
+ * type: KAPI_LOCK_RWLOCK
+ * acquired: conditional
+ * released: true
+ * desc: For regular files using generic_file_write_iter(), the inode's i_rwsem
+ * is acquired in write mode before modifying file data. This is internal to
+ * the filesystem and released before return. Not all filesystems use this
+ * pattern.
+ *
+ * lock: pipe->mutex
+ * type: KAPI_LOCK_MUTEX
+ * acquired: conditional
+ * released: true
+ * desc: For pipes and FIFOs, the pipe's mutex is held while modifying pipe
+ * buffers. Released temporarily while waiting for space, then reacquired.
+ *
+ * lock: RCU read-side
+ * type: KAPI_LOCK_RCU
+ * acquired: conditional
+ * released: true
+ * desc: Used during file descriptor lookup via fdget(). RCU read lock protects
+ * access to the file descriptor table. Released by fdput() at syscall exit.
+ *
+ * signal: SIGPIPE
+ * direction: KAPI_SIGNAL_SEND
+ * action: KAPI_SIGNAL_ACTION_TERMINATE
+ * condition: Writing to a pipe or socket with no readers
+ * desc: When writing to a pipe whose read end is closed, or a socket whose
+ * peer has closed, SIGPIPE is sent to the calling process. The default
+ * action terminates the process. Use signal(SIGPIPE, SIG_IGN) to suppress
+ * for write(). EPIPE is returned regardless of signal disposition.
+ * timing: KAPI_SIGNAL_TIME_DURING
+ *
+ * signal: SIGXFSZ
+ * direction: KAPI_SIGNAL_SEND
+ * action: KAPI_SIGNAL_ACTION_COREDUMP
+ * condition: Writing exceeds RLIMIT_FSIZE
+ * desc: When a write would exceed the soft file size limit (RLIMIT_FSIZE),
+ * SIGXFSZ is sent. The default action terminates with a core dump. The
+ * write returns EFBIG. If RLIMIT_FSIZE is RLIM_INFINITY, no signal is sent.
+ * timing: KAPI_SIGNAL_TIME_DURING
+ *
+ * signal: Any signal
+ * direction: KAPI_SIGNAL_RECEIVE
+ * action: KAPI_SIGNAL_ACTION_RETURN
+ * condition: While blocked waiting for space (pipes, sockets)
+ * desc: The syscall may be interrupted by signals while waiting for buffer
+ * space to become available. If interrupted before any data is written,
+ * returns -EINTR or -ERESTARTSYS. If data was already written, returns the
+ * byte count. Restartable if SA_RESTART is set and no data was written.
+ * error: -EINTR
+ * timing: KAPI_SIGNAL_TIME_DURING
+ * restartable: yes
+ *
+ * side-effect: KAPI_EFFECT_FILE_POSITION
+ * target: file->f_pos
+ * condition: For seekable files when write succeeds (returns > 0)
+ * desc: The file offset (f_pos) is advanced by the number of bytes written.
+ * For files opened with O_APPEND, f_pos is first set to file size. For
+ * stream files (FMODE_STREAM such as pipes and sockets), the offset is not
+ * used or modified. Position updates are protected by f_pos_lock when
+ * shared.
+ * reversible: no
+ *
+ * side-effect: KAPI_EFFECT_MODIFY_STATE
+ * target: inode timestamps (mtime, ctime)
+ * condition: When write succeeds (returns > 0)
+ * desc: Updates the file's modification time (mtime) and change time (ctime)
+ * via file_update_time(). The update precision depends on filesystem mount
+ * options (fine-grained timestamps for multigrain inodes).
+ * reversible: no
+ *
+ * side-effect: KAPI_EFFECT_MODIFY_STATE
+ * target: SUID/SGID bits (mode)
+ * condition: When writing to a setuid/setgid file
+ * desc: The SUID bit is cleared when a non-root user writes to a file with
+ * the bit set. The SGID bit may also be cleared. This is a security feature
+ * to prevent privilege escalation via modified setuid binaries. Done via
+ * file_remove_privs().
+ * reversible: no
+ *
+ * side-effect: KAPI_EFFECT_MODIFY_STATE
+ * target: file data
+ * condition: When write succeeds (returns > 0)
+ * desc: Modifies the file's data content. For regular files, data is written
+ * to the page cache (buffered I/O) or directly to storage (O_DIRECT).
+ * Data may not be persistent until fsync() is called or the file is closed.
+ * reversible: no
+ *
+ * side-effect: KAPI_EFFECT_MODIFY_STATE
+ * target: task I/O accounting
+ * condition: Always
+ * desc: Updates the current task's I/O accounting statistics. The wchar field
+ * (write characters) is incremented by bytes written via add_wchar() only on
+ * successful writes (ret > 0). The syscw field (syscall write count) is
+ * incremented unconditionally via inc_syscw(). These statistics are visible
+ * in /proc/[pid]/io.
+ * reversible: no
+ *
+ * side-effect: KAPI_EFFECT_MODIFY_STATE
+ * target: fsnotify events
+ * condition: When write returns > 0
+ * desc: Generates an FS_MODIFY fsnotify event via fsnotify_modify(), allowing
+ * inotify, fanotify, and dnotify watchers to be notified of the write.
+ *
+ * capability: CAP_DAC_OVERRIDE
+ * type: KAPI_CAP_BYPASS_CHECK
+ * allows: Bypass discretionary access control on write permission
+ * without: Standard DAC checks are enforced
+ * condition: Checked via security_file_permission() during rw_verify_area()
+ *
+ * capability: CAP_FOWNER
+ * type: KAPI_CAP_BYPASS_CHECK
+ * allows: Bypass ownership checks for SUID/SGID clearing
+ * without: SUID/SGID bits are cleared on write by non-owner
+ * condition: Checked during file_remove_privs()
+ *
+ * constraint: MAX_RW_COUNT
+ * desc: The count parameter is silently clamped to MAX_RW_COUNT (INT_MAX &
+ * PAGE_MASK, approximately 2GB minus one page) to prevent integer overflow
+ * in internal calculations. This is transparent to the caller.
+ * expr: actual_count = min(count, MAX_RW_COUNT)
+ *
+ * constraint: File must be open for writing
+ * desc: The file descriptor must have been opened with O_WRONLY or O_RDWR.
+ * Files opened with O_RDONLY or O_PATH cannot be written and return EBADF.
+ * The file must have both FMODE_WRITE and FMODE_CAN_WRITE flags set.
+ * expr: (file->f_mode & FMODE_WRITE) && (file->f_mode & FMODE_CAN_WRITE)
+ *
+ * constraint: RLIMIT_FSIZE
+ * desc: The size of data written is constrained by the RLIMIT_FSIZE resource
+ * limit. If writing would exceed this limit, SIGXFSZ is sent and EFBIG is
+ * returned. The limit does not apply to files beyond the limit - only to
+ * writes that would cross it.
+ * expr: pos + count <= rlimit(RLIMIT_FSIZE) || rlimit(RLIMIT_FSIZE) == RLIM_INFINITY
+ *
+ * constraint: File seals
+ * desc: For memfd or shmem files with F_SEAL_WRITE or F_SEAL_FUTURE_WRITE
+ * seals applied, all write operations fail with EPERM. With F_SEAL_GROW,
+ * writes that would extend file size fail with EPERM.
+ *
+ * examples: n = write(fd, buf, sizeof(buf)); // Basic write
+ * n = write(STDOUT_FILENO, msg, strlen(msg)); // Write to stdout
+ * while (total < len) { n = write(fd, buf+total, len-total); if (n<0) break; total += n; } // Handle short writes
+ * if (write(pipefd[1], &byte, 1) < 0 && errno == EPIPE) { handle_broken_pipe(); } // Pipe error handling
+ *
+ * notes: The behavior of write() varies significantly depending on the type of
+ * file descriptor:
+ *
+ * - Regular files: Writes to the page cache (buffered) or directly to storage
+ * (O_DIRECT). Short writes are rare except near RLIMIT_FSIZE or disk full.
+ * O_APPEND is atomic for determining write position.
+ *
+ * - Pipes and FIFOs: Blocking by default. Writes up to PIPE_BUF (4096 bytes
+ * on Linux) are guaranteed atomic. Larger writes may be interleaved with
+ * writes from other processes. Blocks if pipe is full; returns EAGAIN with
+ * O_NONBLOCK. SIGPIPE/EPIPE if no readers.
+ *
+ * - Sockets: Behavior depends on socket type and protocol. Stream sockets
+ * (TCP) may return partial writes. Datagram sockets (UDP) typically write
+ * complete messages or fail. SIGPIPE/EPIPE for broken connections (unless
+ * MSG_NOSIGNAL). EDESTADDRREQ for unconnected datagram sockets.
+ *
+ * - Terminals: May block on flow control. Canonical vs raw mode affects
+ * behavior. Special characters may be interpreted.
+ *
+ * - Device special files: Behavior is device-specific. Block devices behave
+ * similarly to regular files. Character device behavior varies.
+ *
+ * Race condition considerations: Concurrent writes from threads sharing a
+ * file description race on the file position. Linux 3.14+ provides atomic
+ * position updates via f_pos_lock for regular files (FMODE_ATOMIC_POS), but
+ * for maximum safety, use pwrite() for concurrent positioned writes.
+ *
+ * O_DIRECT writes bypass the page cache and typically require buffer and
+ * offset alignment to filesystem block size. Query requirements via statx()
+ * with STATX_DIOALIGN (Linux 6.1+). Unaligned O_DIRECT writes return EINVAL
+ * on most filesystems.
+ *
+ * For zero-copy writes, consider using splice(2), sendfile(2), or vmsplice(2)
+ * instead of copying data through user-space buffers with write().
+ *
+ * Partial writes (short writes) must be handled by application code.
+ * Applications should loop until all data is written or an error occurs.
+ */
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
--
2.51.0
^ permalink raw reply related
* [PATCH v2 7/9] kernel/api: add API specification for sys_read
From: Sasha Levin @ 2026-03-22 12:10 UTC (permalink / raw)
To: linux-api, linux-kernel
Cc: linux-doc, linux-fsdevel, linux-kbuild, linux-kselftest,
workflows, tools, x86, Thomas Gleixner, Paul E . McKenney,
Greg Kroah-Hartman, Jonathan Corbet, Dmitry Vyukov, Randy Dunlap,
Cyril Hrubis, Kees Cook, Jake Edge, David Laight, Askar Safin,
Gabriele Paoloni, Mauro Carvalho Chehab, Christian Brauner,
Alexander Viro, Andrew Morton, Masahiro Yamada, Shuah Khan,
Ingo Molnar, Arnd Bergmann, Sasha Levin
In-Reply-To: <20260322121026.869758-1-sashal@kernel.org>
Add KAPI-annotated kerneldoc for the sys_read system call in
fs/read_write.c.
The specification documents parameter constraints (fd, user buffer,
count), error conditions, locking requirements, signal handling
behavior, and short read semantics.
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
fs/read_write.c | 303 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 303 insertions(+)
diff --git a/fs/read_write.c b/fs/read_write.c
index 50bff7edc91f3..bee19b7ddadcf 100644
--- a/fs/read_write.c
+++ b/fs/read_write.c
@@ -721,6 +721,304 @@ ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
return ret;
}
+/**
+ * sys_read - Read data from a file descriptor
+ * @fd: File descriptor to read from
+ * @buf: User-space buffer to read data into
+ * @count: Maximum number of bytes to read
+ *
+ * long-desc: Attempts to read up to count bytes from file descriptor fd into
+ * the buffer starting at buf. For seekable files (regular files, block
+ * devices), the read begins at the current file offset, and the file offset
+ * is advanced by the number of bytes read. For non-seekable files (pipes,
+ * FIFOs, sockets, character devices), the file offset is not used.
+ *
+ * If count is zero and fd refers to a regular file, read() may detect errors
+ * as described below. In the absence of errors, or if read() does not check
+ * for errors, a read() with a count of 0 returns zero and has no other effects.
+ *
+ * On success, the number of bytes read is returned (zero indicates end of
+ * file for regular files). It is not an error if this number is smaller than
+ * the number of bytes requested; this may happen because fewer bytes are
+ * actually available right now (maybe because we were close to end-of-file,
+ * or because we are reading from a pipe, socket, or terminal), or because
+ * read() was interrupted by a signal.
+ *
+ * On Linux, read() transfers at most MAX_RW_COUNT (0x7ffff000, approximately
+ * 2GB) bytes per call, regardless of whether the filesystem would allow more.
+ * This is to avoid issues with signed arithmetic overflow on 32-bit systems.
+ *
+ * POSIX allows reads that are interrupted after reading some data to either
+ * return -1 (with errno set to EINTR) or return the number of bytes already
+ * read. Linux follows the latter behavior: if data has been read before a
+ * signal arrives, the call returns the bytes read rather than failing.
+ *
+ * context-flags: KAPI_CTX_PROCESS | KAPI_CTX_SLEEPABLE
+ *
+ * param: fd
+ * type: KAPI_TYPE_FD
+ * flags: KAPI_PARAM_IN
+ * constraint-type: KAPI_CONSTRAINT_RANGE
+ * range: 0, INT_MAX
+ * cdesc: Must be a valid, open file descriptor with read permission.
+ * The file must have been opened with O_RDONLY or O_RDWR. Special values
+ * like AT_FDCWD are not valid. File descriptors for directories return
+ * EISDIR. Standard file descriptors 0 (stdin), 1 (stdout), 2 (stderr) are
+ * valid if open and readable.
+ *
+ * param: buf
+ * type: KAPI_TYPE_USER_PTR
+ * flags: KAPI_PARAM_OUT | KAPI_PARAM_USER
+ * constraint-type: KAPI_CONSTRAINT_BUFFER
+ * size-param: 2
+ * cdesc: Must point to a valid, writable user-space memory region of at
+ * least count bytes. The buffer is validated via access_ok() before any
+ * read operation. NULL is invalid and will return EFAULT. The buffer may
+ * be partially written if an error occurs mid-read. For O_DIRECT reads,
+ * the buffer may need to be aligned to the filesystem's block size (varies
+ * by filesystem, check via statx() with STATX_DIOALIGN).
+ *
+ * param: count
+ * type: KAPI_TYPE_UINT
+ * flags: KAPI_PARAM_IN
+ * constraint-type: KAPI_CONSTRAINT_RANGE
+ * range: 0, SIZE_MAX
+ * cdesc: Maximum number of bytes to read. Clamped internally to
+ * MAX_RW_COUNT (INT_MAX & PAGE_MASK, approximately 0x7ffff000 bytes) to
+ * prevent signed overflow issues. A count of 0 returns immediately with 0
+ * without accessing the file (but may still detect errors). Large values
+ * are not errors but will be clamped. Cast to ssize_t must not be negative.
+ *
+ * return:
+ * type: KAPI_TYPE_INT
+ * check-type: KAPI_RETURN_RANGE
+ * success: >= 0
+ * desc: On success, returns the number of bytes read (non-negative). Zero
+ * indicates end-of-file (EOF) for regular files, or no data available
+ * from a device that does not block. The return value may be less than
+ * count if fewer bytes were available (short read). Partial reads are
+ * not errors. On error, returns a negative error code.
+ *
+ * error: EBADF, Bad file descriptor
+ * desc: fd is not a valid file descriptor, or fd was not opened for reading.
+ * This includes file descriptors opened with O_WRONLY, O_PATH, or file
+ * descriptors that have been closed. Also returned if the file structure
+ * does not have FMODE_READ set.
+ *
+ * error: EFAULT, Bad address
+ * desc: buf points outside the accessible address space. The buffer address
+ * failed access_ok() validation. Can also occur if a fault happens during
+ * copy_to_user() when transferring data to user space after the read
+ * completes in kernel space.
+ *
+ * error: EINVAL, Invalid argument
+ * desc: Returned in several cases: (1) The file descriptor refers to an
+ * object that is not suitable for reading (no read or read_iter method).
+ * (2) The file was opened with O_DIRECT and the buffer alignment, offset,
+ * or count does not meet the filesystem's alignment requirements. (3) For
+ * timerfd file descriptors, the buffer is smaller than 8 bytes. (4) The
+ * count argument, when cast to ssize_t, is negative.
+ *
+ * error: EISDIR, Is a directory
+ * desc: fd refers to a directory. Directories cannot be read using read();
+ * use getdents64() instead. This error is returned by the generic_read_dir()
+ * handler installed for directory file operations.
+ *
+ * error: EAGAIN, Resource temporarily unavailable
+ * desc: fd refers to a file (pipe, socket, device) that is marked non-blocking
+ * (O_NONBLOCK) and the read would block. Also returned with IOCB_NOWAIT
+ * when data is not immediately available. Equivalent to EWOULDBLOCK.
+ * The application should retry the read later or use select/poll/epoll.
+ *
+ * error: EINTR, Interrupted system call
+ * desc: The call was interrupted by a signal before any data was read. This
+ * only occurs if no data has been transferred; if some data was read before
+ * the signal, the call returns the number of bytes read. The caller should
+ * typically restart the read.
+ *
+ * error: EIO, Input/output error
+ * desc: A low-level I/O error occurred. For regular files, this typically
+ * indicates a hardware error on the storage device, a filesystem error,
+ * or a network filesystem timeout. For terminals, this may indicate the
+ * controlling terminal has been closed for a background process.
+ *
+ * error: EOVERFLOW, Value too large for defined data type
+ * desc: The file position plus count would exceed LLONG_MAX. Also returned
+ * when reading from certain files (e.g., some /proc files) where the file
+ * position would overflow. For files without FOP_UNSIGNED_OFFSET flag,
+ * negative file positions are not allowed.
+ *
+ * error: ENOBUFS, No buffer space available
+ * desc: Returned when reading from pipe-based watch queues (CONFIG_WATCH_QUEUE)
+ * when the buffer is too small to hold a complete notification, or when
+ * reading packets from pipes with PIPE_BUF_FLAG_WHOLE set.
+ *
+ * error: ERESTARTSYS, Restart system call (internal)
+ * desc: Internal error code indicating the syscall should be restarted. This
+ * is typically translated to EINTR if SA_RESTART is not set on the signal
+ * handler, or the syscall is transparently restarted if SA_RESTART is set.
+ * User space should not see this error code directly.
+ *
+ * error: EACCES, Permission denied
+ * desc: The security subsystem (LSM such as SELinux or AppArmor) denied
+ * the read operation via security_file_permission(). This can occur even
+ * if the file was successfully opened, as LSM policies may enforce per-
+ * operation checks.
+ *
+ * error: EPERM, Operation not permitted
+ * desc: Returned by fanotify permission events (CONFIG_FANOTIFY_ACCESS_PERMISSIONS)
+ * when a user-space fanotify listener denies the read operation via
+ * fsnotify_file_area_perm().
+ *
+ * error: ENODATA, No data available
+ * desc: Returned when reading from files backed by fscache/cachefiles
+ * and the requested data range is not available in the cache
+ * (e.g., beyond EOF or in an uncached region). Also returned by
+ * some filesystem-specific read handlers (e.g., xattr reads).
+ *
+ * error: EOPNOTSUPP, Operation not supported
+ * desc: Returned when the file descriptor does not support the read
+ * operation, such as reading from certain special files or when the
+ * filesystem does not implement read for this file type.
+ *
+ * lock: file->f_pos_lock
+ * type: KAPI_LOCK_MUTEX
+ * acquired: conditional
+ * released: true
+ * desc: For regular files that require atomic position updates (FMODE_ATOMIC_POS),
+ * the f_pos_lock mutex is acquired by fdget_pos() at syscall entry and released
+ * by fdput_pos() at syscall exit. This serializes concurrent reads that share
+ * the same file description. Not acquired for files opened with FMODE_STREAM
+ * (pipes, sockets) or when the file is not shared.
+ *
+ * lock: Filesystem-specific locks
+ * type: KAPI_LOCK_CUSTOM
+ * acquired: conditional
+ * released: true
+ * desc: The filesystem's read_iter or read method may acquire additional locks.
+ * For regular files, this typically includes the inode's i_rwsem for certain
+ * operations. For pipes, the pipe->mutex is acquired. For sockets, socket
+ * lock is acquired. These are internal to the file operation and released
+ * before return.
+ *
+ * lock: RCU read-side
+ * type: KAPI_LOCK_RCU
+ * acquired: conditional
+ * released: true
+ * desc: Used during file descriptor lookup via fdget(). RCU read lock protects
+ * access to the file descriptor table. Released by fdput() at syscall exit.
+ *
+ * signal: Any signal
+ * direction: KAPI_SIGNAL_RECEIVE
+ * action: KAPI_SIGNAL_ACTION_RETURN
+ * condition: When blocked waiting for data on interruptible operations
+ * desc: The syscall may be interrupted by signals while waiting for data to
+ * become available (pipes, sockets, terminals) or waiting for locks. If
+ * interrupted before any data is read, returns -EINTR or -ERESTARTSYS.
+ * If data has already been read, returns the number of bytes read.
+ * error: -EINTR
+ * timing: KAPI_SIGNAL_TIME_DURING
+ * restartable: yes
+ *
+ * side-effect: KAPI_EFFECT_FILE_POSITION
+ * target: file->f_pos
+ * condition: For seekable files when read succeeds (returns > 0)
+ * desc: The file offset (f_pos) is advanced by the number of bytes read.
+ * For stream files (FMODE_STREAM such as pipes and sockets), the offset
+ * is not used or modified. The offset update is protected by f_pos_lock
+ * when the file is shared between threads/processes.
+ * reversible: no
+ *
+ * side-effect: KAPI_EFFECT_MODIFY_STATE
+ * target: inode access time (atime)
+ * condition: When read succeeds and O_NOATIME is not set
+ * desc: Updates the file's access time (atime) via touch_atime(). The update
+ * may be suppressed by mount options (noatime, relatime), the O_NOATIME
+ * flag, or if the filesystem does not support atime. Relatime only updates
+ * atime if it is older than mtime or ctime, or more than a day old.
+ * reversible: no
+ *
+ * side-effect: KAPI_EFFECT_MODIFY_STATE
+ * target: task I/O accounting
+ * condition: Always
+ * desc: Updates the current task's I/O accounting statistics. The rchar field
+ * (read characters) is incremented by bytes read via add_rchar() only on
+ * successful reads (ret > 0). The syscr field (syscall read count) is
+ * incremented unconditionally via inc_syscr(). These statistics are visible
+ * in /proc/[pid]/io.
+ * reversible: no
+ *
+ * side-effect: KAPI_EFFECT_MODIFY_STATE
+ * target: fsnotify events
+ * condition: When read returns > 0
+ * desc: Generates an FS_ACCESS fsnotify event via fsnotify_access() allowing
+ * inotify, fanotify, and dnotify watchers to be notified of the read. This
+ * occurs after data transfer completes successfully.
+ * reversible: no
+ *
+ * capability: CAP_DAC_OVERRIDE
+ * type: KAPI_CAP_BYPASS_CHECK
+ * allows: Bypass discretionary access control on read permission
+ * without: Standard DAC checks are enforced
+ * condition: Checked via security_file_permission() during rw_verify_area()
+ *
+ * capability: CAP_DAC_READ_SEARCH
+ * type: KAPI_CAP_BYPASS_CHECK
+ * allows: Bypass read permission checks on regular files
+ * without: Must have read permission on file
+ * condition: Checked by LSM hooks during the read operation
+ *
+ * constraint: MAX_RW_COUNT
+ * desc: The count parameter is silently clamped to MAX_RW_COUNT (INT_MAX &
+ * PAGE_MASK, approximately 2GB minus one page) to prevent integer overflow
+ * in internal calculations. This is transparent to the caller; the syscall
+ * succeeds but reads at most MAX_RW_COUNT bytes.
+ * expr: actual_count = min(count, MAX_RW_COUNT)
+ *
+ * constraint: File must be open for reading
+ * desc: The file descriptor must have been opened with O_RDONLY or O_RDWR.
+ * Files opened with O_WRONLY or O_PATH cannot be read and return EBADF.
+ * The file must have both FMODE_READ and FMODE_CAN_READ flags set.
+ * expr: (file->f_mode & FMODE_READ) && (file->f_mode & FMODE_CAN_READ)
+ *
+ * examples: n = read(fd, buf, sizeof(buf)); // Basic read
+ * n = read(STDIN_FILENO, buf, 1024); // Read from stdin
+ * while ((n = read(fd, buf, 4096)) > 0) { process(buf, n); } // Read loop
+ * if (read(fd, buf, count) == 0) { handle_eof(); } // Check for EOF
+ *
+ * notes: The behavior of read() varies significantly depending on the type of
+ * file descriptor:
+ *
+ * - Regular files: Reads from current position, advances position, returns 0
+ * at EOF. Short reads are rare but possible near EOF or on signal.
+ *
+ * - Pipes and FIFOs: Blocking by default. Returns available data (up to count)
+ * or blocks until data is available. Returns 0 when all writers have closed.
+ * O_NONBLOCK returns EAGAIN when empty instead of blocking.
+ *
+ * - Sockets: Similar to pipes. Specific behavior depends on socket type and
+ * protocol. MSG_* flags can be specified via recv() for more control.
+ *
+ * - Terminals: Line-buffered in canonical mode; read returns when newline is
+ * entered or buffer is full. Raw mode returns immediately when data available.
+ * Special handling for signals (SIGINT on Ctrl+C, etc.).
+ *
+ * - Device special files: Behavior is device-specific. Some devices support
+ * seeking, others do not. Read size may be constrained by device.
+ *
+ * Race condition: Concurrent reads from the same file description (not just
+ * file descriptor) can race on the file position. Linux 3.14+ provides atomic
+ * position updates for regular files via f_pos_lock, but applications should
+ * use pread() for concurrent positioned reads.
+ *
+ * O_DIRECT reads bypass the page cache and typically require aligned buffers
+ * and positions. Alignment requirements are filesystem-specific; use statx()
+ * with STATX_DIOALIGN (Linux 6.1+) to query. Unaligned O_DIRECT reads fail
+ * with EINVAL on most filesystems.
+ *
+ * For splice(2)-like zero-copy reads, consider using splice(), sendfile(),
+ * or copy_file_range() instead of read() + write().
+ */
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
return ksys_read(fd, buf, count);
@@ -1821,3 +2119,8 @@ int generic_atomic_write_valid(struct kiocb *iocb, struct iov_iter *iter)
return 0;
}
EXPORT_SYMBOL_GPL(generic_atomic_write_valid);
+
+/* Include auto-generated API specifications from kerneldoc annotations */
+#if IS_ENABLED(CONFIG_KAPI_SPEC)
+#include "read_write.apispec.h"
+#endif
--
2.51.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