* [PATCH v5 01/11] kernel/api: introduce kernel API specification framework
2026-10-08 8:49 [PATCH v5 00/11] Kernel API Specification Framework Sasha Levin
@ 2026-10-08 8:49 ` Sasha Levin
2026-10-08 8:49 ` [PATCH v5 02/11] kernel/api: enable kerneldoc-based API specifications Sasha Levin
` (9 subsequent siblings)
10 siblings, 0 replies; 21+ messages in thread
From: Sasha Levin @ 2026-10-08 8:49 UTC (permalink / raw)
To: linux-api, linux-kernel
Cc: Sasha Levin, 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, Gabriele Paoloni, Mauro Carvalho Chehab,
Christian Brauner, Alexander Viro, Andrew Morton, Masahiro Yamada,
Shuah Khan, Arnd Bergmann, Nathan Chancellor, Steven Rostedt,
Masami Hiramatsu, Mathieu Desnoyers
Add a framework for formally documenting kernel APIs with inline
specifications. This framework provides:
- Structured API documentation with parameter specifications, return
values, error conditions, and execution context requirements
- Runtime validation capabilities for debugging
(CONFIG_KAPI_RUNTIME_CHECKS)
- Support for both internal kernel APIs and system calls
The framework stores specifications in a dedicated ELF section and
provides infrastructure for:
- Runtime querying of API documentation
- Integration with existing SYSCALL_DEFINE macros
A KUnit suite (CONFIG_KAPI_KUNIT_TEST) covers registration, lookup,
parameter and return value validation, and JSON export.
No specifications are added here; subsequent patches add them for
individual syscalls. With CONFIG_KAPI_SPEC=n the SYSCALL_DEFINEx()
expansion is unchanged. CONFIG_KAPI_RUNTIME_CHECKS is a debug option:
a syscall whose arguments violate its specification returns -EINVAL
without running.
Assisted-by: LLM
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
Documentation/dev-tools/index.rst | 1 +
Documentation/dev-tools/kernel-api-spec.rst | 615 ++++++++
MAINTAINERS | 11 +
arch/x86/include/asm/syscall_wrapper.h | 3 +-
include/asm-generic/vmlinux.lds.h | 14 +
include/linux/kapi_syscall.h | 35 +
include/linux/kernel_api_spec.h | 1226 ++++++++++++++++
include/linux/syscalls.h | 42 +-
init/Kconfig | 2 +
kernel/Makefile | 1 +
kernel/api/Kconfig | 54 +
kernel/api/Makefile | 10 +
kernel/api/internal.h | 25 +
kernel/api/kapi_kunit.c | 720 ++++++++++
kernel/api/kernel_api_spec.c | 1415 +++++++++++++++++++
15 files changed, 4172 insertions(+), 2 deletions(-)
create mode 100644 Documentation/dev-tools/kernel-api-spec.rst
create mode 100644 include/linux/kapi_syscall.h
create mode 100644 include/linux/kernel_api_spec.h
create mode 100644 kernel/api/Kconfig
create mode 100644 kernel/api/Makefile
create mode 100644 kernel/api/internal.h
create mode 100644 kernel/api/kapi_kunit.c
create mode 100644 kernel/api/kernel_api_spec.c
diff --git a/Documentation/dev-tools/index.rst b/Documentation/dev-tools/index.rst
index 59cbb77b33ff4..8d3768645d96c 100644
--- a/Documentation/dev-tools/index.rst
+++ b/Documentation/dev-tools/index.rst
@@ -36,6 +36,7 @@ Documentation/process/debugging/index.rst
kunit/index
ktap
checkuapi
+ kernel-api-spec
gpio-sloppy-logic-analyzer
autofdo
propeller
diff --git a/Documentation/dev-tools/kernel-api-spec.rst b/Documentation/dev-tools/kernel-api-spec.rst
new file mode 100644
index 0000000000000..4fb412b2720d3
--- /dev/null
+++ b/Documentation/dev-tools/kernel-api-spec.rst
@@ -0,0 +1,615 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+======================================
+Kernel API Specification Framework
+======================================
+
+:Author: Sasha Levin <sashal@kernel.org>
+
+.. contents:: Table of Contents
+ :depth: 3
+ :local:
+
+Introduction
+============
+
+The Kernel API Specification Framework (KAPI) describes kernel APIs in a
+machine-readable form. The descriptions are written as kerneldoc annotations
+next to the implementation and compiled into the kernel. They can be used to
+check system call arguments at runtime and can be read back through debugfs or
+the ``kapi`` tool.
+
+Purpose and Goals
+-----------------
+
+The framework aims to:
+
+1. **Improve API Documentation**: Provide structured, inline documentation that
+ lives alongside the code and is maintained as part of the development process.
+
+2. **Enable Runtime Validation**: Optionally validate API usage at runtime to catch
+ common programming errors during development and testing.
+
+3. **Support Tooling**: Export API specifications in machine-readable formats for
+ use by static analyzers, documentation generators, and development tools.
+
+4. **Formalize Contracts**: Explicitly document API contracts including parameter
+ constraints, execution contexts, locking requirements, and side effects.
+
+Architecture Overview
+=====================
+
+Components
+----------
+
+The framework consists of several key components:
+
+1. **Core Framework** (``kernel/api/kernel_api_spec.c``)
+
+ - API specification registration and storage
+ - Runtime validation engine
+ - Specification lookup and querying
+
+2. **DebugFS Interface** (``kernel/api/kapi_debugfs.c``)
+
+ - Runtime introspection via ``/sys/kernel/debug/kapi/``
+ - Per-API detailed specification output
+ - List of all registered API specifications
+
+3. **kapi Tool** (``tools/kapi/``)
+
+ - Userspace utility for extracting specifications
+ - Multiple input sources (source, binary, debugfs)
+ - Multiple output formats (plain, JSON, RST)
+ - Testing and validation utilities
+
+Data Model
+----------
+
+The framework uses a hierarchical data model::
+
+ kernel_api_spec
+ ├── Basic Information
+ │ ├── name (API function name)
+ │ ├── version (specification version)
+ │ └── description (human-readable description)
+ │
+ ├── Parameters (up to 16)
+ │ └── kapi_param_spec
+ │ ├── name
+ │ ├── type (int, pointer, fd, path, etc.)
+ │ ├── flags (in, out, inout, optional, etc.)
+ │ ├── constraints (range, mask, enum values)
+ │ └── description
+ │
+ ├── Return Value
+ │ └── kapi_return_spec
+ │ ├── type
+ │ ├── success conditions
+ │ └── validation rules
+ │
+ ├── Error Conditions (up to 32)
+ │ └── kapi_error_spec
+ │ ├── error code
+ │ ├── name
+ │ ├── condition
+ │ └── description
+ │
+ ├── Execution Context
+ │ ├── allowed contexts (process, interrupt, etc.)
+ │ ├── locking requirements
+ │ └── preemption/interrupt state
+ │
+ └── Side Effects
+ ├── memory allocation
+ ├── state changes
+ └── signal handling
+
+Usage Guide
+===========
+
+Basic API Specification
+-----------------------
+
+API specifications are written as KAPI-annotated kerneldoc comments directly in
+the source file, immediately preceding the function implementation. With
+``CONFIG_KAPI_SPEC`` enabled, Kbuild runs ``kernel-doc -apispec`` on each
+built-in C file that has a ``contexts:`` (or ``context-flags:``) line plus at
+least one of ``api-type:``, ``param:``, ``error:``, ``capability:``,
+``signal:``, ``lock:``, ``state-trans:``, ``constraint:``, ``side-effect:`` or
+``long-desc:``, and compiles the generated header into that file. The ``kapi``
+tool reads the same annotations, or the specifications in a built kernel, for
+use outside the kernel build.
+
+The following is an excerpt of the ``sys_read`` specification in
+``fs/read_write.c``, trimmed for length:
+
+.. code-block:: c
+
+ /**
+ * 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. ...
+ *
+ * contexts: process, sleepable
+ *
+ * param: fd
+ * type: fd, input
+ * constraint-type: range(0, INT_MAX)
+ *
+ * param: buf
+ * type: user_ptr, output
+ * constraint-type: buffer(2)
+ *
+ * param: count
+ * type: uint, input
+ *
+ * return:
+ * type: int
+ * check-type: range
+ * success: >= 0
+ * desc: On success, returns the number of bytes read (non-negative). ...
+ *
+ * error: EBADF, Bad file descriptor
+ * desc: fd is not a valid file descriptor, or fd was not opened for
+ * reading. ...
+ */
+ SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
+
+DSL reference:
+
+* ``contexts:`` — comma-separated list of call contexts. Accepted tokens:
+ ``process``, ``softirq``, ``hardirq``, ``nmi``, ``atomic``, ``sleepable``,
+ ``preempt_disabled``, ``irq_disabled``. ``context-flags:`` with
+ ``|``-joined ``KAPI_CTX_*`` constants is equivalent.
+* ``type:`` — parameter type plus direction/qualifier flags on a single
+ line. Type aliases (case-insensitive): ``int``, ``uint``, ``ptr``,
+ ``fd``, ``path``, ``user_ptr`` (or ``uptr``), ``struct``, ``union``,
+ ``enum``, ``func_ptr``, ``array``, ``custom``. Flag aliases:
+ ``input``, ``output``, ``inout``, ``user``, ``optional``, ``const``,
+ ``volatile``, ``dma``, ``aligned``.
+* ``constraint-type:`` — a ``KAPI_CONSTRAINT_*`` enum token or a
+ function-call expression. ``range(lo, hi)``, ``mask(expr)``,
+ ``enum(v1, v2, …)``, ``buffer(size_param_idx)``, ``alignment(N)``,
+ ``user_string``, ``user_path``, ``user_ptr``, ``power_of_two``,
+ ``page_aligned``, ``nonzero``. ``user_string`` takes its length limits
+ from ``range:``. The function-call
+ form populates the matching aux fields
+ (``range:`` / ``valid-mask:`` / ``size-param:``).
+* ``lock: … type:`` accepts ``mutex``, ``spinlock``, ``rwlock``,
+ ``seqlock``, ``rcu``, ``semaphore``, ``custom`` or ``KAPI_LOCK_*``.
+* ``signal: … direction:`` accepts ``receive``, ``send``, ``handle``,
+ ``block``, ``ignore`` (bitmask, joinable with ``|`` or ``,``).
+* ``signal: … action:`` accepts ``default``, ``terminate``, ``coredump``,
+ ``stop``, ``continue``, ``custom``, ``return``, ``restart``,
+ ``queue``, ``discard``, ``transform``.
+* ``signal: … timing:`` accepts ``before``, ``during``, ``after``.
+* ``capability: … type:`` accepts ``bypass_check``, ``increase_limit``,
+ ``override_restriction``, ``grant_permission``, ``modify_behavior``,
+ ``access_resource``, ``perform_operation``.
+* ``side-effect:`` accepts the snake_case effect names
+ (``alloc_memory``, ``free_memory``, ``modify_state``, ``signal_send``,
+ ``file_position``, ``lock_acquire``, ``lock_release``,
+ ``resource_create``, ``resource_destroy``, ``schedule``, ``hardware``,
+ ``network``, ``filesystem``, ``process_state``, ``irreversible``)
+ joined with ``|`` — for example ``side-effect: resource_create | alloc_memory``.
+* ``return: … type:`` reuses the ``type:`` aliases above.
+* ``return: … check-type:`` accepts ``exact``, ``range``,
+ ``fd``, ``no_return``. ``success:``
+ gives the success value for ``exact`` (an integer, optionally
+ written ``= N``) and the lower bound for ``range`` (``>= N``); the
+ other check types do not use it. The ``type:`` of a return block
+ is also kept as written (for example ``int``) in the
+ human-readable ``type_name`` of ``struct kapi_return_spec``.
+* ``error:`` takes a ``NAME, one-line summary`` header followed
+ by optional indented ``desc:`` / ``condition:`` subfields.
+* ``lock:`` and ``signal:`` take an indented ``desc:`` subfield
+ for the long-form description; ``signal:`` also accepts
+ ``number:`` (the signal constant, for example ``SIGPIPE``),
+ ``errno:``, ``priority:``, ``restartable:``, ``interruptible:``,
+ and ``queue:`` subfields.
+* ``state-trans:`` takes ``from:``, ``to:``, ``object:``,
+ optional ``condition:``, and ``desc:`` subfields. The condition is
+ stored in its own field, apart from the description.
+* ``long-desc:`` is a free-form multi-line prose block that
+ populates ``long_description`` in the spec. ``notes:`` is a
+ free-form block of the same kind. In both, a blank line starts a
+ new paragraph, a line starting with ``- `` stays on its own line so
+ bullet lists survive, and any other wrapped line is joined to the
+ previous one with a space. ``examples:`` keeps one example per
+ line, preserving the relative indentation of nested code. The
+ line breaks are stored as ``\n`` in the generated strings, which
+ have no length limit.
+* A subfield line inside a block starts with one of the subfield
+ names of that block followed by ``:``. Every other line, even one
+ with a colon in the middle of a sentence, continues the previous
+ subfield.
+* ``param-count:`` is optional; the parser counts ``param:`` blocks and
+ warns when an explicit count disagrees.
+
+System Call Specification
+-------------------------
+
+System calls are documented inline in the implementation file (e.g., ``fs/open.c``)
+using KAPI-annotated kerneldoc comments. When ``CONFIG_KAPI_RUNTIME_CHECKS`` is
+enabled, the ``SYSCALL_DEFINEx`` macros automatically look up the specification
+and validate parameters before and after the syscall executes.
+
+Runtime Validation
+==================
+
+Enabling Validation
+-------------------
+
+Runtime validation is controlled by kernel configuration:
+
+1. Enable ``CONFIG_KAPI_SPEC`` to build the framework
+2. Enable ``CONFIG_KAPI_RUNTIME_CHECKS`` for runtime validation
+
+Validation Behavior
+-------------------
+
+When ``CONFIG_KAPI_RUNTIME_CHECKS`` is enabled, every system call that has a
+specification is validated in its ``SYSCALL_DEFINEx()`` wrapper: the arguments
+are checked against the parameter constraints before the handler runs, and the
+return value is checked against the return specification afterwards. Violations
+are reported via ``pr_warn_ratelimited`` to avoid flooding the kernel log. On
+the return side only a successful ``fd`` return that is not a valid file
+descriptor is reported. Any other value that does not satisfy the success check
+is treated as an error and accepted, and error codes that the specification
+does not list are only logged at debug level.
+The execution context recorded in a specification is not checked at runtime.
+The option is available on x86 and on architectures that use the generic
+``__SYSCALL_DEFINEx()``.
+
+Custom Validators
+-----------------
+
+``KAPI_CONSTRAINT_CUSTOM`` calls the ``validate`` function of the parameter
+specification. Kerneldoc annotations cannot set it, so it is only available to a
+``struct kapi_param_spec`` that is filled in by hand:
+
+.. code-block:: c
+
+ static bool validate_buffer_size(s64 value)
+ {
+ size_t size = (size_t)value;
+
+ return size > 0 && size <= MAX_BUFFER_SIZE;
+ }
+
+ /* In the parameter definition: */
+ .constraint_type = KAPI_CONSTRAINT_CUSTOM,
+ .validate = validate_buffer_size,
+
+Performance Considerations
+==========================
+
+Memory Overhead
+---------------
+
+Each compiled spec is 26400 bytes (``readelf -sW vmlinux | grep
+__kapi_spec_``), dominated by the fixed-bound arrays
+``struct_specs[8]`` (11584 bytes), ``signal_masks[32]`` (4864),
+``signals[32]`` (3328) and ``params[16]`` (1920). With
+the five syscall specs in this series, ``.kapi_specs`` and the backing
+``.rodata`` objects occupy ~132 KB. Building with ``CONFIG_KAPI_SPEC=n``
+emits no code or data from the framework.
+
+Runtime Overhead
+----------------
+
+When ``CONFIG_KAPI_RUNTIME_CHECKS`` is enabled, each validated
+call pays for a parameter walk plus the per-constraint check
+(range/mask/enum/align/user-ptr/user-path/user-string).
+The cost depends on the parameter count and the constraints involved;
+profile before enabling on workloads where syscall latency matters.
+``CONFIG_KAPI_RUNTIME_CHECKS=n`` compiles the validators away
+entirely.
+
+The kapi Tool
+=============
+
+Overview
+--------
+
+The ``kapi`` tool is a userspace utility that extracts and displays kernel API
+specifications from multiple sources. It provides a unified interface to access
+API documentation whether from compiled kernels, source code, or runtime systems.
+
+Installation
+------------
+
+Build the tool from the kernel source tree::
+
+ $ cd tools/kapi
+ $ cargo build --release
+
+ # Optional: Install system-wide
+ $ cargo install --path .
+
+The tool requires Rust and Cargo to build. The binary will be available at
+``tools/kapi/target/release/kapi``.
+
+Command-Line Usage
+------------------
+
+Basic syntax::
+
+ kapi [OPTIONS] [API_NAME]
+
+Options:
+
+- ``--vmlinux <PATH>``: Extract from compiled kernel binary
+- ``--source <PATH>``: Extract from kernel source code
+- ``--debugfs <PATH>``: Extract from debugfs (default: /sys/kernel/debug)
+- ``-f, --format <FORMAT>``: Output format (plain, json, rst)
+- ``-h, --help``: Display help information
+- ``-V, --version``: Display version information
+
+Input Modes
+-----------
+
+**1. Source Code Mode**
+
+Extract specifications directly from kernel source::
+
+ # Scan entire kernel source tree
+ $ kapi --source /path/to/linux
+
+ # Extract from specific file
+ $ kapi --source fs/open.c
+
+ # Get details for specific API
+ $ kapi --source /path/to/linux sys_close
+
+**2. Vmlinux Mode**
+
+Extract from compiled kernel with debug symbols::
+
+ # List all APIs in vmlinux
+ $ kapi --vmlinux ./vmlinux
+
+ # Get specific syscall details
+ $ kapi --vmlinux ./vmlinux sys_read
+
+**3. Debugfs Mode**
+
+Extract from running kernel via debugfs::
+
+ # Use default debugfs path
+ $ kapi
+
+ # Use custom debugfs mount
+ $ kapi --debugfs /mnt/debugfs
+
+ # Get specific API from running kernel
+ $ kapi sys_write
+
+Output Formats
+--------------
+
+The samples below are shortened; ``...`` marks omitted output.
+
+**Plain Text Format** (default)::
+
+ $ kapi --source . sys_read
+
+ Detailed information for sys_read:
+ ==================================
+ Description: Read data from a file descriptor
+
+ Detailed Description:
+ Attempts to read up to count bytes from file descriptor fd into the buffer starting at buf. ...
+
+ Execution Context:
+ - KAPI_CTX_PROCESS
+ - KAPI_CTX_SLEEPABLE
+
+ Parameters (3):
+ [0] fd (unsigned int fd)
+ File descriptor to read from
+ Flags: IN
+ ...
+
+**JSON Format**::
+
+ $ kapi --source . --format json sys_read
+ {
+ "api_details": {
+ "name": "sys_read",
+ "description": "Read data from a file descriptor",
+ "long_description": "Attempts to read up to count bytes from file descriptor fd into the buffer starting at buf. ...",
+ "context_flags": [
+ "KAPI_CTX_PROCESS",
+ "KAPI_CTX_SLEEPABLE"
+ ],
+ ...
+ }
+ }
+
+**ReStructuredText Format**::
+
+ $ kapi --source . --format rst sys_read
+
+ sys_read
+ ========
+
+ **Read data from a file descriptor**
+
+ Attempts to read up to count bytes from file descriptor fd into the buffer starting at buf. ...
+
+Usage Examples
+--------------
+
+**Generate complete API documentation**::
+
+ # Export all kernel APIs to JSON
+ $ kapi --source /path/to/linux --format json > kernel-apis.json
+
+ # Generate RST documentation for all syscalls
+ $ kapi --vmlinux ./vmlinux --format rst > syscalls.rst
+
+ # List APIs from specific subsystem
+ $ kapi --source fs/
+
+**Integration with other tools**::
+
+ # List the names of all APIs
+ $ kapi --format json | jq -r '.apis[].name'
+
+ # Generate markdown documentation
+ $ kapi --format rst sys_madvise | pandoc -f rst -t markdown
+
+**Debugging and analysis**::
+
+ # Check if specific API exists
+ $ kapi --source . my_custom_api || echo "API not found"
+
+Implementation Details
+----------------------
+
+The tool extracts API specifications from three sources:
+
+1. **Source Code**: Parses KAPI-annotated kerneldoc comments in C files, using
+ the same selection rule as Kbuild; regular expressions only locate the
+ ``SYSCALL_DEFINEx()`` or function that follows each comment
+2. **Vmlinux**: Reads the ``.kapi_specs`` ELF section from compiled kernels
+3. **Debugfs**: Reads from ``/sys/kernel/debug/kapi/`` filesystem interface
+
+The tool supports all KAPI specification types:
+
+- System calls (kerneldoc annotations)
+- Kernel functions (kerneldoc annotations with KAPI tags)
+
+IDE Integration
+---------------
+
+Modern IDEs can use the specification data for:
+
+- Parameter hints
+- Type checking
+- Context validation
+- Error code documentation
+
+Best Practices
+==============
+
+Writing Specifications
+----------------------
+
+1. **Be Comprehensive**: Document all parameters, errors, and side effects
+2. **Keep Updated**: Update specs when API behavior changes
+3. **Use Examples**: Include usage examples in descriptions
+4. **Validate Constraints**: Define realistic constraints for parameters
+5. **Document Context**: Clearly specify allowed execution contexts
+
+Maintenance
+-----------
+
+1. **Version Specifications**: Increment version when API changes
+2. **Deprecation**: Mark deprecated APIs and suggest replacements
+3. **Cross-reference**: Link related APIs in descriptions
+4. **Test Specifications**: Verify specs match implementation
+
+Common Patterns
+---------------
+
+**Optional Parameters**:
+
+.. code-block:: c
+
+ /**
+ * @optional_arg: Optional argument (may be NULL)
+ *
+ * param: optional_arg
+ * type: KAPI_TYPE_PTR
+ * flags: KAPI_PARAM_IN | KAPI_PARAM_OPTIONAL
+ */
+
+**Buffer with Size Parameter**:
+
+.. code-block:: c
+
+ /**
+ * @buf: User-space buffer
+ *
+ * param: buf
+ * type: KAPI_TYPE_USER_PTR
+ * flags: KAPI_PARAM_OUT | KAPI_PARAM_USER
+ * constraint-type: KAPI_CONSTRAINT_BUFFER
+ * size-param: 2
+ */
+
+**Callback Functions**:
+
+.. code-block:: c
+
+ /**
+ * @callback: Callback function
+ *
+ * param: callback
+ * type: KAPI_TYPE_FUNC_PTR
+ * flags: KAPI_PARAM_IN
+ */
+
+Troubleshooting
+===============
+
+Common Issues
+-------------
+
+**Specification Not Found**
+
+A syscall that is missing from ``/sys/kernel/debug/kapi/list`` has no
+specification. Ensure the KAPI-annotated kerneldoc comment is in the same
+translation unit as the function implementation, is named ``sys_<name>`` for
+``SYSCALL_DEFINEx(<name>, ...)``, and has a ``contexts:`` line plus one more
+KAPI section as described above.
+
+**Validation Failures**::
+
+ kapi: Parameter fd: invalid file descriptor -1
+
+ Solution: Check parameter constraints or adjust specification if
+ the constraint is incorrect.
+
+Debug Options
+-------------
+
+Violations are logged with ``pr_warn_ratelimited()``. Error codes that a
+specification does not list are logged with ``pr_debug()``; with
+``CONFIG_DYNAMIC_DEBUG`` they can be enabled with::
+
+ echo 'file kernel_api_spec.c +p' > /sys/kernel/debug/dynamic_debug/control
+
+Contributing
+============
+
+Submitting Specifications
+-------------------------
+
+1. Add specifications to the same file as the API implementation
+2. Follow existing patterns and naming conventions
+3. Test with CONFIG_KAPI_RUNTIME_CHECKS enabled
+4. Run scripts/checkpatch.pl on your changes
+
+Review Criteria
+---------------
+
+Specifications will be reviewed for:
+
+1. **Completeness**: All parameters and errors documented
+2. **Accuracy**: Specification matches implementation
+3. **Clarity**: Descriptions are clear and helpful
+4. **Consistency**: Follows framework conventions
+5. **Performance**: No unnecessary runtime overhead
+
+Contact
+-------
+
+- Maintainer: Sasha Levin <sashal@kernel.org>
diff --git a/MAINTAINERS b/MAINTAINERS
index 65e8a4b5c90b1..cd15c6a776ae3 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14118,6 +14118,17 @@ W: https://linuxtv.org
T: git git://linuxtv.org/media.git
F: drivers/media/radio/radio-keene*
+KERNEL API SPECIFICATION FRAMEWORK (KAPI)
+M: Sasha Levin <sashal@kernel.org>
+L: linux-api@vger.kernel.org
+S: Maintained
+F: Documentation/dev-tools/kernel-api-spec.rst
+F: include/linux/kapi_syscall.h
+F: include/linux/kernel_api_spec.h
+F: kernel/api/
+F: tools/kapi/
+F: tools/lib/python/kdoc/kdoc_apispec.py
+
KERNEL AUTOMOUNTER
M: Ian Kent <raven@themaw.net>
L: autofs@vger.kernel.org
diff --git a/arch/x86/include/asm/syscall_wrapper.h b/arch/x86/include/asm/syscall_wrapper.h
index 7e88705e907f4..54b70c2cac8ac 100644
--- a/arch/x86/include/asm/syscall_wrapper.h
+++ b/arch/x86/include/asm/syscall_wrapper.h
@@ -223,11 +223,12 @@ extern long __ia32_sys_ni_syscall(const struct pt_regs *regs);
#define __SYSCALL_DEFINEx(x, name, ...) \
static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
+ __KAPI_SYSCALL_DEFINEx(x, name, __VA_ARGS__) \
__X64_SYS_STUBx(x, name, __VA_ARGS__) \
__IA32_SYS_STUBx(x, name, __VA_ARGS__) \
static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
- long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
+ long ret = __KAPI_DO_SYS(name)(__MAP(x,__SC_CAST,__VA_ARGS__));\
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return ret; \
diff --git a/include/asm-generic/vmlinux.lds.h b/include/asm-generic/vmlinux.lds.h
index b2988aa12f664..b754fa182a6a3 100644
--- a/include/asm-generic/vmlinux.lds.h
+++ b/include/asm-generic/vmlinux.lds.h
@@ -296,6 +296,19 @@
#define TRACE_SYSCALLS()
#endif
+#ifdef CONFIG_KAPI_SPEC
+/*
+ * .kapi_specs is an array of pointers (see DEFINE_KERNEL_API_SPEC()). Align
+ * __start_kapi_specs to at least pointer alignment so that no padding
+ * separates it from the first entry.
+ */
+#define KAPI_SPECS() \
+ . = ALIGN(8); \
+ BOUNDED_SECTION_BY(.kapi_specs, _kapi_specs)
+#else
+#define KAPI_SPECS()
+#endif
+
#ifdef CONFIG_BPF_EVENTS
#define BPF_RAW_TP() STRUCT_ALIGN(); \
BOUNDED_SECTION_BY(__bpf_raw_tp_map, __bpf_raw_tp)
@@ -485,6 +498,7 @@
. = ALIGN(8); \
BOUNDED_SECTION_BY(__tracepoints_ptrs, ___tracepoints_ptrs) \
*(__tracepoints_strings)/* Tracepoints: strings */ \
+ KAPI_SPECS() \
} \
\
.rodata1 : AT(ADDR(.rodata1) - LOAD_OFFSET) { \
diff --git a/include/linux/kapi_syscall.h b/include/linux/kapi_syscall.h
new file mode 100644
index 0000000000000..bc9387e0e052c
--- /dev/null
+++ b/include/linux/kapi_syscall.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Spec lookup and syscall validation entry points of the kernel API
+ * specification framework, kept apart from <linux/kernel_api_spec.h> so that
+ * <linux/syscalls.h> can declare them cheaply.
+ */
+
+#ifndef _LINUX_KAPI_SYSCALL_H
+#define _LINUX_KAPI_SYSCALL_H
+
+#include <linux/types.h>
+
+struct kernel_api_spec;
+
+const struct kernel_api_spec *kapi_get_spec(const char *name);
+
+#ifdef CONFIG_KAPI_RUNTIME_CHECKS
+int kapi_validate_syscall_params(const struct kernel_api_spec *spec,
+ const s64 *params, int param_count);
+int kapi_validate_syscall_return(const struct kernel_api_spec *spec, s64 retval);
+#else
+static inline int kapi_validate_syscall_params(const struct kernel_api_spec *spec,
+ const s64 *params, int param_count)
+{
+ return 0;
+}
+static inline int kapi_validate_syscall_return(const struct kernel_api_spec *spec, s64 retval)
+{
+ return 0;
+}
+#endif
+
+#endif /* _LINUX_KAPI_SYSCALL_H */
diff --git a/include/linux/kernel_api_spec.h b/include/linux/kernel_api_spec.h
new file mode 100644
index 0000000000000..f72ef5cd73b38
--- /dev/null
+++ b/include/linux/kernel_api_spec.h
@@ -0,0 +1,1226 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * kernel_api_spec.h - Kernel API Specification Framework
+ *
+ * Structures and macros for specifying kernel APIs in a human and
+ * machine-readable form: parameters, return values, error conditions,
+ * and constraints.
+ */
+
+#ifndef _LINUX_KERNEL_API_SPEC_H
+#define _LINUX_KERNEL_API_SPEC_H
+
+#include <linux/array_size.h>
+#include <linux/bits.h>
+#include <linux/compiler.h>
+#include <linux/errno.h>
+#include <linux/kapi_syscall.h>
+#include <linux/kernel.h>
+#include <linux/stringify.h>
+#include <linux/types.h>
+
+struct sigaction;
+
+#define KAPI_MAX_PARAMS 16
+#define KAPI_MAX_ERRORS 32
+#define KAPI_MAX_CONSTRAINTS 32
+#define KAPI_MAX_LOCKS 16
+#define KAPI_MAX_SIGNALS 32
+#define KAPI_MAX_NAME_LEN 128
+#define KAPI_MAX_DESC_LEN 512
+#define KAPI_MAX_CAPABILITIES 8
+
+/* Magic numbers for section validation (ASCII mnemonics) */
+#define KAPI_MAGIC_PARAMS 0x4B415031 /* 'KAP1' */
+#define KAPI_MAGIC_RETURN 0x4B415232 /* 'KAR2' */
+#define KAPI_MAGIC_ERRORS 0x4B414533 /* 'KAE3' */
+#define KAPI_MAGIC_LOCKS 0x4B414C34 /* 'KAL4' */
+#define KAPI_MAGIC_CONSTRAINTS 0x4B414335 /* 'KAC5' */
+#define KAPI_MAGIC_INFO 0x4B414936 /* 'KAI6' */
+#define KAPI_MAGIC_SIGNALS 0x4B415337 /* 'KAS7' */
+#define KAPI_MAGIC_SIGMASK 0x4B414D38 /* 'KAM8' */
+#define KAPI_MAGIC_STRUCTS 0x4B415439 /* 'KAT9' */
+#define KAPI_MAGIC_EFFECTS 0x4B414641 /* 'KAFA' */
+#define KAPI_MAGIC_TRANS 0x4B415442 /* 'KATB' */
+#define KAPI_MAGIC_CAPS 0x4B414343 /* 'KACC' */
+
+/**
+ * enum kapi_param_type - Parameter type classification
+ * @KAPI_TYPE_VOID: void type
+ * @KAPI_TYPE_INT: Integer types (int, long, etc.)
+ * @KAPI_TYPE_UINT: Unsigned integer types
+ * @KAPI_TYPE_PTR: Pointer types
+ * @KAPI_TYPE_STRUCT: Structure types
+ * @KAPI_TYPE_UNION: Union types
+ * @KAPI_TYPE_ENUM: Enumeration types
+ * @KAPI_TYPE_FUNC_PTR: Function pointer types
+ * @KAPI_TYPE_ARRAY: Array types
+ * @KAPI_TYPE_FD: File descriptor - range-checked only
+ * @KAPI_TYPE_USER_PTR: User space pointer - validated for access and size
+ * @KAPI_TYPE_PATH: Pathname - validated for access and path limits
+ * @KAPI_TYPE_CUSTOM: Custom/complex types
+ */
+enum kapi_param_type {
+ KAPI_TYPE_VOID = 0,
+ KAPI_TYPE_INT,
+ KAPI_TYPE_UINT,
+ KAPI_TYPE_PTR,
+ KAPI_TYPE_STRUCT,
+ KAPI_TYPE_UNION,
+ KAPI_TYPE_ENUM,
+ KAPI_TYPE_FUNC_PTR,
+ KAPI_TYPE_ARRAY,
+ KAPI_TYPE_FD, /* File descriptor - range-checked only */
+ KAPI_TYPE_USER_PTR, /* User space pointer - validated for access and size */
+ KAPI_TYPE_PATH, /* Pathname - validated for access and path limits */
+ KAPI_TYPE_CUSTOM,
+};
+
+/**
+ * enum kapi_param_flags - Parameter attribute flags
+ * @KAPI_PARAM_IN: Input parameter
+ * @KAPI_PARAM_OUT: Output parameter
+ * @KAPI_PARAM_INOUT: Input/output parameter
+ * @KAPI_PARAM_OPTIONAL: Optional parameter (can be NULL)
+ * @KAPI_PARAM_CONST: Const qualified parameter
+ * @KAPI_PARAM_VOLATILE: Volatile qualified parameter
+ * @KAPI_PARAM_USER: User space pointer
+ * @KAPI_PARAM_DMA: DMA-capable memory required
+ * @KAPI_PARAM_ALIGNED: Alignment requirements
+ */
+enum kapi_param_flags {
+ KAPI_PARAM_IN = (1 << 0),
+ KAPI_PARAM_OUT = (1 << 1),
+ KAPI_PARAM_INOUT = (KAPI_PARAM_IN | KAPI_PARAM_OUT),
+ KAPI_PARAM_OPTIONAL = (1 << 3),
+ KAPI_PARAM_CONST = (1 << 4),
+ KAPI_PARAM_VOLATILE = (1 << 5),
+ KAPI_PARAM_USER = (1 << 6),
+ KAPI_PARAM_DMA = (1 << 7),
+ KAPI_PARAM_ALIGNED = (1 << 8),
+};
+
+/**
+ * enum kapi_context_flags - Function execution context flags
+ * @KAPI_CTX_PROCESS: Can be called from process context
+ * @KAPI_CTX_SOFTIRQ: Can be called from softirq context
+ * @KAPI_CTX_HARDIRQ: Can be called from hardirq context
+ * @KAPI_CTX_NMI: Can be called from NMI context
+ * @KAPI_CTX_ATOMIC: Must be called in atomic context
+ * @KAPI_CTX_SLEEPABLE: May sleep
+ * @KAPI_CTX_PREEMPT_DISABLED: Requires preemption disabled
+ * @KAPI_CTX_IRQ_DISABLED: Requires interrupts disabled
+ */
+enum kapi_context_flags {
+ KAPI_CTX_PROCESS = (1 << 0),
+ KAPI_CTX_SOFTIRQ = (1 << 1),
+ KAPI_CTX_HARDIRQ = (1 << 2),
+ KAPI_CTX_NMI = (1 << 3),
+ KAPI_CTX_ATOMIC = (1 << 4),
+ KAPI_CTX_SLEEPABLE = (1 << 5),
+ KAPI_CTX_PREEMPT_DISABLED = (1 << 6),
+ KAPI_CTX_IRQ_DISABLED = (1 << 7),
+};
+
+/**
+ * enum kapi_lock_type - Lock types used/required by the function
+ * @KAPI_LOCK_NONE: No locking requirements
+ * @KAPI_LOCK_MUTEX: Mutex lock
+ * @KAPI_LOCK_SPINLOCK: Spinlock
+ * @KAPI_LOCK_RWLOCK: Read-write lock
+ * @KAPI_LOCK_SEQLOCK: Sequence lock
+ * @KAPI_LOCK_RCU: RCU lock
+ * @KAPI_LOCK_SEMAPHORE: Semaphore
+ * @KAPI_LOCK_CUSTOM: Custom locking mechanism
+ */
+enum kapi_lock_type {
+ KAPI_LOCK_NONE = 0,
+ KAPI_LOCK_MUTEX,
+ KAPI_LOCK_SPINLOCK,
+ KAPI_LOCK_RWLOCK,
+ KAPI_LOCK_SEQLOCK,
+ KAPI_LOCK_RCU,
+ KAPI_LOCK_SEMAPHORE,
+ KAPI_LOCK_CUSTOM,
+};
+
+/**
+ * enum kapi_constraint_type - Types of parameter constraints
+ * @KAPI_CONSTRAINT_NONE: No constraint
+ * @KAPI_CONSTRAINT_RANGE: Numeric range constraint
+ * @KAPI_CONSTRAINT_MASK: Bitmask constraint
+ * @KAPI_CONSTRAINT_ENUM: Enumerated values constraint
+ * @KAPI_CONSTRAINT_ALIGNMENT: Alignment constraint (must be aligned to specified boundary)
+ * @KAPI_CONSTRAINT_POWER_OF_TWO: Value must be a power of two
+ * @KAPI_CONSTRAINT_PAGE_ALIGNED: Value must be page-aligned
+ * @KAPI_CONSTRAINT_NONZERO: Value must be non-zero
+ * @KAPI_CONSTRAINT_USER_STRING: Userspace null-terminated string with length range
+ * @KAPI_CONSTRAINT_USER_PATH: Userspace pathname string (validated for accessibility and PATH_MAX)
+ * @KAPI_CONSTRAINT_USER_PTR: Userspace pointer (validated for accessibility and size)
+ * @KAPI_CONSTRAINT_BUFFER: Userspace buffer pointer (validated by copy_to/from_user)
+ * @KAPI_CONSTRAINT_CUSTOM: Custom validation function
+ */
+enum kapi_constraint_type {
+ KAPI_CONSTRAINT_NONE = 0,
+ KAPI_CONSTRAINT_RANGE,
+ KAPI_CONSTRAINT_MASK,
+ KAPI_CONSTRAINT_ENUM,
+ KAPI_CONSTRAINT_ALIGNMENT,
+ KAPI_CONSTRAINT_POWER_OF_TWO,
+ KAPI_CONSTRAINT_PAGE_ALIGNED,
+ KAPI_CONSTRAINT_NONZERO,
+ KAPI_CONSTRAINT_USER_STRING,
+ KAPI_CONSTRAINT_USER_PATH,
+ KAPI_CONSTRAINT_USER_PTR,
+ KAPI_CONSTRAINT_BUFFER,
+ KAPI_CONSTRAINT_CUSTOM,
+};
+
+/**
+ * struct kapi_param_spec - Parameter specification
+ * @name: Parameter name
+ * @type_name: Type name as string
+ * @type: Parameter type classification
+ * @flags: Parameter attribute flags
+ * @size: Size in bytes (for arrays/buffers)
+ * @alignment: Required alignment
+ * @min_value: Minimum valid value (for numeric types)
+ * @max_value: Maximum valid value (for numeric types)
+ * @valid_mask: Valid bits mask (for flag parameters)
+ * @enum_values: Array of valid enumerated values
+ * @enum_count: Number of valid enumerated values
+ * @constraint_type: Type of constraint applied
+ * @validate: Custom validation function
+ * @description: Human-readable description
+ * @constraints: Additional constraints description
+ * @size_param_idx: 1-based index of the parameter that determines size,
+ * or 0 if this parameter has a fixed size
+ * @size_multiplier: Multiplier for size calculation (e.g., sizeof(struct))
+ */
+struct kapi_param_spec {
+ const char *name;
+ const char *type_name;
+ enum kapi_param_type type;
+ u32 flags;
+ size_t size;
+ size_t alignment;
+ s64 min_value;
+ s64 max_value;
+ u64 valid_mask;
+ const s64 *enum_values;
+ u32 enum_count;
+ enum kapi_constraint_type constraint_type;
+ bool (*validate)(s64 value);
+ const char *description;
+ const char *constraints;
+ int size_param_idx; /* 1-based param index for dynamic size; 0 if N/A */
+ size_t size_multiplier; /* Size per unit (e.g., sizeof(struct epoll_event)) */
+};
+
+/**
+ * struct kapi_error_spec - Error condition specification
+ * @error_code: Error code value
+ * @name: Error code name (e.g., "EINVAL")
+ * @condition: Condition that triggers this error
+ * @description: Detailed error description
+ */
+struct kapi_error_spec {
+ int error_code;
+ const char *name;
+ const char *condition;
+ const char *description;
+};
+
+/**
+ * enum kapi_return_check_type - Return value check types
+ * @KAPI_RETURN_EXACT: Success is an exact value
+ * @KAPI_RETURN_RANGE: Success is within a range
+ * @KAPI_RETURN_ERROR_CHECK: Success is when NOT in error list
+ * @KAPI_RETURN_FD: Return value is a file descriptor (>= 0 is success)
+ * @KAPI_RETURN_CUSTOM: Custom validation function
+ * @KAPI_RETURN_NO_RETURN: Function does not return (e.g., exec on success)
+ */
+enum kapi_return_check_type {
+ KAPI_RETURN_EXACT,
+ KAPI_RETURN_RANGE,
+ KAPI_RETURN_ERROR_CHECK,
+ KAPI_RETURN_FD,
+ KAPI_RETURN_CUSTOM,
+ KAPI_RETURN_NO_RETURN,
+};
+
+/**
+ * struct kapi_return_spec - Return value specification
+ * @type_name: Return type name
+ * @type: Return type classification
+ * @check_type: Type of success check to perform
+ * @success_value: Exact value indicating success (for EXACT)
+ * @success_min: Minimum success value (for RANGE)
+ * @success_max: Maximum success value (for RANGE)
+ * @error_values: Array of error values (for ERROR_CHECK)
+ * @error_count: Number of error values
+ * @is_success: Custom function to check success
+ * @description: Return value description
+ */
+struct kapi_return_spec {
+ const char *type_name;
+ enum kapi_param_type type;
+ enum kapi_return_check_type check_type;
+ s64 success_value;
+ s64 success_min;
+ s64 success_max;
+ const s64 *error_values;
+ u32 error_count;
+ bool (*is_success)(s64 retval);
+ const char *description;
+};
+
+/**
+ * enum kapi_lock_scope - Lock acquisition/release scope
+ * @KAPI_LOCK_INTERNAL: Lock is acquired and released within the function (common case)
+ * @KAPI_LOCK_ACQUIRES: Function acquires lock but does not release it
+ * @KAPI_LOCK_RELEASES: Function releases lock (must be held on entry)
+ * @KAPI_LOCK_CALLER_HELD: Lock must be held by caller throughout the call
+ */
+enum kapi_lock_scope {
+ KAPI_LOCK_INTERNAL = 0,
+ KAPI_LOCK_ACQUIRES,
+ KAPI_LOCK_RELEASES,
+ KAPI_LOCK_CALLER_HELD,
+};
+
+/**
+ * struct kapi_lock_spec - Lock requirement specification
+ * @lock_name: Name of the lock
+ * @lock_type: Type of lock
+ * @scope: Lock scope (internal, acquires, releases, or caller-held)
+ * @description: Additional lock requirements
+ */
+struct kapi_lock_spec {
+ const char *lock_name;
+ enum kapi_lock_type lock_type;
+ enum kapi_lock_scope scope;
+ const char *description;
+};
+
+/**
+ * struct kapi_constraint_spec - Additional constraint specification
+ * @name: Constraint name
+ * @description: Constraint description
+ * @expression: Formal expression (if applicable)
+ */
+struct kapi_constraint_spec {
+ const char *name;
+ const char *description;
+ const char *expression;
+};
+
+/**
+ * enum kapi_signal_direction - Signal flow direction
+ * @KAPI_SIGNAL_RECEIVE: Function may receive this signal
+ * @KAPI_SIGNAL_SEND: Function may send this signal
+ * @KAPI_SIGNAL_HANDLE: Function handles this signal specially
+ * @KAPI_SIGNAL_BLOCK: Function blocks this signal
+ * @KAPI_SIGNAL_IGNORE: Function ignores this signal
+ */
+enum kapi_signal_direction {
+ KAPI_SIGNAL_RECEIVE = (1 << 0),
+ KAPI_SIGNAL_SEND = (1 << 1),
+ KAPI_SIGNAL_HANDLE = (1 << 2),
+ KAPI_SIGNAL_BLOCK = (1 << 3),
+ KAPI_SIGNAL_IGNORE = (1 << 4),
+};
+
+/**
+ * enum kapi_signal_action - What the function does with the signal
+ * @KAPI_SIGNAL_ACTION_DEFAULT: Default signal action applies
+ * @KAPI_SIGNAL_ACTION_TERMINATE: Causes termination
+ * @KAPI_SIGNAL_ACTION_COREDUMP: Causes termination with core dump
+ * @KAPI_SIGNAL_ACTION_STOP: Stops the process
+ * @KAPI_SIGNAL_ACTION_CONTINUE: Continues a stopped process
+ * @KAPI_SIGNAL_ACTION_CUSTOM: Custom handling described in notes
+ * @KAPI_SIGNAL_ACTION_RETURN: Returns from syscall with EINTR
+ * @KAPI_SIGNAL_ACTION_RESTART: Restarts the syscall
+ * @KAPI_SIGNAL_ACTION_QUEUE: Queues the signal for later delivery
+ * @KAPI_SIGNAL_ACTION_DISCARD: Discards the signal
+ * @KAPI_SIGNAL_ACTION_TRANSFORM: Transforms to another signal
+ */
+enum kapi_signal_action {
+ KAPI_SIGNAL_ACTION_DEFAULT = 0,
+ KAPI_SIGNAL_ACTION_TERMINATE,
+ KAPI_SIGNAL_ACTION_COREDUMP,
+ KAPI_SIGNAL_ACTION_STOP,
+ KAPI_SIGNAL_ACTION_CONTINUE,
+ KAPI_SIGNAL_ACTION_CUSTOM,
+ KAPI_SIGNAL_ACTION_RETURN,
+ KAPI_SIGNAL_ACTION_RESTART,
+ KAPI_SIGNAL_ACTION_QUEUE,
+ KAPI_SIGNAL_ACTION_DISCARD,
+ KAPI_SIGNAL_ACTION_TRANSFORM,
+};
+
+/**
+ * struct kapi_signal_spec - Signal specification
+ * @signal_num: Signal number (e.g., SIGKILL, SIGTERM)
+ * @signal_name: Signal name as string
+ * @direction: Direction flags (OR of kapi_signal_direction)
+ * @action: What happens when signal is received
+ * @target: Description of target process/thread for sent signals
+ * @condition: Condition under which signal is sent/received/handled
+ * @description: Detailed description of signal handling
+ * @restartable: Whether syscall is restartable after this signal
+ * @sa_flags_required: Required signal action flags (SA_*)
+ * @sa_flags_forbidden: Forbidden signal action flags
+ * @error_on_signal: Error code returned when signal occurs (-EINTR, etc)
+ * @transform_to: Signal number to transform to (if action is TRANSFORM)
+ * @timing: When signal can occur ("entry", "during", "exit", "anytime")
+ * @priority: Signal handling priority (lower processed first)
+ * @interruptible: Whether this operation is interruptible by this signal
+ * @queue_behavior: How signal is queued ("realtime", "standard", "coalesce")
+ * @state_required: Required process state for signal to be delivered
+ * @state_forbidden: Forbidden process state for signal delivery
+ */
+struct kapi_signal_spec {
+ int signal_num;
+ const char *signal_name;
+ u32 direction;
+ enum kapi_signal_action action;
+ const char *target;
+ const char *condition;
+ const char *description;
+ bool restartable;
+ u32 sa_flags_required;
+ u32 sa_flags_forbidden;
+ int error_on_signal;
+ int transform_to;
+ const char *timing;
+ u8 priority;
+ bool interruptible;
+ const char *queue_behavior;
+ u32 state_required;
+ u32 state_forbidden;
+};
+
+/**
+ * struct kapi_signal_mask_spec - Signal mask specification
+ * @mask_name: Name of the signal mask
+ * @signals: Array of signal numbers in the mask
+ * @signal_count: Number of signals in the mask
+ * @description: Description of what this mask represents
+ */
+struct kapi_signal_mask_spec {
+ const char *mask_name;
+ int signals[KAPI_MAX_SIGNALS];
+ u32 signal_count;
+ const char *description;
+};
+
+/**
+ * struct kapi_struct_field - Structure field specification
+ * @name: Field name
+ * @type: Field type classification
+ * @type_name: Type name as string
+ * @offset: Offset within structure
+ * @size: Size of field in bytes
+ * @flags: Field attribute flags
+ * @constraint_type: Type of constraint applied
+ * @min_value: Minimum valid value (for numeric types)
+ * @max_value: Maximum valid value (for numeric types)
+ * @valid_mask: Valid bits mask (for flag fields)
+ * @enum_values: Comma-separated list of valid enum values (for enum types)
+ * @description: Field description
+ */
+struct kapi_struct_field {
+ const char *name;
+ enum kapi_param_type type;
+ const char *type_name;
+ size_t offset;
+ size_t size;
+ u32 flags;
+ enum kapi_constraint_type constraint_type;
+ s64 min_value;
+ s64 max_value;
+ u64 valid_mask;
+ const char *enum_values; /* Comma-separated list of valid enum values */
+ const char *description;
+};
+
+/**
+ * struct kapi_struct_spec - Structure type specification
+ * @name: Structure name
+ * @size: Total size of structure
+ * @alignment: Required alignment
+ * @field_count: Number of fields
+ * @fields: Field specifications
+ * @description: Structure description
+ */
+struct kapi_struct_spec {
+ const char *name;
+ size_t size;
+ size_t alignment;
+ u32 field_count;
+ struct kapi_struct_field fields[KAPI_MAX_PARAMS];
+ const char *description;
+};
+
+/**
+ * enum kapi_capability_action - What the capability allows
+ * @KAPI_CAP_BYPASS_CHECK: Bypasses a check entirely
+ * @KAPI_CAP_INCREASE_LIMIT: Increases or removes a limit
+ * @KAPI_CAP_OVERRIDE_RESTRICTION: Overrides a restriction
+ * @KAPI_CAP_GRANT_PERMISSION: Grants permission that would otherwise be denied
+ * @KAPI_CAP_MODIFY_BEHAVIOR: Changes the behavior of the operation
+ * @KAPI_CAP_ACCESS_RESOURCE: Allows access to restricted resources
+ * @KAPI_CAP_PERFORM_OPERATION: Allows performing a privileged operation
+ */
+enum kapi_capability_action {
+ KAPI_CAP_BYPASS_CHECK = 0,
+ KAPI_CAP_INCREASE_LIMIT,
+ KAPI_CAP_OVERRIDE_RESTRICTION,
+ KAPI_CAP_GRANT_PERMISSION,
+ KAPI_CAP_MODIFY_BEHAVIOR,
+ KAPI_CAP_ACCESS_RESOURCE,
+ KAPI_CAP_PERFORM_OPERATION,
+};
+
+/**
+ * struct kapi_capability_spec - Capability requirement specification
+ * @capability: The capability constant (e.g., CAP_IPC_LOCK)
+ * @cap_name: Capability name as string
+ * @action: What the capability allows (kapi_capability_action)
+ * @allows: Description of what the capability allows
+ * @without_cap: What happens without the capability
+ * @check_condition: Condition when capability is checked
+ * @priority: Check priority (lower checked first)
+ * @alternative: Alternative capabilities that can be used
+ * @alternative_count: Number of alternative capabilities
+ */
+struct kapi_capability_spec {
+ int capability;
+ const char *cap_name;
+ enum kapi_capability_action action;
+ const char *allows;
+ const char *without_cap;
+ const char *check_condition;
+ u8 priority;
+ int alternative[KAPI_MAX_CAPABILITIES];
+ u32 alternative_count;
+};
+
+/**
+ * enum kapi_side_effect_type - Types of side effects
+ * @KAPI_EFFECT_NONE: No side effects
+ * @KAPI_EFFECT_ALLOC_MEMORY: Allocates memory
+ * @KAPI_EFFECT_FREE_MEMORY: Frees memory
+ * @KAPI_EFFECT_MODIFY_STATE: Modifies global/shared state
+ * @KAPI_EFFECT_SIGNAL_SEND: Sends signals
+ * @KAPI_EFFECT_FILE_POSITION: Modifies file position
+ * @KAPI_EFFECT_LOCK_ACQUIRE: Acquires locks
+ * @KAPI_EFFECT_LOCK_RELEASE: Releases locks
+ * @KAPI_EFFECT_RESOURCE_CREATE: Creates system resources (FDs, PIDs, etc)
+ * @KAPI_EFFECT_RESOURCE_DESTROY: Destroys system resources
+ * @KAPI_EFFECT_SCHEDULE: May cause scheduling/context switch
+ * @KAPI_EFFECT_HARDWARE: Interacts with hardware
+ * @KAPI_EFFECT_NETWORK: Network I/O operation
+ * @KAPI_EFFECT_FILESYSTEM: Filesystem modification
+ * @KAPI_EFFECT_PROCESS_STATE: Modifies process state
+ * @KAPI_EFFECT_IRREVERSIBLE: Effect cannot be undone
+ */
+enum kapi_side_effect_type {
+ KAPI_EFFECT_NONE = 0,
+ KAPI_EFFECT_ALLOC_MEMORY = (1 << 0),
+ KAPI_EFFECT_FREE_MEMORY = (1 << 1),
+ KAPI_EFFECT_MODIFY_STATE = (1 << 2),
+ KAPI_EFFECT_SIGNAL_SEND = (1 << 3),
+ KAPI_EFFECT_FILE_POSITION = (1 << 4),
+ KAPI_EFFECT_LOCK_ACQUIRE = (1 << 5),
+ KAPI_EFFECT_LOCK_RELEASE = (1 << 6),
+ KAPI_EFFECT_RESOURCE_CREATE = (1 << 7),
+ KAPI_EFFECT_RESOURCE_DESTROY = (1 << 8),
+ KAPI_EFFECT_SCHEDULE = (1 << 9),
+ KAPI_EFFECT_HARDWARE = (1 << 10),
+ KAPI_EFFECT_NETWORK = (1 << 11),
+ KAPI_EFFECT_FILESYSTEM = (1 << 12),
+ KAPI_EFFECT_PROCESS_STATE = (1 << 13),
+ KAPI_EFFECT_IRREVERSIBLE = (1 << 14),
+};
+
+/**
+ * struct kapi_side_effect - Side effect specification
+ * @type: Bitmask of effect types
+ * @target: What is affected (e.g., "process memory", "file descriptor table")
+ * @condition: Condition under which effect occurs
+ * @description: Detailed description of the effect
+ * @reversible: Whether the effect can be undone
+ */
+struct kapi_side_effect {
+ u32 type;
+ const char *target;
+ const char *condition;
+ const char *description;
+ bool reversible;
+};
+
+/**
+ * struct kapi_state_transition - State transition specification
+ * @from_state: Starting state description
+ * @to_state: Ending state description
+ * @condition: Condition for transition
+ * @object: Object whose state changes
+ * @description: Detailed description
+ */
+struct kapi_state_transition {
+ const char *from_state;
+ const char *to_state;
+ const char *condition;
+ const char *object;
+ const char *description;
+};
+
+#define KAPI_MAX_STRUCT_SPECS 8
+#define KAPI_MAX_SIDE_EFFECTS 32
+#define KAPI_MAX_STATE_TRANS 8
+
+/**
+ * struct kernel_api_spec - Complete kernel API specification
+ * @name: Function name
+ * @version: API version
+ * @description: Brief description
+ * @long_description: Detailed description
+ * @context_flags: Execution context flags
+ * @param_count: Number of parameters
+ * @params: Parameter specifications
+ * @return_spec: Return value specification
+ * @error_count: Number of possible errors
+ * @errors: Error specifications
+ * @lock_count: Number of lock specifications
+ * @locks: Lock requirement specifications
+ * @constraint_count: Number of additional constraints
+ * @constraints: Additional constraint specifications
+ * @examples: Usage examples
+ * @notes: Additional notes
+ * @signal_count: Number of signal specifications
+ * @signals: Signal handling specifications
+ * @signal_mask_count: Number of signal mask specifications
+ * @signal_masks: Signal mask specifications
+ * @struct_spec_count: Number of structure specifications
+ * @struct_specs: Structure type specifications
+ * @side_effect_count: Number of side effect specifications
+ * @side_effects: Side effect specifications
+ * @state_trans_count: Number of state transition specifications
+ * @state_transitions: State transition specifications
+ * @capability_count: Number of required capabilities
+ * @capabilities: Required capability specifications
+ * @param_magic: Magic value marking the start of the params array
+ * @return_magic: Magic value marking the return spec
+ * @error_magic: Magic value marking the start of the errors array
+ * @lock_magic: Magic value marking the start of the locks array
+ * @constraint_magic: Magic value marking the constraints array
+ * @info_magic: Magic value marking the info block (examples, notes)
+ * @signal_magic: Magic value marking the start of the signals array
+ * @sigmask_magic: Magic value marking the signal masks array
+ * @struct_magic: Magic value marking the struct specs array
+ * @effect_magic: Magic value marking the side effects array
+ * @trans_magic: Magic value marking the state transitions array
+ * @cap_magic: Magic value marking the capabilities array
+ */
+struct kernel_api_spec {
+ const char *name;
+ u32 version;
+ const char *description;
+ const char *long_description;
+ u32 context_flags;
+
+ /* Parameters */
+ u32 param_magic; /* 0x4B415031 = 'KAP1' */
+ u32 param_count;
+ struct kapi_param_spec params[KAPI_MAX_PARAMS];
+
+ /* Return value */
+ u32 return_magic; /* 0x4B415232 = 'KAR2' */
+ struct kapi_return_spec return_spec;
+
+ /* Errors */
+ u32 error_magic; /* 0x4B414533 = 'KAE3' */
+ u32 error_count;
+ struct kapi_error_spec errors[KAPI_MAX_ERRORS];
+
+ /* Locking */
+ u32 lock_magic; /* 0x4B414C34 = 'KAL4' */
+ u32 lock_count;
+ struct kapi_lock_spec locks[KAPI_MAX_LOCKS];
+
+ /* Constraints */
+ u32 constraint_magic; /* 0x4B414335 = 'KAC5' */
+ u32 constraint_count;
+ struct kapi_constraint_spec constraints[KAPI_MAX_CONSTRAINTS];
+
+ /* Additional information */
+ u32 info_magic; /* 0x4B414936 = 'KAI6' */
+ const char *examples;
+ const char *notes;
+
+ /* Signal specifications */
+ u32 signal_magic; /* 0x4B415337 = 'KAS7' */
+ u32 signal_count;
+ struct kapi_signal_spec signals[KAPI_MAX_SIGNALS];
+
+ /* Signal mask specifications */
+ u32 sigmask_magic; /* 0x4B414D38 = 'KAM8' */
+ u32 signal_mask_count;
+ struct kapi_signal_mask_spec signal_masks[KAPI_MAX_SIGNALS];
+
+ /* Structure specifications */
+ u32 struct_magic; /* 0x4B415439 = 'KAT9' */
+ u32 struct_spec_count;
+ struct kapi_struct_spec struct_specs[KAPI_MAX_STRUCT_SPECS];
+
+ /* Side effects */
+ u32 effect_magic; /* 0x4B414641 = 'KAFA' */
+ u32 side_effect_count;
+ struct kapi_side_effect side_effects[KAPI_MAX_SIDE_EFFECTS];
+
+ /* State transitions */
+ u32 trans_magic; /* 0x4B415442 = 'KATB' */
+ u32 state_trans_count;
+ struct kapi_state_transition state_transitions[KAPI_MAX_STATE_TRANS];
+
+ /* Capability specifications */
+ u32 cap_magic; /* 0x4B414343 = 'KACC' */
+ u32 capability_count;
+ struct kapi_capability_spec capabilities[KAPI_MAX_CAPABILITIES];
+};
+
+/* Macros for defining API specifications */
+
+/**
+ * DEFINE_KERNEL_API_SPEC - Define a kernel API specification
+ * @func_name: Function name to specify
+ *
+ * The ``.kapi_specs`` section holds an array of pointers to
+ * fully-defined ``kernel_api_spec`` instances, tightly packed so
+ * iteration ``for (pp = __start_kapi_specs; pp < __stop_kapi_specs;
+ * pp++)`` advances by one pointer each step regardless of the real
+ * spec struct size.
+ */
+#define DEFINE_KERNEL_API_SPEC(func_name) \
+ extern const struct kernel_api_spec __kapi_spec_##func_name; \
+ static const struct kernel_api_spec * const \
+ __kapi_spec_ptr_##func_name __used __section(".kapi_specs") = \
+ &__kapi_spec_##func_name; \
+ const struct kernel_api_spec __kapi_spec_##func_name = { \
+ .name = __stringify(func_name), \
+ .version = 1,
+
+/**
+ * KAPI_DESCRIPTION - Set API description
+ * @desc: Description string
+ */
+#define KAPI_DESCRIPTION(desc) \
+ .description = desc,
+
+/**
+ * KAPI_LONG_DESC - Set detailed API description
+ * @desc: Detailed description string
+ */
+#define KAPI_LONG_DESC(desc) \
+ .long_description = desc,
+
+/**
+ * KAPI_CONTEXT - Set execution context flags
+ * @flags: Context flags (OR'ed KAPI_CTX_* values)
+ */
+#define KAPI_CONTEXT(flags) \
+ .context_flags = flags,
+
+/**
+ * KAPI_PARAM - Define a parameter specification
+ * @idx: Parameter index (0-based)
+ * @pname: Parameter name
+ * @ptype: Type name string
+ * @pdesc: Parameter description
+ */
+#define KAPI_PARAM(idx, pname, ptype, pdesc) \
+ .params[idx] = { \
+ .name = pname, \
+ .type_name = ptype, \
+ .description = pdesc,
+
+#define KAPI_PARAM_TYPE(ptype) \
+ .type = ptype,
+
+#define KAPI_PARAM_FLAGS(pflags) \
+ .flags = pflags,
+
+#define KAPI_PARAM_SIZE(psize) \
+ .size = psize,
+
+#define KAPI_PARAM_RANGE(pmin, pmax) \
+ .min_value = pmin, \
+ .max_value = pmax,
+
+#define KAPI_PARAM_CONSTRAINT_TYPE(ctype) \
+ .constraint_type = ctype,
+
+#define KAPI_PARAM_CONSTRAINT(desc) \
+ .constraints = desc,
+
+#define KAPI_PARAM_VALID_MASK(mask) \
+ .valid_mask = mask,
+
+/**
+ * KAPI_PARAM_ENUM_VALUES - Set the valid values of an enumerated parameter
+ * @...: Variadic list of valid values
+ */
+#define KAPI_PARAM_ENUM_VALUES(...) \
+ .enum_values = (const s64[]){ __VA_ARGS__ }, \
+ .enum_count = sizeof((const s64[]){ __VA_ARGS__ }) / sizeof(s64),
+
+#define KAPI_PARAM_ALIGNMENT(align) \
+ .alignment = align,
+
+/*
+ * Store the 1-based parameter index so the zero-initialised default
+ * (no dynamic sizing) remains distinguishable from "uses param 0".
+ */
+#define KAPI_PARAM_SIZE_PARAM(idx) \
+ .size_param_idx = (idx) + 1,
+
+/**
+ * KAPI_PARAM_COUNT - Set the number of parameters
+ * @n: Number of parameters
+ */
+#define KAPI_PARAM_COUNT(n) \
+ .param_magic = KAPI_MAGIC_PARAMS, \
+ .param_count = n,
+
+/**
+ * KAPI_RETURN - Define return value specification
+ * @rtype: Return type name
+ * @rdesc: Return value description
+ */
+#define KAPI_RETURN(rtype, rdesc) \
+ .return_magic = KAPI_MAGIC_RETURN, \
+ .return_spec = { \
+ .type_name = rtype, \
+ .description = rdesc,
+
+#define KAPI_RETURN_SUCCESS(val, ...) \
+ .success_value = val,
+
+#define KAPI_RETURN_TYPE(rtype) \
+ .type = rtype,
+
+#define KAPI_RETURN_CHECK_TYPE(ctype) \
+ .check_type = ctype,
+
+#define KAPI_RETURN_ERROR_VALUES(values) \
+ .error_values = values,
+
+#define KAPI_RETURN_ERROR_COUNT(count) \
+ .error_count = count,
+
+#define KAPI_RETURN_SUCCESS_RANGE(min, max) \
+ .success_min = min, \
+ .success_max = max,
+
+/**
+ * KAPI_ERROR - Define an error condition
+ * @idx: Error index
+ * @ecode: Error code value
+ * @ename: Error name
+ * @econd: Error condition
+ * @edesc: Error description
+ */
+#define KAPI_ERROR(idx, ecode, ename, econd, edesc) \
+ .errors[idx] = { \
+ .error_code = ecode, \
+ .name = ename, \
+ .condition = econd, \
+ .description = edesc, \
+ },
+
+/**
+ * KAPI_ERROR_COUNT - Set the number of errors
+ * @n: Number of errors
+ */
+#define KAPI_ERROR_COUNT(n) \
+ .error_magic = KAPI_MAGIC_ERRORS, \
+ .error_count = n,
+
+/**
+ * KAPI_LOCK - Define a lock requirement
+ * @idx: Lock index
+ * @lname: Lock name
+ * @ltype: Lock type
+ */
+#define KAPI_LOCK(idx, lname, ltype) \
+ .locks[idx] = { \
+ .lock_name = lname, \
+ .lock_type = ltype,
+
+#define KAPI_LOCK_ACQUIRED \
+ .scope = KAPI_LOCK_ACQUIRES,
+
+#define KAPI_LOCK_RELEASED \
+ .scope = KAPI_LOCK_RELEASES,
+
+#define KAPI_LOCK_HELD_ENTRY \
+ .scope = KAPI_LOCK_CALLER_HELD,
+
+#define KAPI_LOCK_HELD_EXIT \
+ .scope = KAPI_LOCK_CALLER_HELD,
+
+#define KAPI_LOCK_DESC(ldesc) \
+ .description = ldesc,
+
+/**
+ * KAPI_CONSTRAINT - Define an additional constraint
+ * @idx: Constraint index
+ * @cname: Constraint name
+ * @cdesc: Constraint description
+ */
+#define KAPI_CONSTRAINT(idx, cname, cdesc) \
+ .constraints[idx] = { \
+ .name = cname, \
+ .description = cdesc,
+
+#define KAPI_CONSTRAINT_EXPR(expr) \
+ .expression = expr,
+
+/**
+ * KAPI_EXAMPLES - Set API usage examples
+ * @ex: Examples string
+ */
+#define KAPI_EXAMPLES(ex) \
+ .info_magic = KAPI_MAGIC_INFO, \
+ .examples = ex,
+
+/**
+ * KAPI_NOTES - Set API notes
+ * @n: Notes string
+ */
+#define KAPI_NOTES(n) \
+ .notes = n,
+
+
+/**
+ * KAPI_SIGNAL - Define a signal specification
+ * @idx: Signal index
+ * @signum: Signal number (e.g., SIGKILL)
+ * @signame: Signal name string
+ * @dir: Direction flags
+ * @act: Action taken
+ */
+#define KAPI_SIGNAL(idx, signum, signame, dir, act) \
+ .signals[idx] = { \
+ .signal_num = signum, \
+ .signal_name = signame, \
+ .direction = dir, \
+ .action = act,
+
+#define KAPI_SIGNAL_TARGET(tgt) \
+ .target = tgt,
+
+#define KAPI_SIGNAL_CONDITION(cond) \
+ .condition = cond,
+
+#define KAPI_SIGNAL_DESC(desc) \
+ .description = desc,
+
+#define KAPI_SIGNAL_RESTARTABLE \
+ .restartable = true,
+
+#define KAPI_SIGNAL_SA_FLAGS_REQ(flags) \
+ .sa_flags_required = flags,
+
+#define KAPI_SIGNAL_SA_FLAGS_FORBID(flags) \
+ .sa_flags_forbidden = flags,
+
+#define KAPI_SIGNAL_ERROR(err) \
+ .error_on_signal = err,
+
+#define KAPI_SIGNAL_TRANSFORM(sig) \
+ .transform_to = sig,
+
+#define KAPI_SIGNAL_TIMING(when) \
+ .timing = when,
+
+#define KAPI_SIGNAL_PRIORITY(prio) \
+ .priority = prio,
+
+#define KAPI_SIGNAL_INTERRUPTIBLE \
+ .interruptible = true,
+
+#define KAPI_SIGNAL_QUEUE(behavior) \
+ .queue_behavior = behavior,
+
+#define KAPI_SIGNAL_STATE_REQ(state) \
+ .state_required = state,
+
+#define KAPI_SIGNAL_STATE_FORBID(state) \
+ .state_forbidden = state,
+
+#define KAPI_SIGNAL_COUNT(n) \
+ .signal_magic = KAPI_MAGIC_SIGNALS, \
+ .signal_count = n,
+
+/**
+ * KAPI_SIGNAL_MASK - Define a signal mask specification
+ * @idx: Mask index
+ * @name: Mask name
+ * @desc: Mask description
+ */
+#define KAPI_SIGNAL_MASK(idx, name, desc) \
+ .signal_masks[idx] = { \
+ .mask_name = name, \
+ .description = desc,
+
+/*
+ * KAPI_SIGNAL_MASK_SIGNALS - Specify signals in a signal mask
+ * @...: Variadic list of signal numbers
+ *
+ * Usage:
+ * KAPI_SIGNAL_MASK(0, "blocked", "Signals blocked during operation")
+ * KAPI_SIGNAL_MASK_SIGNALS(SIGINT, SIGTERM, SIGQUIT)
+ * },
+ */
+#define KAPI_SIGNAL_MASK_SIGNALS(...) \
+ .signals = { __VA_ARGS__ }, \
+ .signal_count = sizeof((int[]){ __VA_ARGS__ }) / sizeof(int),
+
+/**
+ * KAPI_SIGNAL_MASK_COUNT - Set the number of signal mask specifications
+ * @n: Number of signal masks
+ */
+#define KAPI_SIGNAL_MASK_COUNT(n) \
+ .sigmask_magic = KAPI_MAGIC_SIGMASK, \
+ .signal_mask_count = n,
+
+/**
+ * KAPI_STRUCT_SPEC - Define a structure specification
+ * @idx: Structure spec index
+ * @sname: Structure name
+ * @sdesc: Structure description
+ */
+#define KAPI_STRUCT_SPEC(idx, sname, sdesc) \
+ .struct_specs[idx] = { \
+ .name = #sname, \
+ .description = sdesc,
+
+#define KAPI_STRUCT_SIZE(ssize, salign) \
+ .size = ssize, \
+ .alignment = salign,
+
+#define KAPI_STRUCT_FIELD_COUNT(n) \
+ .field_count = n,
+
+/**
+ * KAPI_STRUCT_FIELD - Define a structure field
+ * @fidx: Field index
+ * @fname: Field name
+ * @ftype: Field type (KAPI_TYPE_*)
+ * @ftype_name: Type name as string
+ * @fdesc: Field description
+ */
+#define KAPI_STRUCT_FIELD(fidx, fname, ftype, ftype_name, fdesc) \
+ .fields[fidx] = { \
+ .name = fname, \
+ .type = ftype, \
+ .type_name = ftype_name, \
+ .description = fdesc,
+
+#define KAPI_FIELD_OFFSET(foffset) \
+ .offset = foffset,
+
+#define KAPI_FIELD_SIZE(fsize) \
+ .size = fsize,
+
+#define KAPI_FIELD_FLAGS(fflags) \
+ .flags = fflags,
+
+#define KAPI_FIELD_CONSTRAINT_RANGE(min, max) \
+ .constraint_type = KAPI_CONSTRAINT_RANGE, \
+ .min_value = min, \
+ .max_value = max,
+
+#define KAPI_FIELD_CONSTRAINT_MASK(mask) \
+ .constraint_type = KAPI_CONSTRAINT_MASK, \
+ .valid_mask = mask,
+
+#define KAPI_FIELD_CONSTRAINT_ENUM(values) \
+ .constraint_type = KAPI_CONSTRAINT_ENUM, \
+ .enum_values = values,
+
+/* Counter for structure specifications */
+#define KAPI_STRUCT_SPEC_COUNT(n) \
+ .struct_magic = KAPI_MAGIC_STRUCTS, \
+ .struct_spec_count = n,
+
+/* Additional lock-related macros */
+#define KAPI_LOCK_COUNT(n) \
+ .lock_magic = KAPI_MAGIC_LOCKS, \
+ .lock_count = n,
+
+/**
+ * KAPI_SIDE_EFFECT - Define a side effect
+ * @idx: Side effect index
+ * @etype: Effect type bitmask (OR'ed KAPI_EFFECT_* values)
+ * @etarget: What is affected
+ * @edesc: Effect description
+ */
+#define KAPI_SIDE_EFFECT(idx, etype, etarget, edesc) \
+ .side_effects[idx] = { \
+ .type = etype, \
+ .target = etarget, \
+ .description = edesc,
+
+#define KAPI_EFFECT_CONDITION(cond) \
+ .condition = cond,
+
+#define KAPI_EFFECT_REVERSIBLE \
+ .reversible = true,
+
+/**
+ * KAPI_STATE_TRANS - Define a state transition
+ * @idx: State transition index
+ * @obj: Object whose state changes
+ * @from: From state
+ * @to: To state
+ * @desc: Transition description
+ */
+#define KAPI_STATE_TRANS(idx, obj, from, to, desc) \
+ .state_transitions[idx] = { \
+ .object = obj, \
+ .from_state = from, \
+ .to_state = to, \
+ .description = desc,
+
+#define KAPI_STATE_TRANS_COND(cond) \
+ .condition = cond,
+
+/* Counters for side effects and state transitions */
+#define KAPI_SIDE_EFFECT_COUNT(n) \
+ .effect_magic = KAPI_MAGIC_EFFECTS, \
+ .side_effect_count = n,
+
+#define KAPI_STATE_TRANS_COUNT(n) \
+ .trans_magic = KAPI_MAGIC_TRANS, \
+ .state_trans_count = n,
+
+/* Helper macros for common side effect patterns */
+#define KAPI_EFFECTS_MEMORY (KAPI_EFFECT_ALLOC_MEMORY | KAPI_EFFECT_FREE_MEMORY)
+#define KAPI_EFFECTS_LOCKING (KAPI_EFFECT_LOCK_ACQUIRE | KAPI_EFFECT_LOCK_RELEASE)
+#define KAPI_EFFECTS_RESOURCES (KAPI_EFFECT_RESOURCE_CREATE | KAPI_EFFECT_RESOURCE_DESTROY)
+#define KAPI_EFFECTS_IO (KAPI_EFFECT_NETWORK | KAPI_EFFECT_FILESYSTEM)
+
+/* Common signal timing constants */
+#define KAPI_SIGNAL_TIME_ENTRY "entry"
+#define KAPI_SIGNAL_TIME_DURING "during"
+#define KAPI_SIGNAL_TIME_EXIT "exit"
+#define KAPI_SIGNAL_TIME_ANYTIME "anytime"
+#define KAPI_SIGNAL_TIME_BLOCKING "while_blocked"
+#define KAPI_SIGNAL_TIME_SLEEPING "while_sleeping"
+#define KAPI_SIGNAL_TIME_BEFORE "before"
+#define KAPI_SIGNAL_TIME_AFTER "after"
+
+/* Common signal queue behaviors */
+#define KAPI_SIGNAL_QUEUE_STANDARD "standard"
+#define KAPI_SIGNAL_QUEUE_REALTIME "realtime"
+#define KAPI_SIGNAL_QUEUE_COALESCE "coalesce"
+#define KAPI_SIGNAL_QUEUE_REPLACE "replace"
+#define KAPI_SIGNAL_QUEUE_DISCARD "discard"
+
+/* Process state flags for signal delivery */
+#define KAPI_SIGNAL_STATE_RUNNING BIT(0)
+#define KAPI_SIGNAL_STATE_SLEEPING BIT(1)
+#define KAPI_SIGNAL_STATE_STOPPED BIT(2)
+#define KAPI_SIGNAL_STATE_TRACED BIT(3)
+#define KAPI_SIGNAL_STATE_ZOMBIE BIT(4)
+#define KAPI_SIGNAL_STATE_DEAD BIT(5)
+
+/* Capability specification macros */
+
+/**
+ * KAPI_CAPABILITY - Define a capability requirement
+ * @idx: Capability index
+ * @cap: Capability constant (e.g., CAP_IPC_LOCK)
+ * @name: Capability name string
+ * @act: Action type (kapi_capability_action)
+ */
+#define KAPI_CAPABILITY(idx, cap, name, act) \
+ .capabilities[idx] = { \
+ .capability = cap, \
+ .cap_name = name, \
+ .action = act,
+
+#define KAPI_CAP_ALLOWS(desc) \
+ .allows = desc,
+
+#define KAPI_CAP_WITHOUT(desc) \
+ .without_cap = desc,
+
+#define KAPI_CAP_CONDITION(cond) \
+ .check_condition = cond,
+
+#define KAPI_CAP_PRIORITY(prio) \
+ .priority = prio,
+
+/*
+ * KAPI_CAP_ALTERNATIVE - Capabilities that can be used instead
+ * @...: Variadic list of capability numbers
+ */
+#define KAPI_CAP_ALTERNATIVE(...) \
+ .alternative = { __VA_ARGS__ }, \
+ .alternative_count = sizeof((int[]){ __VA_ARGS__ }) / sizeof(int),
+
+/* Counter for capability specifications */
+#define KAPI_CAPABILITY_COUNT(n) \
+ .cap_magic = KAPI_MAGIC_CAPS, \
+ .capability_count = n,
+
+/* Validation and runtime checking */
+
+#ifdef CONFIG_KAPI_RUNTIME_CHECKS
+bool kapi_validate_param(const struct kapi_param_spec *param_spec, s64 value);
+bool kapi_validate_param_with_context(const struct kapi_param_spec *param_spec,
+ s64 value, const s64 *all_params, int param_count);
+bool kapi_check_return_success(const struct kapi_return_spec *return_spec, s64 retval);
+bool kapi_validate_return_value(const struct kernel_api_spec *spec, s64 retval);
+void kapi_check_context(const struct kernel_api_spec *spec);
+#else
+static inline bool kapi_validate_param(const struct kapi_param_spec *param_spec, s64 value)
+{
+ return true;
+}
+static inline bool
+kapi_validate_param_with_context(const struct kapi_param_spec *param_spec,
+ s64 value, const s64 *all_params, int param_count)
+{
+ return true;
+}
+static inline bool kapi_check_return_success(const struct kapi_return_spec *return_spec, s64 retval)
+{
+ return true;
+}
+static inline bool kapi_validate_return_value(const struct kernel_api_spec *spec, s64 retval)
+{
+ return true;
+}
+static inline void kapi_check_context(const struct kernel_api_spec *spec) {}
+#endif
+
+/* Export/query functions */
+int kapi_export_json(const struct kernel_api_spec *spec, char *buf, size_t size);
+
+/* Registration for dynamic APIs */
+int kapi_register_spec(const struct kernel_api_spec *spec);
+void kapi_unregister_spec(const char *name);
+
+#define KAPI_CONSTRAINT_COUNT(n) \
+ .constraint_magic = KAPI_MAGIC_CONSTRAINTS, \
+ .constraint_count = n,
+
+#endif /* _LINUX_KERNEL_API_SPEC_H */
diff --git a/include/linux/syscalls.h b/include/linux/syscalls.h
index 8413b624ad478..b062bac9ea1ed 100644
--- a/include/linux/syscalls.h
+++ b/include/linux/syscalls.h
@@ -94,6 +94,10 @@ struct file_attr;
#include <linux/personality.h>
#include <trace/syscall.h>
+#ifdef CONFIG_KAPI_RUNTIME_CHECKS
+#include <linux/kapi_syscall.h>
+#endif
+
#ifdef CONFIG_ARCH_HAS_SYSCALL_WRAPPER
/*
* It may be useful for an architecture to override the definitions of the
@@ -237,6 +241,41 @@ static inline int is_syscall_trace_event(struct trace_event_call *tp_event)
#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
+#ifdef CONFIG_KAPI_RUNTIME_CHECKS
+/*
+ * Convert integers as (s64)(a) does, and pointers through unsigned long so
+ * they are not sign-extended on 32-bit. (unsigned long)(t)0 is an integer
+ * constant expression only if t is an integer type.
+ */
+#define __SC_CAST_TO_S64(t, a) \
+ ((__force s64)__builtin_choose_expr( \
+ __is_constexpr((__force unsigned long)(t)0), \
+ (a), (__force unsigned long)(a)))
+
+#define __KAPI_DO_SYS(name) __do_kapi_sys##name
+#define __KAPI_SYSCALL_DEFINEx(x, name, ...) \
+ static inline long __do_kapi_sys##name(__MAP(x, __SC_DECL, __VA_ARGS__)) \
+ { \
+ const struct kernel_api_spec *__spec = kapi_get_spec("sys" #name); \
+ long ret; \
+ \
+ if (__spec) { \
+ s64 __params[x] = { __MAP(x, __SC_CAST_TO_S64, __VA_ARGS__) }; \
+ int __ret = kapi_validate_syscall_params(__spec, __params, x); \
+ \
+ if (__ret) \
+ return __ret; \
+ } \
+ ret = __do_sys##name(__MAP(x, __SC_ARGS, __VA_ARGS__)); \
+ if (__spec) \
+ kapi_validate_syscall_return(__spec, (s64)ret); \
+ return ret; \
+ }
+#else
+#define __KAPI_DO_SYS(name) __do_sys##name
+#define __KAPI_SYSCALL_DEFINEx(x, name, ...)
+#endif
+
/*
* The asmlinkage stub is aliased to a function named __se_sys_*() which
* sign-extends 32-bit ints to longs whenever needed. The actual work is
@@ -255,10 +294,11 @@ static inline int is_syscall_trace_event(struct trace_event_call *tp_event)
__attribute__((alias(__stringify(__se_sys##name)))); \
ALLOW_ERROR_INJECTION(sys##name, ERRNO); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
+ __KAPI_SYSCALL_DEFINEx(x, name, __VA_ARGS__) \
asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
- long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
+ long ret = __KAPI_DO_SYS(name)(__MAP(x,__SC_CAST,__VA_ARGS__));\
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return ret; \
diff --git a/init/Kconfig b/init/Kconfig
index ad592fdf29af4..66c23c4147498 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2306,6 +2306,8 @@ source "kernel/Kconfig.kexec"
source "kernel/liveupdate/Kconfig"
+source "kernel/api/Kconfig"
+
endmenu # General setup
source "arch/Kconfig"
diff --git a/kernel/Makefile b/kernel/Makefile
index 1e1a31673577d..7fd43127d5eee 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -59,6 +59,7 @@ obj-y += dma/
obj-y += entry/
obj-y += unwind/
obj-$(CONFIG_MODULES) += module/
+obj-$(CONFIG_KAPI_SPEC) += api/
obj-$(CONFIG_KCMP) += kcmp.o
obj-$(CONFIG_FREEZER) += freezer.o
diff --git a/kernel/api/Kconfig b/kernel/api/Kconfig
new file mode 100644
index 0000000000000..1cd55b252f0d5
--- /dev/null
+++ b/kernel/api/Kconfig
@@ -0,0 +1,54 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Kernel API Specification Framework Configuration
+#
+
+config KAPI_SPEC
+ bool "Kernel API Specification Framework"
+ help
+ This option enables the kernel API specification framework,
+ which provides formal documentation of kernel APIs in both
+ human and machine-readable formats.
+
+ The framework allows developers to document APIs inline with
+ their implementation, including parameter specifications,
+ return values, error conditions, locking requirements, and
+ execution context constraints.
+
+ When enabled, API specifications can be queried at runtime
+ and exported in JSON format through debugfs.
+
+ If unsure, say N.
+
+config KAPI_RUNTIME_CHECKS
+ bool "Runtime API specification checks"
+ depends on KAPI_SPEC
+ depends on DEBUG_KERNEL
+ # The hook is only in the generic and the x86 __SYSCALL_DEFINEx()
+ depends on X86 || !ARCH_HAS_SYSCALL_WRAPPER
+ help
+ Validate the arguments and the return value of system calls that
+ have an API specification, in their SYSCALL_DEFINEx() wrapper.
+ Violations are reported with pr_warn_ratelimited(). This adds
+ overhead to every system call.
+
+ DEBUG-ONLY: Enabling this changes the errno seen by userspace for
+ syscalls that violate their parameter specification. On violation
+ the validator short-circuits the syscall and returns -EINVAL
+ before the real handler runs, masking whatever errno the handler
+ would otherwise have produced. Do not enable on production
+ kernels.
+
+ If unsure, say N.
+
+config KAPI_KUNIT_TEST
+ tristate "KUnit tests for KAPI framework" if !KUNIT_ALL_TESTS
+ depends on KAPI_SPEC
+ depends on KUNIT
+ default KUNIT_ALL_TESTS
+ help
+ KUnit tests for the Kernel API Specification Framework.
+ Tests registration, lookup, validation constraints, and
+ JSON export functionality.
+
+ If unsure, say N.
diff --git a/kernel/api/Makefile b/kernel/api/Makefile
new file mode 100644
index 0000000000000..6e14ca243980c
--- /dev/null
+++ b/kernel/api/Makefile
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# Makefile for the Kernel API Specification Framework
+#
+
+# Core API specification framework
+obj-y += kernel_api_spec.o
+
+# KUnit tests
+obj-$(CONFIG_KAPI_KUNIT_TEST) += kapi_kunit.o
diff --git a/kernel/api/internal.h b/kernel/api/internal.h
new file mode 100644
index 0000000000000..b6112bcc2e8d0
--- /dev/null
+++ b/kernel/api/internal.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Internal declarations shared by the KAPI core and its debugfs
+ * interface. Not part of the public kernel API.
+ */
+
+#ifndef _KERNEL_API_INTERNAL_H
+#define _KERNEL_API_INTERNAL_H
+
+#include <linux/kernel_api_spec.h>
+
+/*
+ * Section boundaries for the `.kapi_specs` array. Defined by the
+ * linker script in include/asm-generic/vmlinux.lds.h.
+ */
+extern const struct kernel_api_spec * const __start_kapi_specs[];
+extern const struct kernel_api_spec * const __stop_kapi_specs[];
+
+const char *kapi_param_type_to_string(enum kapi_param_type type);
+const char *kapi_lock_type_to_string(enum kapi_lock_type type);
+const char *kapi_lock_scope_to_string(enum kapi_lock_scope scope);
+
+#endif /* _KERNEL_API_INTERNAL_H */
diff --git a/kernel/api/kapi_kunit.c b/kernel/api/kapi_kunit.c
new file mode 100644
index 0000000000000..4f6f874c33b86
--- /dev/null
+++ b/kernel/api/kapi_kunit.c
@@ -0,0 +1,720 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * KUnit tests for the Kernel API Specification Framework
+ *
+ * Tests registration, lookup, validation, and JSON export functionality.
+ */
+
+#include <kunit/test.h>
+#include <linux/kernel_api_spec.h>
+#include <linux/string.h>
+#include <linux/slab.h>
+#include <linux/capability.h>
+#include <linux/mm.h>
+#include <linux/signal.h>
+#include <linux/uaccess.h>
+
+static void init_test_spec(struct kernel_api_spec *spec, const char *name)
+{
+ memset(spec, 0, sizeof(*spec));
+ spec->name = name;
+ spec->version = 1;
+ spec->description = "Test API";
+}
+
+/* kapi_register_spec with valid spec returns 0 */
+static void test_register_valid(struct kunit *test)
+{
+ struct kernel_api_spec *spec;
+ int ret;
+
+ spec = kzalloc_obj(*spec, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);
+
+ init_test_spec(spec, "test_register_valid");
+
+ ret = kapi_register_spec(spec);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ kapi_unregister_spec("test_register_valid");
+ kfree(spec);
+}
+
+/* kapi_get_spec returns registered spec */
+static void test_lookup_registered(struct kunit *test)
+{
+ struct kernel_api_spec *spec;
+ const struct kernel_api_spec *found;
+ int ret;
+
+ spec = kzalloc_obj(*spec, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);
+
+ init_test_spec(spec, "test_lookup_func");
+
+ ret = kapi_register_spec(spec);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ found = kapi_get_spec("test_lookup_func");
+ KUNIT_EXPECT_PTR_EQ(test, found, (const struct kernel_api_spec *)spec);
+
+ kapi_unregister_spec("test_lookup_func");
+ kfree(spec);
+}
+
+/* Double registration returns -EEXIST */
+static void test_double_register(struct kunit *test)
+{
+ struct kernel_api_spec *spec;
+ int ret;
+
+ spec = kzalloc_obj(*spec, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);
+
+ init_test_spec(spec, "test_double_reg");
+
+ ret = kapi_register_spec(spec);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kapi_register_spec(spec);
+ KUNIT_EXPECT_EQ(test, ret, -EEXIST);
+
+ kapi_unregister_spec("test_double_reg");
+ kfree(spec);
+}
+
+/* Unregister makes spec unfindable */
+static void test_unregister(struct kunit *test)
+{
+ struct kernel_api_spec *spec;
+ const struct kernel_api_spec *found;
+ int ret;
+
+ spec = kzalloc_obj(*spec, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);
+
+ init_test_spec(spec, "test_unreg_func");
+
+ ret = kapi_register_spec(spec);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ kapi_unregister_spec("test_unreg_func");
+
+ found = kapi_get_spec("test_unreg_func");
+ KUNIT_EXPECT_NULL(test, found);
+
+ kfree(spec);
+}
+
+/* kapi_get_spec(NULL) returns NULL */
+static void test_get_spec_null(struct kunit *test)
+{
+ const struct kernel_api_spec *found;
+
+ found = kapi_get_spec(NULL);
+ KUNIT_EXPECT_NULL(test, found);
+}
+
+/* kapi_register_spec(NULL) returns -EINVAL */
+static void test_register_null(struct kunit *test)
+{
+ int ret;
+
+ ret = kapi_register_spec(NULL);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+}
+
+/* Spec with a NULL name is rejected */
+static void test_register_null_name(struct kunit *test)
+{
+ struct kernel_api_spec *spec;
+ int ret;
+
+ spec = kzalloc_obj(*spec, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);
+
+ /* spec->name == NULL after zero-init; registration rejects it. */
+ ret = kapi_register_spec(spec);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ kfree(spec);
+}
+
+#ifdef CONFIG_KAPI_RUNTIME_CHECKS
+
+/* RANGE constraint - value in range is valid */
+static void test_constraint_range_valid(struct kunit *test)
+{
+ struct kapi_param_spec param = {};
+
+ param.name = "test_param";
+ param.constraint_type = KAPI_CONSTRAINT_RANGE;
+ param.min_value = 0;
+ param.max_value = 100;
+
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 0));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 50));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 100));
+
+ param.min_value = -10;
+ param.max_value = -1;
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, -10));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, -1));
+
+ /* An unsigned maximum such as U64_MAX reads as -1 */
+ param.min_value = 0;
+ param.max_value = (s64)U64_MAX;
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, S64_MAX));
+}
+
+/* RANGE constraint - value out of range is invalid */
+static void test_constraint_range_invalid(struct kunit *test)
+{
+ struct kapi_param_spec param = {};
+
+ param.name = "test_param";
+ param.constraint_type = KAPI_CONSTRAINT_RANGE;
+ param.min_value = 0;
+ param.max_value = 100;
+
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, -1));
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 101));
+
+ param.min_value = -10;
+ param.max_value = -1;
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, -11));
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 0));
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 5));
+}
+
+/* MASK constraint - valid bits pass */
+static void test_constraint_mask_valid(struct kunit *test)
+{
+ struct kapi_param_spec param = {};
+
+ param.name = "test_flags";
+ param.constraint_type = KAPI_CONSTRAINT_MASK;
+ param.valid_mask = 0xFF;
+
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 0x00));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 0x0F));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 0xFF));
+}
+
+/* MASK constraint - extra bits fail */
+static void test_constraint_mask_invalid(struct kunit *test)
+{
+ struct kapi_param_spec param = {};
+
+ param.name = "test_flags";
+ param.constraint_type = KAPI_CONSTRAINT_MASK;
+ param.valid_mask = 0xFF;
+
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 0x100));
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 0x1FF));
+}
+
+/* POWER_OF_TWO constraint */
+static void test_constraint_power_of_two(struct kunit *test)
+{
+ struct kapi_param_spec param = {};
+
+ param.name = "test_pot";
+ param.constraint_type = KAPI_CONSTRAINT_POWER_OF_TWO;
+
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 1));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 2));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 4));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 8));
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 0));
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 3));
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 5));
+}
+
+/* PAGE_ALIGNED constraint */
+static void test_constraint_page_aligned(struct kunit *test)
+{
+ struct kapi_param_spec param = {};
+
+ param.name = "test_page";
+ param.constraint_type = KAPI_CONSTRAINT_PAGE_ALIGNED;
+
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 0));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, PAGE_SIZE));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 2 * PAGE_SIZE));
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 1));
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, PAGE_SIZE - 1));
+}
+
+/* NONZERO constraint */
+static void test_constraint_nonzero(struct kunit *test)
+{
+ struct kapi_param_spec param = {};
+
+ param.name = "test_nz";
+ param.constraint_type = KAPI_CONSTRAINT_NONZERO;
+
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 0));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 1));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, -1));
+}
+
+/* Return value validation - success */
+static void test_return_validation(struct kunit *test)
+{
+ struct kernel_api_spec *spec;
+
+ spec = kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);
+
+ spec->name = "test_ret";
+ spec->return_magic = KAPI_MAGIC_RETURN;
+ spec->return_spec.check_type = KAPI_RETURN_EXACT;
+ spec->return_spec.success_value = 0;
+
+ KUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, 0));
+}
+
+/* Return value validation - known error */
+static void test_return_known_error(struct kunit *test)
+{
+ struct kernel_api_spec *spec;
+
+ spec = kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);
+
+ spec->name = "test_ret_err";
+ spec->return_magic = KAPI_MAGIC_RETURN;
+ spec->return_spec.check_type = KAPI_RETURN_FD;
+ spec->error_count = 1;
+ spec->errors[0].error_code = -ENOENT;
+ spec->errors[0].name = "ENOENT";
+
+ /* -ENOENT is in the error list, so it's valid */
+ KUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, -ENOENT));
+}
+
+/* Return value validation - unknown error */
+static void test_return_unknown_error(struct kunit *test)
+{
+ struct kernel_api_spec *spec;
+
+ spec = kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);
+
+ spec->name = "test_ret_unk";
+ spec->return_magic = KAPI_MAGIC_RETURN;
+ spec->return_spec.check_type = KAPI_RETURN_FD;
+ spec->error_count = 1;
+ spec->errors[0].error_code = -ENOENT;
+ spec->errors[0].name = "ENOENT";
+
+ /* -EPERM is not in the error list, but unlisted errors are accepted
+ * since filesystem/device-specific errors may not be exhaustively listed
+ */
+ KUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, -EPERM));
+}
+
+/* ALIGNMENT constraint */
+static void test_constraint_alignment(struct kunit *test)
+{
+ struct kapi_param_spec param = {};
+
+ param.name = "test_align";
+ param.constraint_type = KAPI_CONSTRAINT_ALIGNMENT;
+ param.alignment = 8;
+
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 0));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 8));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 16));
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 1));
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 7));
+}
+
+/* FD validation rejects values > INT_MAX */
+static void test_fd_int_overflow(struct kunit *test)
+{
+ struct kapi_param_spec param = {};
+
+ param.name = "test_fd";
+ param.type = KAPI_TYPE_FD;
+ param.constraint_type = KAPI_CONSTRAINT_NONE;
+
+ /* Value that overflows int: 0x100000003 -> truncates to 3 */
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 0x100000003LL));
+}
+
+/* ENUM constraint */
+static const s64 test_enum_vals[] = { 1, 5, 10 };
+
+static void test_constraint_enum(struct kunit *test)
+{
+ struct kapi_param_spec param = {};
+
+ param.name = "test_enum";
+ param.constraint_type = KAPI_CONSTRAINT_ENUM;
+ param.enum_values = test_enum_vals;
+ param.enum_count = ARRAY_SIZE(test_enum_vals);
+
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 1));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 5));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 10));
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 0));
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 3));
+ KUNIT_EXPECT_FALSE(test, kapi_validate_param(¶m, 11));
+}
+
+/* BUFFER constraint always accepts (size checked at runtime) */
+static void test_constraint_buffer(struct kunit *test)
+{
+ struct kapi_param_spec param = {};
+
+ param.name = "test_buf";
+ param.constraint_type = KAPI_CONSTRAINT_BUFFER;
+
+ /* Buffer constraint doesn't validate the value itself */
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 0));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_param(¶m, 4096));
+}
+
+/* RETURN_RANGE check type */
+static void test_return_range(struct kunit *test)
+{
+ struct kernel_api_spec *spec;
+
+ spec = kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);
+
+ spec->name = "test_ret_range";
+ spec->return_magic = KAPI_MAGIC_RETURN;
+ spec->return_spec.check_type = KAPI_RETURN_RANGE;
+ spec->return_spec.success_min = 0;
+ spec->return_spec.success_max = 100;
+
+ KUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, 0));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, 50));
+ KUNIT_EXPECT_TRUE(test, kapi_validate_return_value(spec, 100));
+}
+
+static void init_dyn_buf_param(struct kapi_param_spec *param)
+{
+ param->name = "buf";
+ param->type = KAPI_TYPE_USER_PTR;
+ param->constraint_type = KAPI_CONSTRAINT_BUFFER;
+ param->size_param_idx = 2;
+}
+
+static bool validate_dyn_buf(const struct kapi_param_spec *param,
+ const void __user *ptr, s64 count)
+{
+ s64 params[] = { (s64)(unsigned long)ptr, count };
+
+ return kapi_validate_param_with_context(param, params[0], params,
+ ARRAY_SIZE(params));
+}
+
+/* Dynamic buffer: any pointer is accepted when the size is 0 */
+static void test_dyn_buf_zero_count(struct kunit *test)
+{
+ struct kapi_param_spec param = {};
+ const void __user *kernel_addr = (void __user *)-(unsigned long)PAGE_SIZE;
+
+ init_dyn_buf_param(¶m);
+
+ KUNIT_EXPECT_TRUE(test, validate_dyn_buf(¶m, NULL, 0));
+ KUNIT_EXPECT_TRUE(test, validate_dyn_buf(¶m, (void __user *)PAGE_SIZE, 0));
+ KUNIT_EXPECT_TRUE(test, validate_dyn_buf(¶m, kernel_addr, 0));
+}
+
+/* Dynamic buffer: NULL is rejected when the size is non-zero */
+static void test_dyn_buf_null_nonzero_count(struct kunit *test)
+{
+ struct kapi_param_spec param = {};
+
+ init_dyn_buf_param(¶m);
+
+ KUNIT_EXPECT_FALSE(test, validate_dyn_buf(¶m, NULL, 1));
+ KUNIT_EXPECT_FALSE(test, validate_dyn_buf(¶m, NULL, PAGE_SIZE));
+}
+
+/*
+ * Some architectures (separate user address space, !MMU) accept any address
+ * in access_ok(), so the rejection cases below only apply where it bounds
+ * user ranges.
+ */
+static bool kapi_rejects_kernel_addr(const void __user *addr)
+{
+ return !access_ok(addr, 1);
+}
+
+static bool kapi_bounds_user_range(const void __user *base)
+{
+ return access_ok(base, PAGE_SIZE) && !access_ok(base, 3 * PAGE_SIZE);
+}
+
+/* Dynamic buffer: addresses outside user space are rejected when the size is non-zero */
+static void test_dyn_buf_kernel_addr(struct kunit *test)
+{
+ struct kapi_param_spec param = {};
+ const void __user *kernel_addr = (void __user *)-(unsigned long)PAGE_SIZE;
+ const void __user *top = (void __user *)ULONG_MAX;
+
+ init_dyn_buf_param(¶m);
+
+ if (!kapi_rejects_kernel_addr(kernel_addr) || !kapi_rejects_kernel_addr(top))
+ kunit_skip(test, "access_ok() accepts kernel addresses on this architecture");
+
+ KUNIT_EXPECT_FALSE(test, validate_dyn_buf(¶m, kernel_addr, 1));
+ KUNIT_EXPECT_FALSE(test, validate_dyn_buf(¶m, top, 1));
+}
+
+/* Dynamic buffer: an unset size_multiplier means byte units */
+static void test_dyn_buf_default_multiplier(struct kunit *test)
+{
+ struct kapi_param_spec param = {};
+ const void __user *near_top = (void __user *)(TASK_SIZE_MAX - 2 * PAGE_SIZE);
+
+ init_dyn_buf_param(¶m);
+
+ KUNIT_EXPECT_TRUE(test, validate_dyn_buf(¶m, (void __user *)PAGE_SIZE, 16));
+ KUNIT_EXPECT_TRUE(test, validate_dyn_buf(¶m, near_top, 0));
+
+ if (!kapi_bounds_user_range(near_top))
+ kunit_skip(test, "access_ok() does not bound user ranges on this architecture");
+
+ KUNIT_EXPECT_TRUE(test, validate_dyn_buf(¶m, near_top, PAGE_SIZE));
+ KUNIT_EXPECT_FALSE(test, validate_dyn_buf(¶m, near_top, 3 * PAGE_SIZE));
+}
+
+/* Dynamic buffer: explicit multiplier scales the size, negative and overflowing counts fail */
+static void test_dyn_buf_multiplier_and_bad_count(struct kunit *test)
+{
+ struct kapi_param_spec param = {};
+ const void __user *near_top = (void __user *)(TASK_SIZE_MAX - 2 * PAGE_SIZE);
+
+ init_dyn_buf_param(¶m);
+ param.size_multiplier = 8;
+
+ KUNIT_EXPECT_FALSE(test, validate_dyn_buf(¶m, (void __user *)PAGE_SIZE,
+ (s64)(SIZE_MAX / 8 + 1)));
+ KUNIT_EXPECT_FALSE(test, validate_dyn_buf(¶m, (void __user *)PAGE_SIZE, -1));
+
+ if (!kapi_bounds_user_range(near_top))
+ kunit_skip(test, "access_ok() does not bound user ranges on this architecture");
+
+ KUNIT_EXPECT_TRUE(test, validate_dyn_buf(¶m, near_top, PAGE_SIZE / 8));
+ KUNIT_EXPECT_FALSE(test, validate_dyn_buf(¶m, near_top, PAGE_SIZE));
+}
+
+#endif /* CONFIG_KAPI_RUNTIME_CHECKS */
+
+static const struct kernel_api_spec kapi_test_macro_spec = {
+ .name = "test_macro_spec",
+ KAPI_SIGNAL_MASK_COUNT(1)
+ KAPI_SIGNAL_MASK(0, "blocked", "Signals blocked while waiting")
+ KAPI_SIGNAL_MASK_SIGNALS(SIGINT, SIGTERM, SIGQUIT)
+ },
+ KAPI_CAPABILITY_COUNT(1)
+ KAPI_CAPABILITY(0, CAP_SYS_ADMIN, "CAP_SYS_ADMIN", KAPI_CAP_BYPASS_CHECK)
+ KAPI_CAP_ALTERNATIVE(CAP_SYS_RESOURCE, CAP_NET_ADMIN)
+ },
+};
+
+/* Signal mask and capability alternative macros fill in their counts */
+static void test_signal_mask_and_cap_alternative(struct kunit *test)
+{
+ const struct kernel_api_spec *spec = &kapi_test_macro_spec;
+ char *buf;
+
+ KUNIT_EXPECT_EQ(test, spec->signal_mask_count, 1U);
+ KUNIT_EXPECT_EQ(test, spec->signal_masks[0].signal_count, 3U);
+ KUNIT_EXPECT_EQ(test, spec->signal_masks[0].signals[2], SIGQUIT);
+ KUNIT_EXPECT_EQ(test, spec->capabilities[0].alternative_count, 2U);
+ KUNIT_EXPECT_EQ(test, spec->capabilities[0].alternative[1], CAP_NET_ADMIN);
+
+ buf = kunit_kzalloc(test, 8192, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, buf);
+ KUNIT_ASSERT_GT(test, kapi_export_json(spec, buf, 8192), 0);
+ KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "\"blocked\""));
+ KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "\"alternatives\""));
+}
+
+/* Unregister non-existent spec is a no-op */
+static void test_unregister_nonexistent(struct kunit *test)
+{
+ /* Should not crash or error */
+ kapi_unregister_spec("nonexistent_spec_xyz");
+}
+
+/* Multiple specs can be registered and looked up */
+static void test_multiple_specs(struct kunit *test)
+{
+ struct kernel_api_spec *spec1, *spec2;
+ const struct kernel_api_spec *found;
+
+ spec1 = kzalloc_obj(*spec1, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec1);
+ spec2 = kzalloc_obj(*spec2, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec2);
+
+ init_test_spec(spec1, "multi_spec_1");
+ init_test_spec(spec2, "multi_spec_2");
+
+ KUNIT_ASSERT_EQ(test, kapi_register_spec(spec1), 0);
+ KUNIT_ASSERT_EQ(test, kapi_register_spec(spec2), 0);
+
+ found = kapi_get_spec("multi_spec_1");
+ KUNIT_EXPECT_PTR_EQ(test, found, (const struct kernel_api_spec *)spec1);
+
+ found = kapi_get_spec("multi_spec_2");
+ KUNIT_EXPECT_PTR_EQ(test, found, (const struct kernel_api_spec *)spec2);
+
+ kapi_unregister_spec("multi_spec_1");
+ kapi_unregister_spec("multi_spec_2");
+ kfree(spec1);
+ kfree(spec2);
+}
+
+/* JSON export produces valid output */
+static void test_json_export(struct kunit *test)
+{
+ static const s64 enum_vals[] = { 3, -1 };
+ static const s64 err_vals[] = { -EINVAL, -EFAULT };
+ struct kernel_api_spec *spec;
+ char *buf;
+ int ret;
+
+ spec = kzalloc_obj(*spec, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);
+
+ buf = kzalloc(4096, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buf);
+
+ init_test_spec(spec, "test_json");
+ spec->param_count = 1;
+ spec->params[0].name = "arg0";
+ spec->params[0].type_name = "int";
+ spec->params[0].constraint_type = KAPI_CONSTRAINT_ENUM;
+ spec->params[0].enum_values = enum_vals;
+ spec->params[0].enum_count = ARRAY_SIZE(enum_vals);
+ spec->params[0].valid_mask = 0xff;
+ spec->params[0].size_param_idx = 2;
+ spec->return_spec.check_type = KAPI_RETURN_ERROR_CHECK;
+ spec->return_spec.error_values = err_vals;
+ spec->return_spec.error_count = ARRAY_SIZE(err_vals);
+
+ ret = kapi_export_json(spec, buf, 4096);
+ KUNIT_EXPECT_GT(test, ret, 0);
+
+ /* Verify it starts with '{' and ends with '}' */
+ KUNIT_EXPECT_EQ(test, buf[0], '{');
+ KUNIT_ASSERT_GT(test, ret, 1);
+ /* Find last non-whitespace char */
+ while (ret > 0 && (buf[ret - 1] == '\n' || buf[ret - 1] == ' '))
+ ret--;
+ KUNIT_EXPECT_EQ(test, buf[ret - 1], '}');
+
+ /* Verify key fields are present */
+ KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "\"name\""));
+ KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "\"test_json\""));
+ KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "\"parameters\""));
+
+ KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "\"constraint_type\": \"enum\""));
+ KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "\"enum_values\": [3, -1]"));
+ KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "\"valid_mask\": \"0xff\""));
+ KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "\"size_param_idx\": 1"));
+ KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "\"error_values\": [-22, -14]"));
+ KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "\"state_transitions\""));
+ KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "\"struct_specs\""));
+
+ kfree(buf);
+ kfree(spec);
+}
+
+/* JSON export with NULL args returns -EINVAL */
+static void test_json_export_null(struct kunit *test)
+{
+ struct kernel_api_spec *spec;
+ char buf[64];
+ int ret;
+
+ ret = kapi_export_json(NULL, buf, sizeof(buf));
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ spec = kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);
+ init_test_spec(spec, "test");
+
+ ret = kapi_export_json(spec, NULL, 64);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ ret = kapi_export_json(spec, buf, 0);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+}
+
+/* JSON export into a buffer that is too small reports -E2BIG */
+static void test_json_export_small_buffer(struct kunit *test)
+{
+ struct kernel_api_spec *spec;
+ char buf[64];
+ int ret;
+
+ spec = kunit_kzalloc(test, sizeof(*spec), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, spec);
+ init_test_spec(spec, "test_small");
+
+ ret = kapi_export_json(spec, buf, sizeof(buf));
+
+ KUNIT_EXPECT_EQ(test, ret, -E2BIG);
+}
+
+static struct kunit_case kapi_test_cases[] = {
+ KUNIT_CASE(test_register_valid),
+ KUNIT_CASE(test_lookup_registered),
+ KUNIT_CASE(test_double_register),
+ KUNIT_CASE(test_unregister),
+ KUNIT_CASE(test_get_spec_null),
+ KUNIT_CASE(test_register_null),
+ KUNIT_CASE(test_register_null_name),
+#ifdef CONFIG_KAPI_RUNTIME_CHECKS
+ KUNIT_CASE(test_constraint_range_valid),
+ KUNIT_CASE(test_constraint_range_invalid),
+ KUNIT_CASE(test_constraint_mask_valid),
+ KUNIT_CASE(test_constraint_mask_invalid),
+ KUNIT_CASE(test_constraint_power_of_two),
+ KUNIT_CASE(test_constraint_page_aligned),
+ KUNIT_CASE(test_constraint_nonzero),
+ KUNIT_CASE(test_return_validation),
+ KUNIT_CASE(test_return_known_error),
+ KUNIT_CASE(test_return_unknown_error),
+ KUNIT_CASE(test_constraint_alignment),
+ KUNIT_CASE(test_fd_int_overflow),
+ KUNIT_CASE(test_constraint_enum),
+ KUNIT_CASE(test_constraint_buffer),
+ KUNIT_CASE(test_return_range),
+ KUNIT_CASE(test_dyn_buf_zero_count),
+ KUNIT_CASE(test_dyn_buf_null_nonzero_count),
+ KUNIT_CASE(test_dyn_buf_kernel_addr),
+ KUNIT_CASE(test_dyn_buf_default_multiplier),
+ KUNIT_CASE(test_dyn_buf_multiplier_and_bad_count),
+#endif
+ KUNIT_CASE(test_signal_mask_and_cap_alternative),
+ KUNIT_CASE(test_unregister_nonexistent),
+ KUNIT_CASE(test_multiple_specs),
+ KUNIT_CASE(test_json_export),
+ KUNIT_CASE(test_json_export_null),
+ KUNIT_CASE(test_json_export_small_buffer),
+ {}
+};
+
+static struct kunit_suite kapi_test_suite = {
+ .name = "kapi",
+ .test_cases = kapi_test_cases,
+};
+
+kunit_test_suite(kapi_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for Kernel API Specification Framework");
+MODULE_LICENSE("GPL");
diff --git a/kernel/api/kernel_api_spec.c b/kernel/api/kernel_api_spec.c
new file mode 100644
index 0000000000000..c3b220104c04b
--- /dev/null
+++ b/kernel/api/kernel_api_spec.c
@@ -0,0 +1,1415 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * kernel_api_spec.c - Kernel API Specification Framework Implementation
+ *
+ * Provides runtime support for kernel API specifications including validation,
+ * export to various formats, and querying capabilities.
+ */
+
+#define pr_fmt(fmt) "kapi: " fmt
+
+#include <linux/kernel.h>
+#include <linux/kernel_api_spec.h>
+#include <linux/string.h>
+#include <linux/slab.h>
+#include <linux/list.h>
+#include <linux/mutex.h>
+#include <linux/export.h>
+#include <linux/preempt.h>
+#include <linux/hardirq.h>
+#include <linux/file.h>
+#include <linux/fdtable.h>
+#include <linux/uaccess.h>
+#include <linux/limits.h>
+#include <linux/fcntl.h>
+#include <linux/mm.h>
+#include <linux/ratelimit.h>
+
+#include "internal.h"
+
+/* Dynamic API registration */
+static LIST_HEAD(dynamic_api_specs);
+static DEFINE_MUTEX(api_spec_mutex);
+
+struct dynamic_api_spec {
+ struct list_head list;
+ const struct kernel_api_spec *spec;
+};
+
+/*
+ * __kapi_find_spec_locked - Internal lookup, caller must hold api_spec_mutex
+ */
+static const struct kernel_api_spec *__kapi_find_spec_locked(const char *name)
+{
+ const struct kernel_api_spec * const *pp;
+ struct dynamic_api_spec *dyn_spec;
+
+ for (pp = __start_kapi_specs; pp < __stop_kapi_specs; pp++) {
+ const struct kernel_api_spec *spec = *pp;
+
+ if (spec && spec->name && strcmp(spec->name, name) == 0)
+ return spec;
+ }
+
+ list_for_each_entry(dyn_spec, &dynamic_api_specs, list) {
+ if (dyn_spec->spec->name &&
+ strcmp(dyn_spec->spec->name, name) == 0)
+ return dyn_spec->spec;
+ }
+
+ return NULL;
+}
+
+/**
+ * kapi_get_spec - Get API specification by name
+ * @name: Function name to look up
+ *
+ * Return: Pointer to the API specification, or NULL if not found. The
+ * pointer stays valid for specifications in the ``.kapi_specs`` ELF section
+ * (built-in, statically defined). A dynamically registered spec stays valid
+ * only until kapi_unregister_spec() is called for it, so the caller must
+ * serialize against unregistration.
+ *
+ * Context: May sleep. Do not call under spinlock or in IRQ context.
+ */
+const struct kernel_api_spec *kapi_get_spec(const char *name)
+{
+ const struct kernel_api_spec *spec;
+
+ if (!name)
+ return NULL;
+
+ mutex_lock(&api_spec_mutex);
+ spec = __kapi_find_spec_locked(name);
+ mutex_unlock(&api_spec_mutex);
+
+ return spec;
+}
+EXPORT_SYMBOL_GPL(kapi_get_spec);
+
+/**
+ * kapi_register_spec - Register a dynamic API specification
+ * @spec: API specification to register
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int kapi_register_spec(const struct kernel_api_spec *spec)
+{
+ struct dynamic_api_spec *dyn_spec;
+ int ret = 0;
+
+ if (!spec || !spec->name || !spec->name[0])
+ return -EINVAL;
+
+ dyn_spec = kzalloc_obj(*dyn_spec, GFP_KERNEL);
+ if (!dyn_spec)
+ return -ENOMEM;
+
+ dyn_spec->spec = spec;
+
+ mutex_lock(&api_spec_mutex);
+
+ /* Check if already exists while holding lock to prevent races */
+ if (__kapi_find_spec_locked(spec->name)) {
+ ret = -EEXIST;
+ kfree(dyn_spec);
+ } else {
+ list_add_tail(&dyn_spec->list, &dynamic_api_specs);
+ }
+
+ mutex_unlock(&api_spec_mutex);
+
+ return ret;
+}
+EXPORT_SYMBOL_GPL(kapi_register_spec);
+
+/**
+ * kapi_unregister_spec - Unregister a dynamic API specification
+ * @name: Name of API to unregister
+ */
+void kapi_unregister_spec(const char *name)
+{
+ struct dynamic_api_spec *dyn_spec, *tmp;
+
+ if (!name)
+ return;
+
+ mutex_lock(&api_spec_mutex);
+ list_for_each_entry_safe(dyn_spec, tmp, &dynamic_api_specs, list) {
+ if (dyn_spec->spec->name &&
+ strcmp(dyn_spec->spec->name, name) == 0) {
+ list_del(&dyn_spec->list);
+ kfree(dyn_spec);
+ break;
+ }
+ }
+ mutex_unlock(&api_spec_mutex);
+}
+EXPORT_SYMBOL_GPL(kapi_unregister_spec);
+
+/**
+ * kapi_param_type_to_string - Convert parameter type to string
+ * @type: Parameter type
+ *
+ * Return: String representation of type
+ */
+const char *kapi_param_type_to_string(enum kapi_param_type type)
+{
+ static const char * const type_names[] = {
+ [KAPI_TYPE_VOID] = "void",
+ [KAPI_TYPE_INT] = "int",
+ [KAPI_TYPE_UINT] = "uint",
+ [KAPI_TYPE_PTR] = "pointer",
+ [KAPI_TYPE_STRUCT] = "struct",
+ [KAPI_TYPE_UNION] = "union",
+ [KAPI_TYPE_ENUM] = "enum",
+ [KAPI_TYPE_FUNC_PTR] = "function_pointer",
+ [KAPI_TYPE_ARRAY] = "array",
+ [KAPI_TYPE_FD] = "file_descriptor",
+ [KAPI_TYPE_USER_PTR] = "user_pointer",
+ [KAPI_TYPE_PATH] = "pathname",
+ [KAPI_TYPE_CUSTOM] = "custom",
+ };
+
+ if (type >= ARRAY_SIZE(type_names))
+ return "unknown";
+
+ return type_names[type];
+}
+
+/**
+ * kapi_lock_type_to_string - Convert lock type to string
+ * @type: Lock type
+ *
+ * Return: String representation of lock type
+ */
+const char *kapi_lock_type_to_string(enum kapi_lock_type type)
+{
+ static const char * const lock_names[] = {
+ [KAPI_LOCK_NONE] = "none",
+ [KAPI_LOCK_MUTEX] = "mutex",
+ [KAPI_LOCK_SPINLOCK] = "spinlock",
+ [KAPI_LOCK_RWLOCK] = "rwlock",
+ [KAPI_LOCK_SEQLOCK] = "seqlock",
+ [KAPI_LOCK_RCU] = "rcu",
+ [KAPI_LOCK_SEMAPHORE] = "semaphore",
+ [KAPI_LOCK_CUSTOM] = "custom",
+ };
+
+ if (type >= ARRAY_SIZE(lock_names))
+ return "unknown";
+
+ return lock_names[type];
+}
+
+/**
+ * kapi_lock_scope_to_string - Convert lock scope to string
+ * @scope: Lock scope
+ *
+ * Return: String representation of lock scope
+ */
+const char *kapi_lock_scope_to_string(enum kapi_lock_scope scope)
+{
+ static const char * const scope_names[] = {
+ [KAPI_LOCK_INTERNAL] = "internal",
+ [KAPI_LOCK_ACQUIRES] = "acquires",
+ [KAPI_LOCK_RELEASES] = "releases",
+ [KAPI_LOCK_CALLER_HELD] = "caller_held",
+ };
+
+ if (scope >= ARRAY_SIZE(scope_names))
+ return "unknown";
+
+ return scope_names[scope];
+}
+
+/**
+ * return_check_type_to_string - Convert return check type to string
+ * @type: Return check type
+ *
+ * Return: String representation of return check type
+ */
+static const char *return_check_type_to_string(enum kapi_return_check_type type)
+{
+ static const char * const check_names[] = {
+ [KAPI_RETURN_EXACT] = "exact",
+ [KAPI_RETURN_RANGE] = "range",
+ [KAPI_RETURN_ERROR_CHECK] = "error_check",
+ [KAPI_RETURN_FD] = "file_descriptor",
+ [KAPI_RETURN_CUSTOM] = "custom",
+ [KAPI_RETURN_NO_RETURN] = "no_return",
+ };
+
+ if (type >= ARRAY_SIZE(check_names))
+ return "unknown";
+
+ return check_names[type];
+}
+
+/**
+ * capability_action_to_string - Convert capability action to string
+ * @action: Capability action
+ *
+ * Return: String representation of capability action
+ */
+static const char *capability_action_to_string(enum kapi_capability_action action)
+{
+ static const char * const action_names[] = {
+ [KAPI_CAP_BYPASS_CHECK] = "bypass_check",
+ [KAPI_CAP_INCREASE_LIMIT] = "increase_limit",
+ [KAPI_CAP_OVERRIDE_RESTRICTION] = "override_restriction",
+ [KAPI_CAP_GRANT_PERMISSION] = "grant_permission",
+ [KAPI_CAP_MODIFY_BEHAVIOR] = "modify_behavior",
+ [KAPI_CAP_ACCESS_RESOURCE] = "access_resource",
+ [KAPI_CAP_PERFORM_OPERATION] = "perform_operation",
+ };
+
+ if (action >= ARRAY_SIZE(action_names))
+ return "unknown";
+
+ return action_names[action];
+}
+
+/**
+ * constraint_type_to_string - Convert constraint type to string
+ * @type: Constraint type
+ *
+ * Return: String representation of constraint type
+ */
+static const char *constraint_type_to_string(enum kapi_constraint_type type)
+{
+ static const char * const constraint_names[] = {
+ [KAPI_CONSTRAINT_NONE] = "none",
+ [KAPI_CONSTRAINT_RANGE] = "range",
+ [KAPI_CONSTRAINT_MASK] = "mask",
+ [KAPI_CONSTRAINT_ENUM] = "enum",
+ [KAPI_CONSTRAINT_ALIGNMENT] = "alignment",
+ [KAPI_CONSTRAINT_POWER_OF_TWO] = "power_of_two",
+ [KAPI_CONSTRAINT_PAGE_ALIGNED] = "page_aligned",
+ [KAPI_CONSTRAINT_NONZERO] = "nonzero",
+ [KAPI_CONSTRAINT_USER_STRING] = "user_string",
+ [KAPI_CONSTRAINT_USER_PATH] = "user_path",
+ [KAPI_CONSTRAINT_USER_PTR] = "user_ptr",
+ [KAPI_CONSTRAINT_BUFFER] = "buffer",
+ [KAPI_CONSTRAINT_CUSTOM] = "custom",
+ };
+
+ if (type >= ARRAY_SIZE(constraint_names))
+ return "unknown";
+
+ return constraint_names[type];
+}
+
+/*
+ * kapi_json_escape - Write a JSON-escaped string into a buffer
+ * @buf: Output buffer
+ * @size: Remaining space in buffer
+ * @str: Input string to escape
+ *
+ * Escapes backslash, double-quote, and control characters for JSON output.
+ * Return: Number of bytes written (via scnprintf semantics)
+ */
+static int kapi_json_escape(char *buf, size_t size, const char *str)
+{
+ int ret = 0;
+ const char *p;
+
+ if (!str || size == 0)
+ return 0;
+
+ for (p = str; *p && ret < size - 1; p++) {
+ switch (*p) {
+ case '\\':
+ ret += scnprintf(buf + ret, size - ret, "\\\\");
+ break;
+ case '"':
+ ret += scnprintf(buf + ret, size - ret, "\\\"");
+ break;
+ case '\n':
+ ret += scnprintf(buf + ret, size - ret, "\\n");
+ break;
+ case '\r':
+ ret += scnprintf(buf + ret, size - ret, "\\r");
+ break;
+ case '\t':
+ ret += scnprintf(buf + ret, size - ret, "\\t");
+ break;
+ default:
+ if ((unsigned char)*p < 0x20) {
+ ret += scnprintf(buf + ret, size - ret,
+ "\\u%04x", (unsigned char)*p);
+ } else {
+ ret += scnprintf(buf + ret, size - ret,
+ "%c", *p);
+ }
+ break;
+ }
+ }
+
+ if (ret < size)
+ buf[ret] = '\0';
+
+ return ret;
+}
+
+static int kapi_json_str(char *buf, size_t size, const char *str)
+{
+ int ret = 0;
+
+ ret += scnprintf(buf, size, "\"");
+ ret += kapi_json_escape(buf + ret, size - ret, str);
+ ret += scnprintf(buf + ret, size - ret, "\"");
+ return ret;
+}
+
+static int kapi_json_s64_list(char *buf, size_t size, const s64 *vals, u32 count)
+{
+ int ret = scnprintf(buf, size, "[");
+ u32 i;
+
+ for (i = 0; vals && i < count; i++)
+ ret += scnprintf(buf + ret, size - ret, "%s%lld",
+ i ? ", " : "", vals[i]);
+
+ ret += scnprintf(buf + ret, size - ret, "]");
+ return ret;
+}
+
+static int kapi_json_struct_spec(char *buf, size_t size,
+ const struct kapi_struct_spec *st)
+{
+ int ret;
+ u32 i;
+
+ ret = scnprintf(buf, size, " {\n \"name\": ");
+ ret += kapi_json_str(buf + ret, size - ret, st->name);
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n \"size\": %zu,\n \"alignment\": %zu,\n \"description\": ",
+ st->size, st->alignment);
+ ret += kapi_json_str(buf + ret, size - ret, st->description);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"fields\": [\n");
+
+ for (i = 0; i < st->field_count && i < KAPI_MAX_PARAMS; i++) {
+ const struct kapi_struct_field *field = &st->fields[i];
+
+ ret += scnprintf(buf + ret, size - ret,
+ " {\n \"name\": ");
+ ret += kapi_json_str(buf + ret, size - ret, field->name);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"type\": ");
+ ret += kapi_json_str(buf + ret, size - ret, field->type_name);
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n"
+ " \"type_class\": \"%s\",\n"
+ " \"offset\": %zu,\n"
+ " \"size\": %zu,\n"
+ " \"flags\": \"0x%x\",\n"
+ " \"constraint_type\": \"%s\",\n"
+ " \"min_value\": %lld,\n"
+ " \"max_value\": %lld,\n"
+ " \"valid_mask\": \"0x%llx\",\n"
+ " \"enum_values\": ",
+ kapi_param_type_to_string(field->type),
+ field->offset, field->size, field->flags,
+ constraint_type_to_string(field->constraint_type),
+ field->min_value, field->max_value, field->valid_mask);
+ ret += kapi_json_str(buf + ret, size - ret, field->enum_values);
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n \"description\": ");
+ ret += kapi_json_str(buf + ret, size - ret, field->description);
+ ret += scnprintf(buf + ret, size - ret,
+ "\n }%s\n",
+ (i < st->field_count - 1) ? "," : "");
+ }
+
+ ret += scnprintf(buf + ret, size - ret, " ]\n }");
+ return ret;
+}
+
+/**
+ * kapi_export_json - Export API specification to JSON format
+ * @spec: API specification to export
+ * @buf: Buffer to write JSON to
+ * @size: Size of buffer
+ *
+ * Return: Number of bytes written, -EINVAL on bad arguments, or -E2BIG
+ * if the output did not fit in @buf (the contents are then incomplete)
+ */
+int kapi_export_json(const struct kernel_api_spec *spec, char *buf, size_t size)
+{
+ int ret = 0;
+ int i, j;
+
+ if (!spec || !buf || size == 0)
+ return -EINVAL;
+
+ ret = scnprintf(buf, size, "{\n \"name\": ");
+ ret += kapi_json_str(buf + ret, size - ret, spec->name);
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n \"version\": %u,\n \"description\": ",
+ spec->version);
+ ret += kapi_json_str(buf + ret, size - ret, spec->description);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"long_description\": ");
+ ret += kapi_json_str(buf + ret, size - ret, spec->long_description);
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n \"context_flags\": \"0x%x\",\n",
+ spec->context_flags);
+
+ /* Parameters */
+ ret += scnprintf(buf + ret, size - ret, " \"parameters\": [\n");
+
+ for (i = 0; i < spec->param_count && i < KAPI_MAX_PARAMS; i++) {
+ const struct kapi_param_spec *param = &spec->params[i];
+
+ ret += scnprintf(buf + ret, size - ret, " {\n \"name\": ");
+ ret += kapi_json_str(buf + ret, size - ret, param->name);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"type\": ");
+ ret += kapi_json_str(buf + ret, size - ret, param->type_name);
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n \"type_class\": \"%s\",\n \"flags\": \"0x%x\",\n \"description\": ",
+ kapi_param_type_to_string(param->type),
+ param->flags);
+ ret += kapi_json_str(buf + ret, size - ret, param->description);
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n \"constraint_type\": \"%s\",\n \"constraint_desc\": ",
+ constraint_type_to_string(param->constraint_type));
+ ret += kapi_json_str(buf + ret, size - ret, param->constraints);
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n"
+ " \"min_value\": %lld,\n"
+ " \"max_value\": %lld,\n"
+ " \"valid_mask\": \"0x%llx\",\n"
+ " \"enum_values\": ",
+ param->min_value, param->max_value, param->valid_mask);
+ ret += kapi_json_s64_list(buf + ret, size - ret,
+ param->enum_values, param->enum_count);
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n \"size\": %zu,\n \"alignment\": %zu,\n \"size_param_idx\": ",
+ param->size, param->alignment);
+ if (param->size_param_idx > 0)
+ ret += scnprintf(buf + ret, size - ret, "%d",
+ param->size_param_idx - 1);
+ else
+ ret += scnprintf(buf + ret, size - ret, "null");
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n \"size_multiplier\": %zu\n }%s\n",
+ param->size_multiplier,
+ (i < spec->param_count - 1) ? "," : "");
+ }
+
+ ret += scnprintf(buf + ret, size - ret, " ],\n");
+
+ /* Return value */
+ ret += scnprintf(buf + ret, size - ret, " \"return\": {\n \"type\": ");
+ ret += kapi_json_str(buf + ret, size - ret, spec->return_spec.type_name);
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n"
+ " \"type_class\": \"%s\",\n"
+ " \"check_type\": \"%s\",\n"
+ " \"success_value\": %lld,\n"
+ " \"success_min\": %lld,\n"
+ " \"success_max\": %lld,\n"
+ " \"error_values\": ",
+ kapi_param_type_to_string(spec->return_spec.type),
+ return_check_type_to_string(spec->return_spec.check_type),
+ spec->return_spec.success_value,
+ spec->return_spec.success_min,
+ spec->return_spec.success_max);
+ ret += kapi_json_s64_list(buf + ret, size - ret,
+ spec->return_spec.error_values,
+ spec->return_spec.error_count);
+
+ ret += scnprintf(buf + ret, size - ret, ",\n \"description\": ");
+ ret += kapi_json_str(buf + ret, size - ret, spec->return_spec.description);
+ ret += scnprintf(buf + ret, size - ret, "\n },\n");
+
+ /* Errors */
+ ret += scnprintf(buf + ret, size - ret, " \"errors\": [\n");
+
+ for (i = 0; i < spec->error_count && i < KAPI_MAX_ERRORS; i++) {
+ const struct kapi_error_spec *error = &spec->errors[i];
+
+ ret += scnprintf(buf + ret, size - ret,
+ " {\n \"code\": %d,\n \"name\": ",
+ error->error_code);
+ ret += kapi_json_str(buf + ret, size - ret, error->name);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"condition\": ");
+ ret += kapi_json_str(buf + ret, size - ret, error->condition);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"description\": ");
+ ret += kapi_json_str(buf + ret, size - ret, error->description);
+ ret += scnprintf(buf + ret, size - ret,
+ "\n }%s\n",
+ (i < spec->error_count - 1) ? "," : "");
+ }
+
+ ret += scnprintf(buf + ret, size - ret, " ],\n");
+
+ /* Locks */
+ ret += scnprintf(buf + ret, size - ret, " \"locks\": [\n");
+
+ for (i = 0; i < spec->lock_count && i < KAPI_MAX_LOCKS; i++) {
+ const struct kapi_lock_spec *lock = &spec->locks[i];
+
+ ret += scnprintf(buf + ret, size - ret, " {\n \"name\": ");
+ ret += kapi_json_str(buf + ret, size - ret, lock->lock_name);
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n \"type\": \"%s\",\n \"scope\": \"%s\",\n \"description\": ",
+ kapi_lock_type_to_string(lock->lock_type),
+ kapi_lock_scope_to_string(lock->scope));
+ ret += kapi_json_str(buf + ret, size - ret, lock->description);
+ ret += scnprintf(buf + ret, size - ret,
+ "\n }%s\n",
+ (i < spec->lock_count - 1) ? "," : "");
+ }
+
+ ret += scnprintf(buf + ret, size - ret, " ],\n");
+
+ /* Capabilities */
+ ret += scnprintf(buf + ret, size - ret, " \"capabilities\": [\n");
+
+ for (i = 0; i < spec->capability_count && i < KAPI_MAX_CAPABILITIES; i++) {
+ const struct kapi_capability_spec *cap = &spec->capabilities[i];
+
+ ret += scnprintf(buf + ret, size - ret,
+ " {\n \"capability\": %d,\n \"name\": ",
+ cap->capability);
+ ret += kapi_json_str(buf + ret, size - ret, cap->cap_name);
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n \"action\": \"%s\",\n \"allows\": ",
+ capability_action_to_string(cap->action));
+ ret += kapi_json_str(buf + ret, size - ret, cap->allows);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"without_cap\": ");
+ ret += kapi_json_str(buf + ret, size - ret, cap->without_cap);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"check_condition\": ");
+ ret += kapi_json_str(buf + ret, size - ret, cap->check_condition);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"priority\": %u", cap->priority);
+
+ if (cap->alternative_count > 0) {
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n \"alternatives\": [");
+ for (j = 0; j < cap->alternative_count && j < KAPI_MAX_CAPABILITIES; j++) {
+ ret += scnprintf(buf + ret, size - ret,
+ "%s%d", j ? ", " : "",
+ cap->alternative[j]);
+ }
+ ret += scnprintf(buf + ret, size - ret, "]");
+ }
+
+ ret += scnprintf(buf + ret, size - ret,
+ "\n }%s\n",
+ (i < spec->capability_count - 1) ? "," : "");
+ }
+
+ ret += scnprintf(buf + ret, size - ret, " ],\n");
+
+ /* Constraints */
+ ret += scnprintf(buf + ret, size - ret, " \"constraints\": [\n");
+ for (i = 0; i < spec->constraint_count && i < KAPI_MAX_CONSTRAINTS; i++) {
+ const struct kapi_constraint_spec *con = &spec->constraints[i];
+
+ ret += scnprintf(buf + ret, size - ret, " {\n \"name\": ");
+ ret += kapi_json_str(buf + ret, size - ret, con->name);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"description\": ");
+ ret += kapi_json_str(buf + ret, size - ret, con->description);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"expression\": ");
+ ret += kapi_json_str(buf + ret, size - ret, con->expression);
+ ret += scnprintf(buf + ret, size - ret,
+ "\n }%s\n",
+ (i < spec->constraint_count - 1) ? "," : "");
+ }
+ ret += scnprintf(buf + ret, size - ret, " ],\n");
+
+ /* Signals */
+ ret += scnprintf(buf + ret, size - ret, " \"signals\": [\n");
+ for (i = 0; i < spec->signal_count && i < KAPI_MAX_SIGNALS; i++) {
+ const struct kapi_signal_spec *sig = &spec->signals[i];
+
+ ret += scnprintf(buf + ret, size - ret,
+ " {\n \"signal_num\": %d,\n \"signal_name\": ",
+ sig->signal_num);
+ ret += kapi_json_str(buf + ret, size - ret, sig->signal_name);
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n \"direction\": \"0x%x\",\n \"action\": %u,\n \"target\": ",
+ sig->direction, sig->action);
+ ret += kapi_json_str(buf + ret, size - ret, sig->target);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"condition\": ");
+ ret += kapi_json_str(buf + ret, size - ret, sig->condition);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"description\": ");
+ ret += kapi_json_str(buf + ret, size - ret, sig->description);
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n"
+ " \"restartable\": %s,\n"
+ " \"sa_flags_required\": \"0x%x\",\n"
+ " \"sa_flags_forbidden\": \"0x%x\",\n"
+ " \"error_on_signal\": %d,\n"
+ " \"transform_to\": %d,\n"
+ " \"timing\": ",
+ sig->restartable ? "true" : "false",
+ sig->sa_flags_required,
+ sig->sa_flags_forbidden,
+ sig->error_on_signal,
+ sig->transform_to);
+ ret += kapi_json_str(buf + ret, size - ret, sig->timing);
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n \"priority\": %u,\n \"interruptible\": %s,\n \"queue_behavior\": ",
+ sig->priority,
+ sig->interruptible ? "true" : "false");
+ ret += kapi_json_str(buf + ret, size - ret, sig->queue_behavior);
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n \"state_required\": \"0x%x\",\n \"state_forbidden\": \"0x%x\"\n }%s\n",
+ sig->state_required,
+ sig->state_forbidden,
+ (i < spec->signal_count - 1) ? "," : "");
+ }
+ ret += scnprintf(buf + ret, size - ret, " ],\n");
+
+ /* Side effects */
+ ret += scnprintf(buf + ret, size - ret, " \"side_effects\": [\n");
+ for (i = 0; i < spec->side_effect_count && i < KAPI_MAX_SIDE_EFFECTS; i++) {
+ const struct kapi_side_effect *eff = &spec->side_effects[i];
+
+ ret += scnprintf(buf + ret, size - ret,
+ " {\n \"type\": \"0x%x\",\n \"target\": ",
+ eff->type);
+ ret += kapi_json_str(buf + ret, size - ret, eff->target);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"condition\": ");
+ ret += kapi_json_str(buf + ret, size - ret, eff->condition);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"description\": ");
+ ret += kapi_json_str(buf + ret, size - ret, eff->description);
+ ret += scnprintf(buf + ret, size - ret,
+ ",\n \"reversible\": %s\n }%s\n",
+ eff->reversible ? "true" : "false",
+ (i < spec->side_effect_count - 1) ? "," : "");
+ }
+ ret += scnprintf(buf + ret, size - ret, " ],\n");
+
+ /* State transitions */
+ ret += scnprintf(buf + ret, size - ret, " \"state_transitions\": [\n");
+ for (i = 0; i < spec->state_trans_count && i < KAPI_MAX_STATE_TRANS; i++) {
+ const struct kapi_state_transition *trans = &spec->state_transitions[i];
+
+ ret += scnprintf(buf + ret, size - ret, " {\n \"object\": ");
+ ret += kapi_json_str(buf + ret, size - ret, trans->object);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"from_state\": ");
+ ret += kapi_json_str(buf + ret, size - ret, trans->from_state);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"to_state\": ");
+ ret += kapi_json_str(buf + ret, size - ret, trans->to_state);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"condition\": ");
+ ret += kapi_json_str(buf + ret, size - ret, trans->condition);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"description\": ");
+ ret += kapi_json_str(buf + ret, size - ret, trans->description);
+ ret += scnprintf(buf + ret, size - ret,
+ "\n }%s\n",
+ (i < spec->state_trans_count - 1) ? "," : "");
+ }
+ ret += scnprintf(buf + ret, size - ret, " ],\n");
+
+ /* Signal masks */
+ ret += scnprintf(buf + ret, size - ret, " \"signal_masks\": [\n");
+ for (i = 0; i < spec->signal_mask_count && i < KAPI_MAX_SIGNALS; i++) {
+ const struct kapi_signal_mask_spec *mask = &spec->signal_masks[i];
+
+ ret += scnprintf(buf + ret, size - ret, " {\n \"name\": ");
+ ret += kapi_json_str(buf + ret, size - ret, mask->mask_name);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"description\": ");
+ ret += kapi_json_str(buf + ret, size - ret, mask->description);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"signals\": [");
+ for (j = 0; j < mask->signal_count && j < KAPI_MAX_SIGNALS; j++)
+ ret += scnprintf(buf + ret, size - ret, "%s%d",
+ j ? ", " : "", mask->signals[j]);
+ ret += scnprintf(buf + ret, size - ret,
+ "]\n }%s\n",
+ (i < spec->signal_mask_count - 1) ? "," : "");
+ }
+ ret += scnprintf(buf + ret, size - ret, " ],\n");
+
+ /* Structure specifications */
+ ret += scnprintf(buf + ret, size - ret, " \"struct_specs\": [\n");
+ for (i = 0; i < spec->struct_spec_count && i < KAPI_MAX_STRUCT_SPECS; i++) {
+ ret += kapi_json_struct_spec(buf + ret, size - ret,
+ &spec->struct_specs[i]);
+ ret += scnprintf(buf + ret, size - ret, "%s\n",
+ (i < spec->struct_spec_count - 1) ? "," : "");
+ }
+ ret += scnprintf(buf + ret, size - ret, " ],\n");
+
+ /* Additional info */
+ ret += scnprintf(buf + ret, size - ret, " \"examples\": ");
+ ret += kapi_json_str(buf + ret, size - ret, spec->examples);
+ ret += scnprintf(buf + ret, size - ret, ",\n \"notes\": ");
+ ret += kapi_json_str(buf + ret, size - ret, spec->notes);
+ ret += scnprintf(buf + ret, size - ret, "\n}\n");
+
+ /* scnprintf() never writes past size - 1, so a full buffer means truncation */
+ if (ret >= size - 1)
+ return -E2BIG;
+
+ return ret;
+}
+EXPORT_SYMBOL_GPL(kapi_export_json);
+
+#ifdef CONFIG_KAPI_RUNTIME_CHECKS
+
+/**
+ * kapi_validate_fd - Validate that a file descriptor value is in valid range
+ * @fd: File descriptor to validate
+ *
+ * Only the numeric range is checked: AT_FDCWD or a non-negative value.
+ * Openness is left to the syscall, since the fd can be closed between check
+ * and use. Other negative values are rejected here, so a syscall such as
+ * close() fails with EINVAL for them rather than EBADF.
+ *
+ * Return: true if fd is in valid range, false otherwise
+ */
+static bool kapi_validate_fd(int fd)
+{
+ return fd == AT_FDCWD || fd >= 0;
+}
+
+/**
+ * kapi_validate_user_ptr - Validate that a user pointer is accessible
+ * @ptr: User pointer to validate
+ * @size: Size in bytes to validate
+ *
+ * Return: true if user memory is accessible, false otherwise
+ */
+static bool kapi_validate_user_ptr(const void __user *ptr, size_t size)
+{
+ /* NULL pointers are not valid; caller handles optional case */
+ if (!ptr)
+ return false;
+
+ return access_ok(ptr, size);
+}
+
+/**
+ * kapi_validate_user_ptr_with_params - Validate user pointer with dynamic size
+ * @param_spec: Parameter specification
+ * @ptr: User pointer to validate
+ * @all_params: Array of all parameter values
+ * @param_count: Number of parameters
+ *
+ * Return: true if user memory is accessible, false otherwise
+ */
+static bool kapi_validate_user_ptr_with_params(const struct kapi_param_spec *param_spec,
+ const void __user *ptr,
+ const s64 *all_params,
+ int param_count)
+{
+ size_t actual_size;
+
+ /* NULL is allowed for optional parameters */
+ if (!ptr && (param_spec->flags & KAPI_PARAM_OPTIONAL))
+ return true;
+
+ /*
+ * size_param_idx is stored 1-based (0 means "no dynamic sizing").
+ * Convert to a real index before looking into all_params.
+ */
+ if (param_spec->size_param_idx > 0 &&
+ param_spec->size_param_idx - 1 < param_count) {
+ s64 count = all_params[param_spec->size_param_idx - 1];
+ size_t unit = param_spec->size_multiplier ?: 1;
+
+ if (count < 0) {
+ pr_warn_ratelimited("Parameter %s: size determinant is negative (%lld)\n",
+ param_spec->name, count);
+ return false;
+ }
+
+ /* A zero-length access never dereferences the pointer */
+ if (count == 0)
+ return true;
+
+ if (count > SIZE_MAX / unit) {
+ pr_warn_ratelimited("Parameter %s: size calculation overflow\n",
+ param_spec->name);
+ return false;
+ }
+
+ actual_size = (size_t)count * unit;
+ } else {
+ actual_size = param_spec->size;
+ }
+
+ return kapi_validate_user_ptr(ptr, actual_size);
+}
+
+/**
+ * kapi_validate_path - Validate that a pathname is accessible and within limits
+ * @path: User pointer to pathname
+ * @param_spec: Parameter specification
+ *
+ * Return: true if path is valid, false otherwise
+ */
+static bool kapi_validate_path(const char __user *path,
+ const struct kapi_param_spec *param_spec)
+{
+ size_t len;
+
+ /* NULL is allowed for optional parameters */
+ if (!path && (param_spec->flags & KAPI_PARAM_OPTIONAL))
+ return true;
+
+ if (!path) {
+ pr_warn_ratelimited("Parameter %s: NULL path not allowed\n", param_spec->name);
+ return false;
+ }
+
+ if (!access_ok(path, 1)) {
+ pr_warn_ratelimited("Parameter %s: path pointer %p not accessible\n",
+ param_spec->name, path);
+ return false;
+ }
+
+ /*
+ * Use strnlen_user to check the path length and accessibility.
+ * Note: strnlen_user() is subject to TOCTOU -- the measured length
+ * may change if another thread modifies the user memory. This is
+ * acceptable since the kernel re-copies and re-validates the path
+ * later in the syscall path. This check is best-effort.
+ */
+ len = strnlen_user(path, PATH_MAX + 1);
+ if (len == 0) {
+ pr_warn_ratelimited("Parameter %s: invalid path pointer %p\n",
+ param_spec->name, path);
+ return false;
+ }
+
+ if (len > PATH_MAX) {
+ pr_warn_ratelimited("Parameter %s: path too long (exceeds PATH_MAX)\n",
+ param_spec->name);
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * kapi_validate_user_string - Validate a userspace null-terminated string
+ * @str: User pointer to string
+ * @param_spec: Parameter specification containing length constraints
+ *
+ * Validates that the userspace string pointer is accessible and that the
+ * string length (excluding null terminator) is within the range specified
+ * by min_value and max_value in the parameter specification.
+ *
+ * Return: true if string is valid, false otherwise
+ */
+static bool kapi_validate_user_string(const char __user *str,
+ const struct kapi_param_spec *param_spec)
+{
+ size_t len;
+ size_t max_check_len;
+
+ /* NULL is allowed for optional parameters */
+ if (!str && (param_spec->flags & KAPI_PARAM_OPTIONAL))
+ return true;
+
+ if (!str) {
+ pr_warn_ratelimited("Parameter %s: NULL string not allowed\n", param_spec->name);
+ return false;
+ }
+
+ if (!access_ok(str, 1)) {
+ pr_warn_ratelimited("Parameter %s: string pointer %p not accessible\n",
+ param_spec->name, str);
+ return false;
+ }
+
+ /*
+ * Use strnlen_user to check the string length and validate accessibility.
+ * Check up to max_value + 1 to detect strings that are too long.
+ * If max_value is 0 or unset, use PATH_MAX as a reasonable default.
+ *
+ * Note: strnlen_user() is subject to TOCTOU -- see comment in
+ * kapi_validate_path() above. This check is best-effort.
+ */
+ max_check_len = param_spec->max_value > 0 ?
+ (size_t)param_spec->max_value + 1 : PATH_MAX + 1;
+ len = strnlen_user(str, max_check_len);
+
+ if (len == 0) {
+ pr_warn_ratelimited("Parameter %s: invalid string pointer %p\n",
+ param_spec->name, str);
+ return false;
+ }
+
+ /*
+ * strnlen_user returns the length including the null terminator.
+ * Convert to string length (excluding terminator) for range check.
+ */
+ len--;
+
+ if (param_spec->min_value > 0 && len < (size_t)param_spec->min_value) {
+ pr_warn_ratelimited("Parameter %s: string too short (%zu < %lld)\n",
+ param_spec->name, len, param_spec->min_value);
+ return false;
+ }
+
+ if (param_spec->max_value > 0 && len > (size_t)param_spec->max_value) {
+ pr_warn_ratelimited("Parameter %s: string too long (%zu > %lld)\n",
+ param_spec->name, len, param_spec->max_value);
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * kapi_validate_user_ptr_constraint - Validate a userspace pointer with size
+ * @ptr: User pointer to validate
+ * @param_spec: Parameter specification containing size
+ *
+ * Validates that the userspace pointer is accessible and that the memory
+ * region of the specified size can be accessed. The size is taken from
+ * the param_spec->size field.
+ *
+ * Return: true if pointer is valid, false otherwise
+ */
+static bool kapi_validate_user_ptr_constraint(const void __user *ptr,
+ const struct kapi_param_spec *param_spec)
+{
+ /* NULL is allowed for optional parameters */
+ if (!ptr && (param_spec->flags & KAPI_PARAM_OPTIONAL))
+ return true;
+
+ if (!ptr) {
+ pr_warn_ratelimited("Parameter %s: NULL pointer not allowed\n", param_spec->name);
+ return false;
+ }
+
+ if (param_spec->size == 0) {
+ pr_warn_ratelimited("Parameter %s: size not specified for user pointer validation\n",
+ param_spec->name);
+ return false;
+ }
+
+ if (!access_ok(ptr, param_spec->size)) {
+ pr_warn_ratelimited("Parameter %s: user pointer %p not accessible for %zu bytes\n",
+ param_spec->name, ptr, param_spec->size);
+ return false;
+ }
+
+ return true;
+}
+
+/*
+ * check_user_ptr is false once the pointer was validated against its dynamic
+ * size; the fixed-size check would reject NULL even when that size is 0.
+ */
+static bool kapi_validate_param_checks(const struct kapi_param_spec *param_spec,
+ s64 value, bool check_user_ptr)
+{
+ int i;
+
+ /* Special handling for file descriptor type */
+ if (param_spec->type == KAPI_TYPE_FD &&
+ !(param_spec->flags & KAPI_PARAM_OPTIONAL)) {
+ if (value < INT_MIN || value > INT_MAX) {
+ pr_warn_ratelimited("Parameter %s: file descriptor %lld out of int range\n",
+ param_spec->name, value);
+ return false;
+ }
+ if (!kapi_validate_fd((int)value)) {
+ pr_warn_ratelimited("Parameter %s: invalid file descriptor %lld\n",
+ param_spec->name, value);
+ return false;
+ }
+ }
+
+ /* Special handling for user pointer type */
+ if (check_user_ptr && param_spec->type == KAPI_TYPE_USER_PTR) {
+ const void __user *ptr = (const void __user *)(unsigned long)value;
+
+ /* NULL is allowed for optional parameters */
+ if (!ptr && (param_spec->flags & KAPI_PARAM_OPTIONAL))
+ return true;
+
+ if (!kapi_validate_user_ptr(ptr, param_spec->size)) {
+ pr_warn_ratelimited("Parameter %s: invalid user pointer %p (size: %zu)\n",
+ param_spec->name, ptr, param_spec->size);
+ return false;
+ }
+ }
+
+ /* Special handling for path type */
+ if (param_spec->type == KAPI_TYPE_PATH) {
+ const char __user *path = (const char __user *)(unsigned long)value;
+
+ if (!kapi_validate_path(path, param_spec))
+ return false;
+ }
+
+ switch (param_spec->constraint_type) {
+ case KAPI_CONSTRAINT_NONE:
+ case KAPI_CONSTRAINT_BUFFER:
+ return true;
+
+ case KAPI_CONSTRAINT_RANGE:
+ /*
+ * If max_value is below min_value, it was likely set from an
+ * unsigned constant (e.g. SIZE_MAX) that overflowed s64. Treat
+ * as no upper bound; only check the minimum.
+ */
+ if (param_spec->max_value >= param_spec->min_value) {
+ if (value < param_spec->min_value ||
+ value > param_spec->max_value) {
+ pr_warn_ratelimited("Parameter %s value %lld out of range [%lld, %lld]\n",
+ param_spec->name, value,
+ param_spec->min_value,
+ param_spec->max_value);
+ return false;
+ }
+ } else {
+ if (value < param_spec->min_value) {
+ pr_warn_ratelimited("Parameter %s value %lld below minimum %lld\n",
+ param_spec->name, value,
+ param_spec->min_value);
+ return false;
+ }
+ }
+ return true;
+
+ case KAPI_CONSTRAINT_MASK:
+ if (value & ~param_spec->valid_mask) {
+ pr_warn_ratelimited("Parameter %s value 0x%llx contains invalid bits (valid mask: 0x%llx)\n",
+ param_spec->name, value, param_spec->valid_mask);
+ return false;
+ }
+ return true;
+
+ case KAPI_CONSTRAINT_ENUM:
+ if (!param_spec->enum_values || param_spec->enum_count == 0)
+ return true;
+
+ for (i = 0; i < param_spec->enum_count; i++) {
+ if (value == param_spec->enum_values[i])
+ return true;
+ }
+ pr_warn_ratelimited("Parameter %s value %lld not in valid enumeration\n",
+ param_spec->name, value);
+ return false;
+
+ case KAPI_CONSTRAINT_ALIGNMENT:
+ if (param_spec->alignment == 0) {
+ pr_warn_ratelimited("Parameter %s: alignment constraint specified but alignment is 0\n",
+ param_spec->name);
+ return false;
+ }
+ if (param_spec->alignment & (param_spec->alignment - 1)) {
+ pr_warn_ratelimited("Parameter %s: alignment %zu is not a power of two\n",
+ param_spec->name, param_spec->alignment);
+ return false;
+ }
+ if (value & (param_spec->alignment - 1)) {
+ pr_warn_ratelimited("Parameter %s value 0x%llx not aligned to %zu boundary\n",
+ param_spec->name, value, param_spec->alignment);
+ return false;
+ }
+ return true;
+
+ case KAPI_CONSTRAINT_POWER_OF_TWO:
+ if (value == 0 || (value & (value - 1))) {
+ pr_warn_ratelimited("Parameter %s value %lld is not a power of two\n",
+ param_spec->name, value);
+ return false;
+ }
+ return true;
+
+ case KAPI_CONSTRAINT_PAGE_ALIGNED:
+ if (value & (PAGE_SIZE - 1)) {
+ pr_warn_ratelimited("Parameter %s value 0x%llx not page-aligned (PAGE_SIZE=%ld)\n",
+ param_spec->name, value, PAGE_SIZE);
+ return false;
+ }
+ return true;
+
+ case KAPI_CONSTRAINT_NONZERO:
+ if (value == 0) {
+ pr_warn_ratelimited("Parameter %s must be non-zero\n", param_spec->name);
+ return false;
+ }
+ return true;
+
+ case KAPI_CONSTRAINT_USER_STRING:
+ return kapi_validate_user_string((const char __user *)(unsigned long)value,
+ param_spec);
+
+ case KAPI_CONSTRAINT_USER_PATH:
+ return kapi_validate_path((const char __user *)(unsigned long)value, param_spec);
+
+ case KAPI_CONSTRAINT_USER_PTR:
+ return kapi_validate_user_ptr_constraint((const void __user *)(unsigned long)value,
+ param_spec);
+
+ case KAPI_CONSTRAINT_CUSTOM:
+ if (param_spec->validate)
+ return param_spec->validate(value);
+ return true;
+
+ default:
+ return true;
+ }
+}
+
+/**
+ * kapi_validate_param - Validate a parameter against its specification
+ * @param_spec: Parameter specification
+ * @value: Parameter value to validate
+ *
+ * Return: true if valid, false otherwise
+ */
+bool kapi_validate_param(const struct kapi_param_spec *param_spec, s64 value)
+{
+ return kapi_validate_param_checks(param_spec, value, true);
+}
+EXPORT_SYMBOL_GPL(kapi_validate_param);
+
+/**
+ * kapi_validate_param_with_context - Validate parameter with access to all params
+ * @param_spec: Parameter specification
+ * @value: Parameter value to validate
+ * @all_params: Array of all parameter values
+ * @param_count: Number of parameters
+ *
+ * Return: true if valid, false otherwise
+ */
+bool kapi_validate_param_with_context(const struct kapi_param_spec *param_spec,
+ s64 value, const s64 *all_params, int param_count)
+{
+ /* Special handling for user pointer type with dynamic sizing */
+ if (param_spec->type == KAPI_TYPE_USER_PTR) {
+ const void __user *ptr = (const void __user *)(unsigned long)value;
+
+ /* NULL is allowed for optional parameters */
+ if (!ptr && (param_spec->flags & KAPI_PARAM_OPTIONAL))
+ return true;
+
+ if (!kapi_validate_user_ptr_with_params(param_spec, ptr, all_params, param_count)) {
+ pr_warn_ratelimited("Parameter %s: invalid user pointer %p\n",
+ param_spec->name, ptr);
+ return false;
+ }
+ return kapi_validate_param_checks(param_spec, value, false);
+ }
+
+ /* For other types, fall back to regular validation */
+ return kapi_validate_param(param_spec, value);
+}
+EXPORT_SYMBOL_GPL(kapi_validate_param_with_context);
+
+/**
+ * kapi_validate_syscall_params - Validate all syscall parameters together
+ * @spec: API specification
+ * @params: Array of parameter values
+ * @param_count: Number of parameters
+ *
+ * Return: -EINVAL if any parameter is invalid, 0 if all valid
+ */
+int kapi_validate_syscall_params(const struct kernel_api_spec *spec,
+ const s64 *params, int param_count)
+{
+ int i;
+
+ if (!spec || !params)
+ return 0;
+
+ /* Validate that we have the expected number of parameters */
+ if (param_count != spec->param_count) {
+ pr_warn_ratelimited("API %s: parameter count mismatch (expected %u, got %d)\n",
+ spec->name, spec->param_count, param_count);
+ return -EINVAL;
+ }
+
+ /* Validate each parameter with context */
+ for (i = 0; i < spec->param_count && i < KAPI_MAX_PARAMS; i++) {
+ const struct kapi_param_spec *param_spec = &spec->params[i];
+
+ if (!kapi_validate_param_with_context(param_spec, params[i], params, param_count)) {
+ if (strncmp(spec->name, "sys_", 4) == 0) {
+ /* For syscalls, we can return EINVAL to userspace */
+ return -EINVAL;
+ }
+ }
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(kapi_validate_syscall_params);
+
+/**
+ * kapi_check_return_success - Check if return value indicates success
+ * @return_spec: Return specification
+ * @retval: Return value to check
+ *
+ * Return: true if the return value indicates success according to the spec.
+ */
+bool kapi_check_return_success(const struct kapi_return_spec *return_spec, s64 retval)
+{
+ u32 i;
+
+ if (!return_spec)
+ return true;
+
+ switch (return_spec->check_type) {
+ case KAPI_RETURN_EXACT:
+ return retval == return_spec->success_value;
+
+ case KAPI_RETURN_RANGE:
+ return retval >= return_spec->success_min &&
+ retval <= return_spec->success_max;
+
+ case KAPI_RETURN_ERROR_CHECK:
+ /* Success if NOT in error list */
+ if (return_spec->error_values) {
+ for (i = 0; i < return_spec->error_count; i++) {
+ if (retval == return_spec->error_values[i])
+ return false;
+ }
+ }
+ return true;
+
+ case KAPI_RETURN_FD:
+ /* File descriptors: >= 0 is success, < 0 is error */
+ return retval >= 0;
+
+ case KAPI_RETURN_CUSTOM:
+ if (return_spec->is_success)
+ return return_spec->is_success(retval);
+ fallthrough;
+
+ default:
+ return true;
+ }
+}
+EXPORT_SYMBOL_GPL(kapi_check_return_success);
+
+/**
+ * kapi_validate_return_value - Validate that return value matches spec
+ * @spec: API specification
+ * @retval: Return value to validate
+ *
+ * Return: false if the spec is a KAPI_RETURN_FD check and a successful @retval
+ * is not a valid file descriptor, true otherwise.
+ *
+ * kapi_check_return_success() runs first. A value that does not satisfy it is
+ * treated as an error and is accepted. An error code that is not listed in the
+ * spec is only logged with pr_debug().
+ */
+bool kapi_validate_return_value(const struct kernel_api_spec *spec, s64 retval)
+{
+ int i;
+ bool is_success;
+
+ if (!spec)
+ return true; /* No spec means we can't validate */
+
+ /* First check if this is a success return */
+ is_success = kapi_check_return_success(&spec->return_spec, retval);
+
+ if (is_success) {
+ /* Special validation for file descriptor returns */
+ if (spec->return_spec.check_type == KAPI_RETURN_FD) {
+ if (retval > INT_MAX || !kapi_validate_fd((int)retval)) {
+ pr_warn_ratelimited("API %s returned invalid file descriptor %lld\n",
+ spec->name, retval);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ if (spec->error_count == 0) {
+ pr_debug("API %s returned unspecified error %lld\n",
+ spec->name, retval);
+ return true;
+ }
+
+ for (i = 0; i < spec->error_count && i < KAPI_MAX_ERRORS; i++) {
+ if (retval == spec->errors[i].error_code)
+ return true;
+ }
+
+ /*
+ * Error not in spec - log at debug level since filesystem-specific and
+ * device-specific error codes may not be exhaustively listed.
+ */
+ pr_debug("API %s returned error code %lld not listed in spec\n",
+ spec->name, retval);
+
+ return true;
+}
+EXPORT_SYMBOL_GPL(kapi_validate_return_value);
+
+/**
+ * kapi_validate_syscall_return - Validate syscall return value
+ * @spec: API specification
+ * @retval: Return value
+ *
+ * Return: always 0. A return value that does not match the spec is only
+ * logged and the syscall result is left unchanged.
+ */
+int kapi_validate_syscall_return(const struct kernel_api_spec *spec, s64 retval)
+{
+ if (!spec)
+ return 0;
+
+ /* Skip return validation if return spec was not defined */
+ if (spec->return_magic != KAPI_MAGIC_RETURN)
+ return 0;
+
+ if (!kapi_validate_return_value(spec, retval)) {
+ /* Log the violation but don't change the return value */
+ pr_warn_ratelimited("KAPI: Syscall %s returned unspecified value %lld\n",
+ spec->name, retval);
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(kapi_validate_syscall_return);
+
+/**
+ * kapi_check_context - Check if current context matches API requirements
+ * @spec: API specification to check against
+ */
+void kapi_check_context(const struct kernel_api_spec *spec)
+{
+ bool valid = false;
+ u32 ctx;
+
+ if (!spec)
+ return;
+
+ ctx = spec->context_flags;
+
+ if (!ctx)
+ return;
+
+ /* Check if we're in an allowed context */
+ if ((ctx & KAPI_CTX_PROCESS) && !in_interrupt())
+ valid = true;
+
+ if ((ctx & KAPI_CTX_SOFTIRQ) && in_softirq())
+ valid = true;
+
+ if ((ctx & KAPI_CTX_HARDIRQ) && in_hardirq())
+ valid = true;
+
+ if ((ctx & KAPI_CTX_NMI) && in_nmi())
+ valid = true;
+
+ if (!valid)
+ WARN_ONCE(1, "API %s called from invalid context\n", spec->name);
+
+ /* Check specific requirements */
+ if ((ctx & KAPI_CTX_ATOMIC) && preemptible())
+ WARN_ONCE(1, "API %s requires atomic context\n", spec->name);
+
+ if ((ctx & KAPI_CTX_SLEEPABLE) && !preemptible())
+ WARN_ONCE(1, "API %s requires sleepable context\n", spec->name);
+}
+EXPORT_SYMBOL_GPL(kapi_check_context);
+
+#endif /* CONFIG_KAPI_RUNTIME_CHECKS */
--
2.53.0
^ permalink raw reply related [flat|nested] 21+ messages in thread* [PATCH v5 02/11] kernel/api: enable kerneldoc-based API specifications
2026-10-08 8:49 [PATCH v5 00/11] Kernel API Specification Framework Sasha Levin
2026-10-08 8:49 ` [PATCH v5 01/11] kernel/api: introduce kernel API specification framework Sasha Levin
@ 2026-10-08 8:49 ` Sasha Levin
2026-10-08 8:49 ` [PATCH v5 03/11] kernel/api: add debugfs interface for kernel " Sasha Levin
` (8 subsequent siblings)
10 siblings, 0 replies; 21+ messages in thread
From: Sasha Levin @ 2026-10-08 8:49 UTC (permalink / raw)
To: linux-api, linux-kernel
Cc: Sasha Levin, 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, Gabriele Paoloni, Mauro Carvalho Chehab,
Christian Brauner, Alexander Viro, Andrew Morton, Masahiro Yamada,
Shuah Khan, Arnd Bergmann, Nathan Chancellor, Steven Rostedt,
Masami Hiramatsu, Mathieu Desnoyers
Add support for extracting API specifications from kernel-doc comments
and generating C macro invocations for the kernel API specification
framework.
Changes include:
- New kdoc_apispec.py module for generating API spec macros
- Updates to tools/docs/kernel-doc to support the -apispec output format
- Build system integration in Makefile.build
- Support for API-specific sections in kernel-doc comments
kernel-doc recognises the KAPI sections only in -apispec mode, so its
other output formats are unchanged. With CONFIG_KAPI_SPEC=y, Kbuild
generates <file>.apispec.h for each built source that has a contexts:
or context-flags: line plus another KAPI section, and force-includes it
when compiling that source.
Assisted-by: LLM
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
.gitignore | 1 +
Documentation/dev-tools/kernel-api-spec.rst | 24 +
Makefile | 1 +
scripts/Makefile.build | 26 +
tools/docs/kernel-doc | 12 +-
tools/lib/python/kdoc/kdoc_apispec.py | 1391 +++++++++++++++++++
tools/lib/python/kdoc/kdoc_files.py | 12 +-
tools/lib/python/kdoc/kdoc_parser.py | 101 +-
8 files changed, 1558 insertions(+), 10 deletions(-)
create mode 100644 tools/lib/python/kdoc/kdoc_apispec.py
diff --git a/.gitignore b/.gitignore
index 9875120ea7bde..37e6cd1d83fc9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@
#
.*
*.a
+*.apispec.h
*.asn1.[ch]
*.bc
*.bin
diff --git a/Documentation/dev-tools/kernel-api-spec.rst b/Documentation/dev-tools/kernel-api-spec.rst
index 4fb412b2720d3..03b32d3718c8e 100644
--- a/Documentation/dev-tools/kernel-api-spec.rst
+++ b/Documentation/dev-tools/kernel-api-spec.rst
@@ -180,6 +180,19 @@ DSL reference:
from ``range:``. The function-call
form populates the matching aux fields
(``range:`` / ``valid-mask:`` / ``size-param:``).
+* ``arch-mask:`` — extends a ``mask(...)`` constraint with bits that
+ are valid only on a named architecture. The form is
+ ``arch-mask: <arch> = <bits-expr>`` and may appear multiple times.
+ The named arch must be one of the known short names
+ (``alpha``, ``arc``, ``arm``, ``arm64``, ``csky``, ``hexagon``,
+ ``loongarch``, ``m68k``, ``microblaze``, ``mips``, ``nios2``,
+ ``openrisc``, ``parisc``, ``powerpc``, ``riscv``, ``s390``, ``sh``,
+ ``sparc``, ``um``, ``x86``, ``xtensa``); the generator translates it
+ to the matching ``CONFIG_*`` symbol and emits the bits inside an
+ ``#ifdef`` so a single generated apispec.h compiles on every
+ architecture and folds in the arch-specific bits at compile time.
+ Use this for PROT/MAP bits whose UAPI symbols are defined only on
+ the matching arch.
* ``lock: … type:`` accepts ``mutex``, ``spinlock``, ``rwlock``,
``seqlock``, ``rcu``, ``semaphore``, ``custom`` or ``KAPI_LOCK_*``.
* ``signal: … direction:`` accepts ``receive``, ``send``, ``handle``,
@@ -266,6 +279,17 @@ The execution context recorded in a specification is not checked at runtime.
The option is available on x86 and on architectures that use the generic
``__SYSCALL_DEFINEx()``.
+.. warning::
+
+ Userspace errno is affected when this option is on. For syscalls that
+ violate their parameter specification, KAPI short-circuits the call and
+ returns ``-EINVAL`` from the validator **before** the real handler runs.
+ That errno can differ from what the real handler would have produced for
+ the same condition (for example, ``-ENOMEM`` from an allocation path or
+ ``-EFAULT`` from a deeper copy-in). ``CONFIG_KAPI_RUNTIME_CHECKS`` is a
+ debug-only option; do not enable it on production kernels or in
+ userspace-visible test environments where error-code fidelity matters.
+
Custom Validators
-----------------
diff --git a/Makefile b/Makefile
index 7c855e6fe5448..fd7917f78fa45 100644
--- a/Makefile
+++ b/Makefile
@@ -2251,6 +2251,7 @@ clean: $(clean-dirs)
-o -name '*.ll' \
-o -name '*.gcno' \
-o -name '*.long-type-*.txt' \
+ -o -name '*.apispec.h' \
\) -type f -print \
-o -name '.tmp_*' -print \
| xargs rm -rf
diff --git a/scripts/Makefile.build b/scripts/Makefile.build
index 4349108e75e1f..6fa9ae47fb2c8 100644
--- a/scripts/Makefile.build
+++ b/scripts/Makefile.build
@@ -175,6 +175,32 @@ ifneq ($(KBUILD_EXTRA_WARN),)
endif
endif
+ifeq ($(CONFIG_KAPI_SPEC),y)
+# kernel-doc -apispec emits a spec only for comments with two or more KAPI
+# sections; pick files with a contexts: (or context-flags:) line and one more
+# KAPI section.
+has-apispec = $(if $(strip $(1)),$(shell \
+ grep -lE '^[[:space:]]*\*[[:space:]]*(contexts|context-flags):' $(1) 2>/dev/null | \
+ xargs -r grep -lE '^[[:space:]]*\*[[:space:]]*(api-type|param|error|capability|signal|lock|state-trans|constraint|side-effect|long-desc):'))
+apispec-c-files := $(call has-apispec, \
+ $(patsubst $(obj)/%.o,$(src)/%.c, \
+ $(filter-out %/built-in.a,$(real-obj-y))))
+apispec-y := $(patsubst $(src)/%.c,$(obj)/%.apispec.h,$(apispec-c-files))
+always-y += $(apispec-y)
+targets += $(apispec-y)
+
+quiet_cmd_apispec = APISPEC $@
+ cmd_apispec = PYTHONDONTWRITEBYTECODE=1 $(PYTHON3) $(KERNELDOC) -apispec \
+ $(KDOCFLAGS) $< > $@
+
+$(obj)/%.apispec.h: $(src)/%.c $(KERNELDOC) \
+ $(wildcard $(srctree)/tools/lib/python/kdoc/*.py) FORCE
+ $(call if_changed,apispec)
+
+$(apispec-y:.apispec.h=.o): $(obj)/%.o: $(obj)/%.apispec.h
+$(apispec-y:.apispec.h=.o): private c_flags += -include $(obj)/$*.apispec.h
+endif
+
# Compile C sources (.c)
# ---------------------------------------------------------------------------
diff --git a/tools/docs/kernel-doc b/tools/docs/kernel-doc
index d9192c3f1645e..4c45c0d6cfa94 100755
--- a/tools/docs/kernel-doc
+++ b/tools/docs/kernel-doc
@@ -250,6 +250,8 @@ def main():
help="Output reStructuredText format (default).")
out_fmt.add_argument("-N", "-none", "--none", action="store_true",
help="Do not output documentation, only warnings.")
+ out_fmt.add_argument("-apispec", "--apispec", action="store_true",
+ help="Output C macro invocations for kernel API specifications.")
out_fmt.add_argument("-y", "--yaml-file", "--yaml",
help="Stores kernel-doc output on a yaml file.")
@@ -326,6 +328,7 @@ def main():
#
from kdoc.kdoc_files import KernelFiles # pylint: disable=C0415
from kdoc.kdoc_output import RestFormat, ManFormat # pylint: disable=C0415
+ from kdoc.kdoc_apispec import ApiSpecFormat # pylint: disable=C0415
yaml_content = set()
if args.yaml_file:
@@ -351,12 +354,16 @@ def main():
out_style = None
n_outputs += 1
+ if args.apispec:
+ out_style = ApiSpecFormat()
+ n_outputs += 1
+
if args.rst or n_outputs == 0:
n_outputs += 1
out_style = RestFormat()
if n_outputs > 1:
- parser.error("Those arguments are muttually exclusive: --man, --rst, --none, except when generating a YAML file.")
+ parser.error("Those arguments are muttually exclusive: --man, --rst, --none, --apispec, except when generating a YAML file.")
elif not n_outputs:
out_style = RestFormat()
@@ -365,7 +372,8 @@ def main():
yaml_file=args.yaml_file, yaml_content=yaml_content,
out_style=out_style, werror=args.werror,
wreturn=args.wreturn, wshort_desc=args.wshort_desc,
- wcontents_before_sections=args.wcontents_before_sections)
+ wcontents_before_sections=args.wcontents_before_sections,
+ apispec=args.apispec)
kfiles.parse(args.files, export_file=args.export_file)
diff --git a/tools/lib/python/kdoc/kdoc_apispec.py b/tools/lib/python/kdoc/kdoc_apispec.py
new file mode 100644
index 0000000000000..fb8e243a7b77d
--- /dev/null
+++ b/tools/lib/python/kdoc/kdoc_apispec.py
@@ -0,0 +1,1391 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+
+"""
+Generate C macro invocations for kernel API specifications from kernel-doc comments.
+
+This module creates C header files with API specification macros that match
+the kernel API specification framework in include/linux/kernel_api_spec.h.
+"""
+
+from kdoc.kdoc_output import OutputFormat
+import re
+import sys
+
+
+# Valid KAPI effect types
+VALID_EFFECT_TYPES = {
+ 'KAPI_EFFECT_NONE', 'KAPI_EFFECT_MODIFY_STATE', 'KAPI_EFFECT_PROCESS_STATE',
+ 'KAPI_EFFECT_IRREVERSIBLE', 'KAPI_EFFECT_SCHEDULE', 'KAPI_EFFECT_FILESYSTEM',
+ 'KAPI_EFFECT_HARDWARE', 'KAPI_EFFECT_ALLOC_MEMORY', 'KAPI_EFFECT_FREE_MEMORY',
+ 'KAPI_EFFECT_SIGNAL_SEND', 'KAPI_EFFECT_FILE_POSITION', 'KAPI_EFFECT_LOCK_ACQUIRE',
+ 'KAPI_EFFECT_LOCK_RELEASE', 'KAPI_EFFECT_RESOURCE_CREATE', 'KAPI_EFFECT_RESOURCE_DESTROY',
+ 'KAPI_EFFECT_NETWORK'
+}
+
+# DSL aliases mapping short tokens to their canonical KAPI_* C
+# identifier. Unknown tokens pass through unchanged.
+_CTX_ALIASES = {
+ 'process': 'KAPI_CTX_PROCESS',
+ 'softirq': 'KAPI_CTX_SOFTIRQ',
+ 'hardirq': 'KAPI_CTX_HARDIRQ',
+ 'nmi': 'KAPI_CTX_NMI',
+ 'atomic': 'KAPI_CTX_ATOMIC',
+ 'sleepable': 'KAPI_CTX_SLEEPABLE',
+ 'preempt_disabled': 'KAPI_CTX_PREEMPT_DISABLED',
+ 'irq_disabled': 'KAPI_CTX_IRQ_DISABLED',
+}
+_TYPE_ALIASES = {
+ 'int': 'KAPI_TYPE_INT',
+ 'uint': 'KAPI_TYPE_UINT',
+ 'ptr': 'KAPI_TYPE_PTR',
+ 'struct': 'KAPI_TYPE_STRUCT',
+ 'union': 'KAPI_TYPE_UNION',
+ 'enum': 'KAPI_TYPE_ENUM',
+ 'func_ptr': 'KAPI_TYPE_FUNC_PTR',
+ 'array': 'KAPI_TYPE_ARRAY',
+ 'fd': 'KAPI_TYPE_FD',
+ 'user_ptr': 'KAPI_TYPE_USER_PTR',
+ 'uptr': 'KAPI_TYPE_USER_PTR',
+ 'path': 'KAPI_TYPE_PATH',
+ 'custom': 'KAPI_TYPE_CUSTOM',
+}
+_FLAG_ALIASES = {
+ 'input': 'KAPI_PARAM_IN',
+ 'in': 'KAPI_PARAM_IN',
+ 'output': 'KAPI_PARAM_OUT',
+ 'out': 'KAPI_PARAM_OUT',
+ 'inout': 'KAPI_PARAM_INOUT',
+ 'optional': 'KAPI_PARAM_OPTIONAL',
+ 'const': 'KAPI_PARAM_CONST',
+ 'volatile': 'KAPI_PARAM_VOLATILE',
+ 'user': 'KAPI_PARAM_USER',
+ 'dma': 'KAPI_PARAM_DMA',
+ 'aligned': 'KAPI_PARAM_ALIGNED',
+}
+
+
+def _canon_token(tok, table):
+ """Look up `tok` (case-insensitive) in `table`. Unknown tokens
+ pass through verbatim."""
+ t = tok.strip()
+ if not t:
+ return ''
+ return table.get(t.lower(), t)
+
+
+def _canon_context_expr(expr):
+ """Canonicalise a context flag expression. Accepts '|'- or
+ ','-joined tokens; returns a '|'-joined string of KAPI_CTX_*
+ identifiers ready for KAPI_CONTEXT()."""
+ if not expr:
+ return expr
+ sep = ',' if ',' in expr and '|' not in expr else '|'
+ tokens = [_canon_token(t, _CTX_ALIASES) for t in expr.split(sep)]
+ return ' | '.join(t for t in tokens if t)
+
+
+def _canon_flags_expr(expr):
+ """Canonicalise a parameter flags expression. Accepts '|'- or
+ ','-joined KAPI_PARAM_* tokens or their aliases; returns a
+ '|'-joined canonical string."""
+ if not expr:
+ return expr
+ sep = ',' if ',' in expr and '|' not in expr else '|'
+ tokens = [_canon_token(t, _FLAG_ALIASES) for t in expr.split(sep)]
+ return ' | '.join(t for t in tokens if t)
+
+
+# Alias tables for enum families used as block-attribute values
+# (lock type, signal direction/action/timing, return check type) and
+# for the top-level `side-effect:` bitmask.
+_LOCK_TYPE_ALIASES = {
+ 'none': 'KAPI_LOCK_NONE',
+ 'mutex': 'KAPI_LOCK_MUTEX',
+ 'spinlock': 'KAPI_LOCK_SPINLOCK',
+ 'rwlock': 'KAPI_LOCK_RWLOCK',
+ 'seqlock': 'KAPI_LOCK_SEQLOCK',
+ 'rcu': 'KAPI_LOCK_RCU',
+ 'semaphore': 'KAPI_LOCK_SEMAPHORE',
+ 'custom': 'KAPI_LOCK_CUSTOM',
+}
+_SIGNAL_DIR_ALIASES = {
+ 'receive': 'KAPI_SIGNAL_RECEIVE',
+ 'send': 'KAPI_SIGNAL_SEND',
+ 'handle': 'KAPI_SIGNAL_HANDLE',
+ 'block': 'KAPI_SIGNAL_BLOCK',
+ 'ignore': 'KAPI_SIGNAL_IGNORE',
+}
+_SIGNAL_ACTION_ALIASES = {
+ 'default': 'KAPI_SIGNAL_ACTION_DEFAULT',
+ 'terminate': 'KAPI_SIGNAL_ACTION_TERMINATE',
+ 'coredump': 'KAPI_SIGNAL_ACTION_COREDUMP',
+ 'stop': 'KAPI_SIGNAL_ACTION_STOP',
+ 'continue': 'KAPI_SIGNAL_ACTION_CONTINUE',
+ 'custom': 'KAPI_SIGNAL_ACTION_CUSTOM',
+ 'return': 'KAPI_SIGNAL_ACTION_RETURN',
+ 'restart': 'KAPI_SIGNAL_ACTION_RESTART',
+ 'queue': 'KAPI_SIGNAL_ACTION_QUEUE',
+ 'discard': 'KAPI_SIGNAL_ACTION_DISCARD',
+ 'transform': 'KAPI_SIGNAL_ACTION_TRANSFORM',
+}
+_SIGNAL_TIMING_ALIASES = {
+ 'before': 'KAPI_SIGNAL_TIME_BEFORE',
+ 'during': 'KAPI_SIGNAL_TIME_DURING',
+ 'after': 'KAPI_SIGNAL_TIME_AFTER',
+}
+_EFFECT_ALIASES = {
+ 'none': 'KAPI_EFFECT_NONE',
+ 'alloc_memory': 'KAPI_EFFECT_ALLOC_MEMORY',
+ 'free_memory': 'KAPI_EFFECT_FREE_MEMORY',
+ 'modify_state': 'KAPI_EFFECT_MODIFY_STATE',
+ 'signal_send': 'KAPI_EFFECT_SIGNAL_SEND',
+ 'file_position': 'KAPI_EFFECT_FILE_POSITION',
+ 'lock_acquire': 'KAPI_EFFECT_LOCK_ACQUIRE',
+ 'lock_release': 'KAPI_EFFECT_LOCK_RELEASE',
+ 'resource_create': 'KAPI_EFFECT_RESOURCE_CREATE',
+ 'resource_destroy': 'KAPI_EFFECT_RESOURCE_DESTROY',
+ 'schedule': 'KAPI_EFFECT_SCHEDULE',
+ 'hardware': 'KAPI_EFFECT_HARDWARE',
+ 'network': 'KAPI_EFFECT_NETWORK',
+ 'filesystem': 'KAPI_EFFECT_FILESYSTEM',
+ 'process_state': 'KAPI_EFFECT_PROCESS_STATE',
+ 'irreversible': 'KAPI_EFFECT_IRREVERSIBLE',
+}
+_RETURN_CHECK_ALIASES = {
+ 'exact': 'KAPI_RETURN_EXACT',
+ 'range': 'KAPI_RETURN_RANGE',
+ 'error_check': 'KAPI_RETURN_ERROR_CHECK',
+ 'fd': 'KAPI_RETURN_FD',
+ 'custom': 'KAPI_RETURN_CUSTOM',
+ 'no_return': 'KAPI_RETURN_NO_RETURN',
+}
+
+# Mapping from short architecture name (as used under arch/<name>/) to the
+# kernel CONFIG_* symbol that selects that architecture. Used by the
+# `arch-mask:` DSL form to wrap arch-specific mask bits in #ifdef so a
+# single generated apispec.h compiles on every architecture and folds
+# the right bits into the mask at compile time.
+_ARCH_CONFIG = {
+ 'alpha': 'CONFIG_ALPHA',
+ 'arc': 'CONFIG_ARC',
+ 'arm': 'CONFIG_ARM',
+ 'arm64': 'CONFIG_ARM64',
+ 'csky': 'CONFIG_CSKY',
+ 'hexagon': 'CONFIG_HEXAGON',
+ 'loongarch': 'CONFIG_LOONGARCH',
+ 'm68k': 'CONFIG_M68K',
+ 'microblaze': 'CONFIG_MICROBLAZE',
+ 'mips': 'CONFIG_MIPS',
+ 'nios2': 'CONFIG_NIOS2',
+ 'openrisc': 'CONFIG_OPENRISC',
+ 'parisc': 'CONFIG_PARISC',
+ 'powerpc': 'CONFIG_PPC',
+ 'riscv': 'CONFIG_RISCV',
+ 's390': 'CONFIG_S390',
+ 'sh': 'CONFIG_SUPERH',
+ 'sparc': 'CONFIG_SPARC',
+ 'um': 'CONFIG_UML',
+ 'x86': 'CONFIG_X86',
+ 'xtensa': 'CONFIG_XTENSA',
+}
+
+
+def _canon_bitmask_expr(expr, table):
+ """Canonicalise a bitmask expression (e.g. signal direction/timing,
+ side-effect flags). Accepts `|`- or `,`-joined tokens and returns a
+ `|`-joined canonical KAPI_* string."""
+ if not expr:
+ return expr
+ sep = ',' if ',' in expr and '|' not in expr else '|'
+ tokens = [_canon_token(t, table) for t in expr.split(sep)]
+ return ' | '.join(t for t in tokens if t)
+
+
+# Types that carry user-space pointer semantics. A param with one of
+# these types implicitly gets KAPI_PARAM_USER.
+_IMPLIES_USER_FLAG = {'KAPI_TYPE_USER_PTR', 'KAPI_TYPE_PATH'}
+
+
+def _split_type_line(value):
+ """Split a 'type:' line into (type, [flags...]).
+
+ Accepts a single-token value (e.g. 'KAPI_TYPE_UINT' or 'uint')
+ leaving flags empty, or a comma-separated form
+ (e.g. 'uint, input, user') where the first token is the type and
+ subsequent tokens are flag aliases.
+
+ When the type is user-space (user_ptr, path), KAPI_PARAM_USER is
+ added to the flags list if not already present."""
+ parts = [p.strip() for p in value.split(',') if p.strip()]
+ if not parts:
+ return None, []
+ ty = _canon_token(parts[0], _TYPE_ALIASES)
+ flags = [_canon_token(f, _FLAG_ALIASES) for f in parts[1:]]
+ if ty in _IMPLIES_USER_FLAG and 'KAPI_PARAM_USER' not in flags:
+ flags.append('KAPI_PARAM_USER')
+ return ty, flags
+
+
+def _split_constraint_expr(value):
+ """Parse a constraint expression into (canonical_type, extras).
+
+ Shapes:
+ NAME e.g. 'user_path', 'nonzero'
+ NAME ( ARG (, ARG)* ) e.g. 'range(0, 4096)', 'buffer(2)'
+
+ Returns None for free text. Otherwise returns
+ (constraint_type, {aux_field: value, ...}) where the aux fields map
+ onto the matching param-range / param-mask / param-size /
+ param-enum-values / param-constraint slots.
+ """
+ t = value.strip()
+ if not t:
+ return None
+ # Split NAME ( ARGS )
+ lp = t.find('(')
+ rp = t.rfind(')')
+ if lp > 0 and rp > lp:
+ name = t[:lp].strip()
+ args_raw = t[lp + 1:rp].strip()
+ elif lp < 0:
+ name = t
+ args_raw = None
+ else:
+ return None
+ # Bareword must be a single identifier; multi-word values are free text.
+ if not name or any(c.isspace() for c in name):
+ return None
+ key = name.lower()
+ table = {
+ 'range': ('KAPI_CONSTRAINT_RANGE', 'param-range'),
+ 'mask': ('KAPI_CONSTRAINT_MASK', 'param-mask'),
+ 'enum': ('KAPI_CONSTRAINT_ENUM', 'param-enum-values'),
+ 'alignment': ('KAPI_CONSTRAINT_ALIGNMENT', 'param-alignment'),
+ 'align': ('KAPI_CONSTRAINT_ALIGNMENT', 'param-alignment'),
+ 'power_of_two': ('KAPI_CONSTRAINT_POWER_OF_TWO', None),
+ 'page_aligned': ('KAPI_CONSTRAINT_PAGE_ALIGNED', None),
+ 'nonzero': ('KAPI_CONSTRAINT_NONZERO', None),
+ 'user_string': ('KAPI_CONSTRAINT_USER_STRING', 'param-size'),
+ 'user_path': ('KAPI_CONSTRAINT_USER_PATH', None),
+ 'user_ptr': ('KAPI_CONSTRAINT_USER_PTR', None),
+ 'buffer': ('KAPI_CONSTRAINT_BUFFER', 'param-size-param'),
+ 'custom': ('KAPI_CONSTRAINT_CUSTOM', 'param-constraint'),
+ }
+ if key not in table:
+ return None
+ ctype, aux_key = table[key]
+ extras = {}
+ if aux_key and args_raw is not None:
+ extras[aux_key] = args_raw
+ return ctype, extras
+
+
+# Subfield names consumed per block type. An indented line opens a new
+# subfield only when it starts with one of these followed by ':'; any
+# other line continues the previous subfield.
+_SIGNAL_SUBFIELDS = frozenset({
+ 'direction', 'action', 'condition', 'desc', 'errno', 'timing',
+ 'priority', 'restartable', 'interruptible', 'number', 'target',
+ 'queue', 'queue_behavior', 'transform', 'transform_to', 'transform-to',
+ 'sa_flags_required', 'sa-flags-required',
+ 'sa_flags_forbidden', 'sa-flags-forbidden',
+ 'state_required', 'state-required',
+ 'state_forbidden', 'state-forbidden',
+})
+_LOCK_SUBFIELDS = frozenset({
+ 'type', 'scope', 'acquired', 'released', 'held-on-entry',
+ 'held-on-exit', 'desc',
+})
+_CONSTRAINT_SUBFIELDS = frozenset({'desc', 'expr'})
+_SIDE_EFFECT_SUBFIELDS = frozenset({'target', 'desc', 'condition', 'reversible'})
+_STATE_TRANS_SUBFIELDS = frozenset({'object', 'from', 'to', 'condition', 'desc'})
+_CAPABILITY_SUBFIELDS = frozenset({
+ 'type', 'allows', 'without', 'condition', 'priority', 'desc',
+})
+_RETURN_SUBFIELDS = frozenset({
+ 'type', 'check-type', 'success', 'success-range', 'error-values', 'desc',
+})
+
+_RETURN_INT = r'-?(?:0[xX][0-9a-fA-F]+|0[bB][01]+|\d+)[uUlL]*'
+_RETURN_EXACT_RE = re.compile(rf'^(?:==?\s*)?({_RETURN_INT})$')
+_RETURN_RANGE_RE = re.compile(rf'^>=\s*({_RETURN_INT})$')
+
+
+def _fold_paragraphs(content):
+ """Fold free-form prose into paragraphs.
+
+ Blank lines separate paragraphs ("\\n\\n"); wrapped lines inside a
+ paragraph are joined with spaces, except that a line starting with
+ "- " always begins a new line so bullet lists survive."""
+ paragraphs = []
+ current = []
+ for line in content.split('\n'):
+ line = line.strip()
+ if not line:
+ if current:
+ paragraphs.append(current)
+ current = []
+ elif line.startswith('- ') or not current:
+ current.append(line)
+ else:
+ current[-1] += ' ' + line
+ if current:
+ paragraphs.append(current)
+ return '\n\n'.join('\n'.join(p) for p in paragraphs) or None
+
+
+def _fold_lines(content):
+ """Keep every line of a block on its own line.
+
+ The indentation shared by the continuation lines is removed so that
+ relative indentation (nested code) is preserved; runs of blank lines
+ collapse into one."""
+ lines = [line.rstrip() for line in content.expandtabs().split('\n')]
+ while lines and not lines[0]:
+ lines.pop(0)
+ while lines and not lines[-1]:
+ lines.pop()
+ if not lines:
+ return None
+
+ indents = [len(line) - len(line.lstrip()) for line in lines[1:] if line]
+ base = min(indents) if indents else 0
+
+ out = []
+ for line in lines:
+ if line:
+ line = line[min(base, len(line) - len(line.lstrip())):]
+ elif out and not out[-1]:
+ continue
+ out.append(line)
+ return '\n'.join(out)
+
+
+def _return_success_macro(check_type, value):
+ """Translate a return `success:` value into the macro matching the
+ check type, or None when the check type does not use one (or the
+ value is not a plain integer expression)."""
+ if check_type in ('', 'KAPI_RETURN_EXACT'):
+ m = _RETURN_EXACT_RE.match(value)
+ return f"KAPI_RETURN_SUCCESS({m.group(1)})" if m else None
+ if check_type == 'KAPI_RETURN_RANGE':
+ m = _RETURN_RANGE_RE.match(value)
+ return f"KAPI_RETURN_SUCCESS_RANGE({m.group(1) if m else 0}, S64_MAX)"
+ return None
+
+
+class ApiSpecFormat(OutputFormat):
+ """Generate C macro invocations for kernel API specifications"""
+
+ def __init__(self):
+ super().__init__()
+ self.header_written = False
+
+ def msg(self, fname, name, args):
+ """Handles a single entry from kernel-doc parser"""
+ if not self.header_written:
+ header = self._generate_header()
+ self.header_written = True
+ else:
+ header = ""
+
+ self.data = ""
+ result = super().msg(fname, name, args)
+ return header + (result if result else self.data)
+
+ def _generate_header(self):
+ """Generate the file header"""
+ return (
+ "/* SPDX-License-Identifier: GPL-2.0 */\n"
+ "/* Auto-generated from kerneldoc annotations - DO NOT EDIT */\n\n"
+ "#include <linux/capability.h>\n"
+ "#include <linux/errno.h>\n"
+ "#include <linux/fcntl.h>\n"
+ "#include <linux/kernel_api_spec.h>\n"
+ "#include <linux/signal.h>\n"
+ "#include <linux/stat.h>\n\n"
+ )
+
+ def _format_macro_param(self, value, multiline=False):
+ """Format a value for use in C macro parameter.
+
+ Every string field in the kernel structs is a `const char *`, so
+ there is no length limit. Newlines become spaces unless
+ `multiline` is set, in which case they are kept as "\\n" escapes.
+ """
+ if value is None:
+ return '""'
+ value = str(value).replace('\\', '\\\\').replace('"', '\\"')
+ value = value.replace('\t', ' ').replace('\r', '').replace('\0', '')
+ value = value.replace('\n', '\\n' if multiline else ' ')
+ return f'"{value}"'
+
+ def _get_section(self, sections, key):
+ """Get first line from sections, checking with and without @ prefix and case variants"""
+ for variant in [key, key.capitalize(), key.title()]:
+ for prefix in ['', '@']:
+ full_key = prefix + variant
+ if full_key in sections:
+ content = sections[full_key].strip()
+ # Return only first line to avoid mixing sections
+ return content.split('\n')[0].strip() if content else ''
+ return None
+
+ def _get_raw_section(self, sections, key):
+ """Get full section content, checking with and without @ prefix and case variants"""
+ for variant in [key, key.capitalize(), key.title()]:
+ for prefix in ['', '@']:
+ full_key = prefix + variant
+ if full_key in sections:
+ return sections[full_key]
+ return ''
+
+ def _get_multiline_section(self, sections, key, fold=_fold_paragraphs):
+ """Get a multi-line section, structured by `fold`.
+
+ This is used for fields like notes, long-desc, and examples that
+ can span multiple lines in the kerneldoc comment.
+ """
+ content = self._get_raw_section(sections, key)
+ return fold(content) if content else None
+
+ def _parse_indented_items(self, section_content, item_parser):
+ """Generic parser for indented items.
+
+ Args:
+ section_content: Raw section content
+ item_parser: Function that takes (lines, start_index) and returns (item, next_index)
+
+ Returns:
+ List of parsed items
+ """
+ if not section_content:
+ return []
+
+ items = []
+ lines = section_content.strip().split('\n')
+ i = 0
+
+ while i < len(lines):
+ if not lines[i].strip():
+ i += 1
+ continue
+
+ # Check if this is a main item (not indented)
+ if not lines[i].startswith((' ', '\t')):
+ item, i = item_parser(lines, i)
+ if item:
+ items.append(item)
+ else:
+ i += 1
+
+ return items
+
+ def _parse_subfields(self, lines, start_idx, keys):
+ """Parse indented subfields starting from start_idx+1.
+
+ A line opens a new subfield only if it starts with one of `keys`
+ followed by ':'; every other line continues the previous one.
+ Blank lines inside the block are skipped when more indented
+ lines follow.
+
+ Returns: (dict of subfields, next index)
+ """
+ subfields = {}
+ i = start_idx + 1
+
+ current_key = None
+ while i < len(lines):
+ if not lines[i].strip():
+ nxt = i + 1
+ while nxt < len(lines) and not lines[nxt].strip():
+ nxt += 1
+ if nxt == len(lines) or not lines[nxt].startswith((' ', '\t')):
+ break
+ i = nxt
+ continue
+ if not lines[i].startswith((' ', '\t')):
+ break
+ line = lines[i].strip()
+ key, sep, value = line.partition(':')
+ key = key.strip()
+ if sep and key in keys:
+ current_key = key
+ subfields[current_key] = value.strip()
+ elif current_key:
+ subfields[current_key] = (subfields[current_key] + ' ' + line).strip()
+ i += 1
+
+ return subfields, i
+
+ def _parse_signal_item(self, lines, i):
+ """Parse a single signal specification"""
+ signal = {'name': lines[i].strip()}
+ subfields, next_i = self._parse_subfields(lines, i, _SIGNAL_SUBFIELDS)
+
+ # `direction` and `timing` are bitmasks of KAPI_SIGNAL_* /
+ # KAPI_SIGNAL_TIME_* values; `action` is a single
+ # KAPI_SIGNAL_ACTION_* enum. All three canonicalise aliases to
+ # their KAPI_* spelling.
+ raw_direction = subfields.get('direction', 'KAPI_SIGNAL_RECEIVE')
+ raw_action = subfields.get('action', 'KAPI_SIGNAL_ACTION_RETURN')
+ raw_timing = subfields.get('timing')
+ # `errno:` carries the signal's errno-on-return. The plain
+ # `error:` spelling cannot be used inside a signal block
+ # because kerneldoc promotes it to a top-level `error:` section.
+ signal.update({
+ 'direction': _canon_bitmask_expr(raw_direction, _SIGNAL_DIR_ALIASES),
+ 'action': _canon_token(raw_action, _SIGNAL_ACTION_ALIASES),
+ 'condition': subfields.get('condition'),
+ 'desc': subfields.get('desc'),
+ 'error': subfields.get('errno'),
+ 'timing': _canon_bitmask_expr(raw_timing, _SIGNAL_TIMING_ALIASES)
+ if raw_timing else None,
+ 'priority': subfields.get('priority'),
+ 'restartable': subfields.get('restartable', '').lower() == 'yes',
+ 'interruptible': subfields.get('interruptible', '').lower() == 'yes',
+ 'number': subfields.get('number', '0'),
+ # Additional struct fields. These are optional; if absent, no
+ # KAPI_SIGNAL_* macro is emitted and the field stays at its
+ # zero-initialised default.
+ 'target': subfields.get('target'),
+ 'queue': subfields.get('queue') or subfields.get('queue_behavior'),
+ 'transform': subfields.get('transform') or subfields.get('transform_to')
+ or subfields.get('transform-to'),
+ 'sa_flags_required': subfields.get('sa_flags_required')
+ or subfields.get('sa-flags-required'),
+ 'sa_flags_forbidden': subfields.get('sa_flags_forbidden')
+ or subfields.get('sa-flags-forbidden'),
+ 'state_required': subfields.get('state_required')
+ or subfields.get('state-required'),
+ 'state_forbidden': subfields.get('state_forbidden')
+ or subfields.get('state-forbidden'),
+ })
+
+ return signal, next_i
+
+ def _parse_error_item(self, lines, i):
+ """Parse a single error specification"""
+ line = lines[i].strip()
+
+ # Skip desc: lines
+ if line.startswith('desc:'):
+ return None, i + 1
+
+ # Check for error pattern
+ if not re.match(r'^[A-Z][A-Z0-9_]+,', line):
+ return None, i + 1
+
+ error = {'line': line, 'desc': ''}
+
+ # Look for desc: and condition: subfields
+ i += 1
+ desc_lines = []
+ while i < len(lines):
+ next_line = lines[i].strip()
+ if next_line.startswith('desc:'):
+ desc_lines.append(next_line[5:].strip())
+ i += 1
+ elif next_line.startswith('condition:'):
+ error['condition'] = next_line[10:].strip()
+ i += 1
+ elif not next_line:
+ break
+ elif not desc_lines and re.match(r'^[A-Z][A-Z0-9_]+,', next_line):
+ # New error entry, but only if we haven't started a desc block
+ break
+ else:
+ desc_lines.append(next_line)
+ i += 1
+
+ if desc_lines:
+ error['desc'] = ' '.join(desc_lines)
+
+ return error, i
+
+ def _parse_lock_item(self, lines, i):
+ """Parse a single lock specification.
+
+ Two shapes are accepted:
+ * inline `NAME, TYPE` on the main line; or
+ * `NAME` on the main line with `type:` as an indented
+ subfield.
+ Lock-type values are canonicalised to KAPI_LOCK_* spellings.
+ """
+ head = lines[i].strip()
+ if not head:
+ return None, i + 1
+
+ parts = head.split(',', 1)
+ subfields, next_i = self._parse_subfields(lines, i, _LOCK_SUBFIELDS)
+
+ name = parts[0].strip()
+ type_raw = (parts[1].strip() if len(parts) >= 2
+ else subfields.get('type', '').strip())
+ if not name or not type_raw:
+ return None, next_i
+
+ lock = {
+ 'name': name,
+ 'type': _canon_token(type_raw, _LOCK_TYPE_ALIASES),
+ }
+
+ for field in ['acquired', 'released', 'held-on-entry', 'held-on-exit']:
+ if subfields.get(field, '').lower() in ('true', 'yes'):
+ lock[field] = True
+
+ lock['desc'] = subfields.get('desc', '')
+
+ return lock, next_i
+
+ def _parse_constraint_item(self, lines, i):
+ """Parse a single constraint specification"""
+ line = lines[i].strip()
+
+ # NAME, description form
+ if ',' in line:
+ parts = line.split(',', 1)
+ constraint = {
+ 'name': parts[0].strip(),
+ 'desc': parts[1].strip() if len(parts) > 1 else '',
+ 'expr': None
+ }
+ else:
+ constraint = {'name': line, 'desc': '', 'expr': None}
+
+ subfields, next_i = self._parse_subfields(lines, i, _CONSTRAINT_SUBFIELDS)
+
+ if 'desc' in subfields:
+ constraint['desc'] = (constraint['desc'] + ' ' + subfields['desc']).strip()
+ constraint['expr'] = subfields.get('expr')
+
+ return constraint, next_i
+
+ def _parse_side_effect_item(self, lines, i):
+ """Parse a single side effect specification"""
+ line = lines[i].strip()
+
+ # Defaults for the subfield form
+ effect = {
+ 'type': line,
+ 'target': '',
+ 'desc': '',
+ 'condition': None,
+ 'reversible': False
+ }
+
+ # Inline comma-separated form
+ if ',' in line:
+ # Handle condition and reversible flags
+ cond_match = re.search(r',\s*condition=([^,]+?)(?:\s*,\s*reversible=(yes|no)\s*)?$', line)
+ if cond_match:
+ effect['condition'] = cond_match.group(1).strip()
+ effect['reversible'] = cond_match.group(2) == 'yes'
+ line = line[:cond_match.start()]
+ elif ', reversible=yes' in line:
+ effect['reversible'] = True
+ line = line.replace(', reversible=yes', '')
+ elif ', reversible=no' in line:
+ line = line.replace(', reversible=no', '')
+
+ parts = line.split(',', 2)
+ if len(parts) >= 1:
+ effect['type'] = parts[0].strip()
+ if len(parts) >= 2:
+ effect['target'] = parts[1].strip()
+ if len(parts) >= 3:
+ effect['desc'] = parts[2].strip()
+ else:
+ # Multi-line format with subfields
+ subfields, next_i = self._parse_subfields(
+ lines, i, _SIDE_EFFECT_SUBFIELDS)
+ effect.update({
+ 'target': subfields.get('target', ''),
+ 'desc': subfields.get('desc', ''),
+ 'condition': subfields.get('condition'),
+ 'reversible': subfields.get('reversible', '').lower() == 'yes'
+ })
+ return effect, next_i
+
+ return effect, i + 1
+
+ def _parse_state_trans_item(self, lines, i):
+ """Parse a single state transition specification"""
+ line = lines[i].strip()
+
+ trans = {
+ 'target': line,
+ 'from': '',
+ 'to': '',
+ 'condition': '',
+ 'desc': ''
+ }
+
+ # Inline comma-separated form
+ if ',' in line:
+ parts = line.split(',', 3)
+ if len(parts) >= 1:
+ trans['target'] = parts[0].strip()
+ if len(parts) >= 2:
+ trans['from'] = parts[1].strip()
+ if len(parts) >= 3:
+ trans['to'] = parts[2].strip()
+ if len(parts) >= 4:
+ trans['desc'] = parts[3].strip()
+ return trans, i + 1
+ else:
+ # Multi-line format with subfields
+ subfields, next_i = self._parse_subfields(
+ lines, i, _STATE_TRANS_SUBFIELDS)
+ trans.update({
+ 'target': subfields.get('object', line),
+ 'from': subfields.get('from', ''),
+ 'to': subfields.get('to', ''),
+ 'condition': subfields.get('condition', ''),
+ 'desc': subfields.get('desc', '')
+ })
+ return trans, next_i
+
+ def _process_parameters(self, sections, parameterlist, parameterdescs, parametertypes):
+ """Process and output parameter specifications"""
+ param_count = len(parameterlist)
+ if param_count > 0:
+ self.data += f"\n\tKAPI_PARAM_COUNT({param_count})\n"
+
+ for param_idx, param in enumerate(parameterlist):
+ param_name = param.strip()
+ param_desc = parameterdescs.get(param_name, '').strip()
+ param_ctype = parametertypes.get(param_name, '')
+
+ # Parse parameter specifications
+ param_section = self._get_raw_section(sections, 'param')
+ param_specs = {}
+ if param_section:
+ param_specs = self._parse_param_spec(param_section, param_name)
+
+ self.data += f"\n\tKAPI_PARAM({param_idx}, {self._format_macro_param(param_name)}, "
+ self.data += f"{self._format_macro_param(param_ctype)}, {self._format_macro_param(param_desc)})\n"
+
+ # Add parameter attributes
+ for key, macro in [
+ ('param-type', 'KAPI_PARAM_TYPE'),
+ ('param-flags', 'KAPI_PARAM_FLAGS'),
+ ('param-size', 'KAPI_PARAM_SIZE'),
+ ('param-alignment', 'KAPI_PARAM_ALIGNMENT'),
+ ]:
+ if key in param_specs:
+ self.data += f"\t\t{macro}({param_specs[key]})\n"
+
+ # Handle constraint type
+ if 'param-constraint-type' in param_specs:
+ ctype = param_specs['param-constraint-type']
+ self.data += f"\t\tKAPI_PARAM_CONSTRAINT_TYPE({ctype})\n"
+
+ # Handle range
+ if 'param-range' in param_specs and ',' in param_specs['param-range']:
+ min_val, max_val = param_specs['param-range'].split(',', 1)
+ self.data += f"\t\tKAPI_PARAM_RANGE({min_val.strip()}, {max_val.strip()})\n"
+
+ # Handle mask. If `arch-mask:` lines are present, fall back
+ # to a raw `.valid_mask = (...)` initializer so we can wrap
+ # arch-specific bits in #ifdef CONFIG_<ARCH> ... #endif, which
+ # is illegal inside a function-like macro argument.
+ if 'param-mask' in param_specs:
+ arch_masks = param_specs.get('param-arch-mask') or []
+ if arch_masks:
+ base = param_specs['param-mask']
+ self.data += f"\t\t.valid_mask = ({base})"
+ for arch, bits in arch_masks:
+ config = _ARCH_CONFIG.get(arch.lower())
+ if config is None:
+ sys.stderr.write(
+ f"kdoc_apispec: unknown arch '{arch}' in "
+ f"arch-mask: line; skipping\n")
+ continue
+ self.data += (f"\n#ifdef {config}\n"
+ f"\t\t\t| ({bits})\n"
+ f"#endif\n")
+ self.data += "\t\t,\n"
+ else:
+ self.data += (
+ f"\t\tKAPI_PARAM_VALID_MASK("
+ f"{param_specs['param-mask']})\n")
+
+ # Handle enum values
+ if 'param-enum-values' in param_specs:
+ self.data += f"\t\tKAPI_PARAM_ENUM_VALUES({param_specs['param-enum-values']})\n"
+
+ # Handle size parameter index
+ if 'param-size-param' in param_specs:
+ self.data += f"\t\tKAPI_PARAM_SIZE_PARAM({param_specs['param-size-param']})\n"
+
+ # Handle constraint description
+ if 'param-constraint' in param_specs:
+ self.data += f"\t\tKAPI_PARAM_CONSTRAINT({self._format_macro_param(param_specs['param-constraint'])})\n"
+
+ self.data += "\t},\n"
+
+ def _parse_param_spec(self, section_content, param_name):
+ """Parse parameter specifications from indented format"""
+ specs = {}
+ lines = section_content.strip().split('\n')
+ current_item = None
+
+ # Map to expected keys
+ field_map = {
+ 'type': 'param-type',
+ 'flags': 'param-flags',
+ 'size': 'param-size',
+ 'constraint-type': 'param-constraint-type',
+ 'constraint': 'param-constraint',
+ 'cdesc': 'param-constraint',
+ 'range': 'param-range',
+ 'mask': 'param-mask',
+ 'valid-mask': 'param-mask',
+ 'valid-values': 'param-enum-values',
+ 'alignment': 'param-alignment',
+ 'size-param': 'param-size-param',
+ 'struct-type': 'param-struct-type',
+ 'arch-mask': 'param-arch-mask',
+ }
+
+ i = 0
+ while i < len(lines):
+ line = lines[i]
+ if not line.strip():
+ i += 1
+ continue
+
+ # Check if this is our parameter (non-indented line)
+ if not line.startswith((' ', '\t')):
+ parts = line.strip().split(',', 1)
+ current_item = param_name if parts[0].strip() == param_name else None
+ if current_item and len(parts) > 1:
+ specs['param-type'] = parts[1].strip()
+ i += 1
+ elif current_item == param_name:
+ # Parse subfield
+ stripped = line.strip()
+ if ':' in stripped:
+ key, value = stripped.split(':', 1)
+ key = key.strip()
+ value = value.strip()
+
+ # Collect continuation lines (indented lines without a colon that
+ # defines a new key, i.e., lines that are pure continuations)
+ i += 1
+ while i < len(lines):
+ next_line = lines[i]
+ # Stop if we hit a non-indented line (new param)
+ if next_line.strip() and not next_line.startswith((' ', '\t')):
+ break
+ next_stripped = next_line.strip()
+ # Stop if we hit a new key (contains colon with known key prefix)
+ if next_stripped and ':' in next_stripped:
+ potential_key = next_stripped.split(':', 1)[0].strip()
+ if potential_key in field_map or potential_key in ['type', 'desc']:
+ break
+ # This is a continuation line
+ if next_stripped:
+ value = value + ' ' + next_stripped
+ i += 1
+
+ if key in field_map:
+ # Clean up the value - remove excessive whitespace
+ value = ' '.join(value.split())
+ mapped = field_map[key]
+ if mapped == 'param-type':
+ # Single token sets the type; additional
+ # comma-separated tokens are flags OR'd
+ # into param-flags.
+ ty, extra_flags = _split_type_line(value)
+ if ty:
+ specs['param-type'] = ty
+ if extra_flags:
+ existing = specs.get('param-flags', '')
+ merged = (existing + ' | ' if existing else '') \
+ + ' | '.join(extra_flags)
+ specs['param-flags'] = merged
+ elif mapped == 'param-flags':
+ specs['param-flags'] = _canon_flags_expr(value)
+ elif mapped == 'param-constraint-type':
+ # Accepts a KAPI_CONSTRAINT_* token or a
+ # function-call expression like
+ # `range(0, 4096)` / `mask(0xff)` /
+ # `buffer(2)` that also populates the
+ # matching aux field.
+ parsed = _split_constraint_expr(value)
+ if parsed is not None:
+ ctype, extras = parsed
+ specs['param-constraint-type'] = ctype
+ for aux_k, aux_v in extras.items():
+ specs[aux_k] = aux_v
+ else:
+ specs['param-constraint-type'] = value
+ elif mapped == 'param-arch-mask':
+ # `arch-mask: <arch> = <bits>`: multiple
+ # entries accumulate into a list of
+ # (arch, bits) tuples that the emitter
+ # turns into per-arch #ifdef-guarded mask
+ # contributions.
+ if '=' in value:
+ arch, bits = value.split('=', 1)
+ specs.setdefault(mapped, []).append(
+ (arch.strip(), bits.strip()))
+ else:
+ specs[mapped] = value
+ else:
+ i += 1
+ else:
+ i += 1
+
+ return specs
+
+ def _validate_effect_type(self, effect_type):
+ """Validate and normalize effect type"""
+ if 'KAPI_EFFECT_' in effect_type and effect_type not in VALID_EFFECT_TYPES:
+ if '|' in effect_type:
+ parts = [p.strip() for p in effect_type.split('|')]
+ valid_parts = []
+ for p in parts:
+ if p in VALID_EFFECT_TYPES:
+ valid_parts.append(p)
+ else:
+ print(f"warning: unrecognized effect type '{p}', "
+ f"defaulting to KAPI_EFFECT_MODIFY_STATE", file=sys.stderr)
+ valid_parts.append('KAPI_EFFECT_MODIFY_STATE')
+ return ' | '.join(valid_parts)
+ print(f"warning: unrecognized effect type '{effect_type}', "
+ f"defaulting to KAPI_EFFECT_MODIFY_STATE", file=sys.stderr)
+ return 'KAPI_EFFECT_MODIFY_STATE'
+
+ return effect_type
+
+ def _has_api_spec(self, sections):
+ """Check if this function has an API specification.
+
+ Returns True if a `contexts:` or `context-flags:` section is present
+ together with at least one other KAPI section. Regular kernel-doc
+ comments that only use a common section name like 'return' or
+ 'error' do not produce a spec.
+ """
+ context_keys = ['context-flags', 'contexts']
+ indicators = [
+ 'api-type', 'param', 'error', 'capability', 'signal', 'lock',
+ 'state-trans', 'constraint', 'side-effect', 'long-desc'
+ ]
+
+ def has_section(names):
+ return any(key.lower().startswith(name) or
+ key.lower().startswith('@' + name)
+ for key in sections.keys() for name in names)
+
+ return has_section(context_keys) and has_section(indicators)
+
+ def out_function(self, fname, name, args):
+ """Generate API spec for a function"""
+ function_name = args.get('function', name)
+ sections = args.sections if hasattr(args, 'sections') else args.get('sections', {})
+
+ if not self._has_api_spec(sections):
+ return
+
+ parameterlist = args.parameterlist if hasattr(args, 'parameterlist') else args.get('parameterlist', [])
+ parameterdescs = args.parameterdescs if hasattr(args, 'parameterdescs') else args.get('parameterdescs', {})
+ parametertypes = args.parametertypes if hasattr(args, 'parametertypes') else args.get('parametertypes', {})
+ purpose = args.get('purpose', '')
+
+ # Start macro invocation
+ self.data += f"DEFINE_KERNEL_API_SPEC({function_name})\n"
+
+ # Basic info
+ if purpose:
+ self.data += f"\tKAPI_DESCRIPTION({self._format_macro_param(purpose)})\n"
+
+ long_desc = self._get_multiline_section(sections, 'long-desc')
+ if long_desc:
+ self.data += f"\tKAPI_LONG_DESC({self._format_macro_param(long_desc, True)})\n"
+
+ # Context flags. `contexts:` and `context-flags:` both work;
+ # tokens canonicalise to KAPI_CTX_* for KAPI_CONTEXT().
+ context = (self._get_section(sections, 'contexts')
+ or self._get_section(sections, 'context-flags'))
+ if context:
+ self.data += f"\tKAPI_CONTEXT({_canon_context_expr(context)})\n"
+
+ # Process parameters
+ self._process_parameters(sections, parameterlist, parameterdescs, parametertypes)
+
+ # Process return value
+ self._process_return(sections)
+
+ # Process errors
+ errors = self._parse_indented_items(
+ self._get_raw_section(sections, 'error'),
+ self._parse_error_item
+ )
+
+ if errors:
+ self.data += f"\n\tKAPI_ERROR_COUNT({len(errors)})\n"
+
+ for idx, error in enumerate(errors):
+ self._output_error(idx, error)
+
+ # Process signals
+ signals = self._parse_indented_items(
+ self._get_raw_section(sections, 'signal'),
+ self._parse_signal_item
+ )
+
+ if signals:
+ self.data += f"\n\tKAPI_SIGNAL_COUNT({len(signals)})\n"
+
+ for idx, signal in enumerate(signals):
+ self._output_signal(idx, signal)
+
+ # Process other specifications
+ self._process_locks(sections)
+ self._process_constraints(sections)
+ self._process_side_effects(sections)
+ self._process_state_transitions(sections)
+ self._process_capabilities(sections)
+
+ # Add examples and notes
+ for key, macro, fold in [
+ ('examples', 'KAPI_EXAMPLES', _fold_lines),
+ ('notes', 'KAPI_NOTES', _fold_paragraphs),
+ ]:
+ value = self._get_multiline_section(sections, key, fold)
+ if value:
+ self.data += f"\n\t{macro}({self._format_macro_param(value, True)})\n"
+
+ self.data += "\n};\n\n"
+
+ def _process_return(self, sections):
+ """Process the return value specification from kerneldoc annotations"""
+ raw = self._get_raw_section(sections, 'return')
+ if not raw:
+ return
+
+ # Parse subfields from the return section, handling continuation lines
+ lines = raw.strip().split('\n')
+ subfields = {}
+ current_key = None
+ for line in lines:
+ stripped = line.strip()
+ key, sep, value = stripped.partition(':')
+ key = key.strip()
+ if sep and key in _RETURN_SUBFIELDS:
+ current_key = key
+ subfields[current_key] = value.strip()
+ elif current_key and stripped:
+ # Continuation line
+ subfields[current_key] += ' ' + stripped
+
+ ret_type = subfields.get('type', '')
+ check_type = subfields.get('check-type', '')
+ desc = subfields.get('desc', '')
+ success = subfields.get('success', '')
+
+ if not ret_type and not desc:
+ return
+
+ # Canonicalise short aliases:
+ # type: int -> KAPI_TYPE_INT
+ # check-type: fd -> KAPI_RETURN_FD
+ # The type name itself is kept as written.
+ type_name = ret_type
+ if ret_type:
+ ret_type = _canon_token(ret_type, _TYPE_ALIASES)
+ if check_type:
+ check_type = _canon_token(check_type, _RETURN_CHECK_ALIASES)
+
+ self.data += f"\n\tKAPI_RETURN({self._format_macro_param(type_name)}, "
+ self.data += f"{self._format_macro_param(desc)})\n"
+
+ if ret_type:
+ self.data += f"\t\tKAPI_RETURN_TYPE({ret_type})\n"
+
+ if check_type:
+ self.data += f"\t\tKAPI_RETURN_CHECK_TYPE({check_type})\n"
+
+ if success:
+ macro = _return_success_macro(check_type, success)
+ if macro:
+ self.data += f"\t\t{macro}\n"
+ elif check_type in ('', 'KAPI_RETURN_EXACT'):
+ sys.stderr.write(
+ f"kdoc_apispec: ignoring unsupported success value "
+ f"'{success}' for an exact return check\n")
+
+ self.data += "\t},\n"
+
+ def _output_error(self, idx, error):
+ """Output a single error specification"""
+ # Format: NAME, description
+ parts = error['line'].split(',', 1)
+ if len(parts) < 2:
+ return
+
+ name = parts[0].strip()
+ short_desc = parts[1].strip()
+ code = f"-{name}"
+
+ condition = error.get('condition') or short_desc
+ long_desc = error.get('desc', '') or short_desc
+
+ self.data += f"\n\tKAPI_ERROR({idx}, {code}, {self._format_macro_param(name)}, "
+ self.data += f"{self._format_macro_param(condition)},\n\t\t {self._format_macro_param(long_desc)})\n"
+
+ def _output_signal(self, idx, signal):
+ """Output a single signal specification"""
+ self.data += f"\n\tKAPI_SIGNAL({idx}, {signal['number']}, "
+ self.data += f"{self._format_macro_param(signal['name'])}, "
+ self.data += f"{signal['direction']}, {signal['action']})\n"
+
+ # String-valued subfields emitted as KAPI_SIGNAL_* macros.
+ if signal.get('condition'):
+ self.data += f"\t\tKAPI_SIGNAL_CONDITION({self._format_macro_param(signal['condition'])})\n"
+ if signal.get('desc'):
+ self.data += f"\t\tKAPI_SIGNAL_DESC({self._format_macro_param(signal['desc'])})\n"
+ if signal.get('error'):
+ # KAPI_SIGNAL_ERROR expects a numeric/token expression
+ # (e.g. -EINTR), not a quoted string.
+ self.data += f"\t\tKAPI_SIGNAL_ERROR({signal['error']})\n"
+
+ # Enum-valued subfields emitted as unquoted tokens.
+ if signal.get('timing'):
+ self.data += f"\t\tKAPI_SIGNAL_TIMING({signal['timing']})\n"
+ if signal.get('priority'):
+ self.data += f"\t\tKAPI_SIGNAL_PRIORITY({signal['priority']})\n"
+
+ # Boolean flag subfields.
+ if signal.get('restartable'):
+ self.data += "\t\tKAPI_SIGNAL_RESTARTABLE\n"
+ if signal.get('interruptible'):
+ self.data += "\t\tKAPI_SIGNAL_INTERRUPTIBLE\n"
+
+ # Additional struct fields, emitted only when present in the
+ # kerneldoc.
+ if signal.get('target'):
+ self.data += f"\t\tKAPI_SIGNAL_TARGET({self._format_macro_param(signal['target'])})\n"
+ if signal.get('queue'):
+ self.data += f"\t\tKAPI_SIGNAL_QUEUE({self._format_macro_param(signal['queue'])})\n"
+ if signal.get('transform'):
+ # Numeric/token expression (e.g. SIGKILL), not a quoted string.
+ self.data += f"\t\tKAPI_SIGNAL_TRANSFORM({signal['transform']})\n"
+ if signal.get('sa_flags_required'):
+ self.data += f"\t\tKAPI_SIGNAL_SA_FLAGS_REQ({signal['sa_flags_required']})\n"
+ if signal.get('sa_flags_forbidden'):
+ self.data += f"\t\tKAPI_SIGNAL_SA_FLAGS_FORBID({signal['sa_flags_forbidden']})\n"
+ if signal.get('state_required'):
+ self.data += f"\t\tKAPI_SIGNAL_STATE_REQ({signal['state_required']})\n"
+ if signal.get('state_forbidden'):
+ self.data += f"\t\tKAPI_SIGNAL_STATE_FORBID({signal['state_forbidden']})\n"
+
+ self.data += "\t},\n"
+
+ def _process_locks(self, sections):
+ """Process lock specifications"""
+ locks = self._parse_indented_items(
+ self._get_raw_section(sections, 'lock'),
+ self._parse_lock_item
+ )
+
+ if locks:
+ self.data += f"\n\tKAPI_LOCK_COUNT({len(locks)})\n"
+
+ for idx, lock in enumerate(locks):
+ self.data += f"\n\tKAPI_LOCK({idx}, {self._format_macro_param(lock['name'])}, {lock['type']})\n"
+
+ # `.scope` is zero-initialised to KAPI_LOCK_INTERNAL
+ # (acquired-and-released). Emit KAPI_LOCK_ACQUIRED /
+ # KAPI_LOCK_RELEASED only when exactly one of the flags
+ # is true; emitting both would double-initialise `.scope`
+ # which breaks `-Werror=override-init` at W=1.
+ acquired = bool(lock.get('acquired'))
+ released = bool(lock.get('released'))
+ if acquired and not released:
+ self.data += "\t\tKAPI_LOCK_ACQUIRED\n"
+ elif released and not acquired:
+ self.data += "\t\tKAPI_LOCK_RELEASED\n"
+
+ if lock.get('desc'):
+ self.data += f"\t\tKAPI_LOCK_DESC({self._format_macro_param(lock['desc'])})\n"
+
+ self.data += "\t},\n"
+
+ def _process_constraints(self, sections):
+ """Process constraint specifications"""
+ constraints = self._parse_indented_items(
+ self._get_raw_section(sections, 'constraint'),
+ self._parse_constraint_item
+ )
+
+ if constraints:
+ self.data += f"\n\tKAPI_CONSTRAINT_COUNT({len(constraints)})\n"
+
+ for idx, constraint in enumerate(constraints):
+ self.data += f"\n\tKAPI_CONSTRAINT({idx}, {self._format_macro_param(constraint['name'])},\n"
+ self.data += f"\t\t\t{self._format_macro_param(constraint['desc'])})\n"
+
+ if constraint.get('expr'):
+ self.data += f"\t\tKAPI_CONSTRAINT_EXPR({self._format_macro_param(constraint['expr'])})\n"
+
+ self.data += "\t},\n"
+
+ def _process_side_effects(self, sections):
+ """Process side effect specifications"""
+ effects = self._parse_indented_items(
+ self._get_raw_section(sections, 'side-effect'),
+ self._parse_side_effect_item
+ )
+
+ if effects:
+ self.data += f"\n\tKAPI_SIDE_EFFECT_COUNT({len(effects)})\n"
+
+ for idx, effect in enumerate(effects):
+ # Canonicalise aliases (alloc_memory, modify_state, ...)
+ # to KAPI_EFFECT_*. Accepts '|' or ',' as separators.
+ effect_type = _canon_bitmask_expr(effect['type'], _EFFECT_ALIASES)
+ effect_type = self._validate_effect_type(effect_type)
+
+ self.data += f"\n\tKAPI_SIDE_EFFECT({idx}, {effect_type},\n"
+ self.data += f"\t\t\t {self._format_macro_param(effect['target'])},\n"
+ self.data += f"\t\t\t {self._format_macro_param(effect['desc'])})\n"
+
+ if effect.get('condition'):
+ self.data += f"\t\tKAPI_EFFECT_CONDITION({self._format_macro_param(effect['condition'])})\n"
+
+ if effect.get('reversible'):
+ self.data += "\t\tKAPI_EFFECT_REVERSIBLE\n"
+
+ self.data += "\t},\n"
+
+ def _process_state_transitions(self, sections):
+ """Process state transition specifications"""
+ transitions = self._parse_indented_items(
+ self._get_raw_section(sections, 'state-trans'),
+ self._parse_state_trans_item
+ )
+
+ if transitions:
+ self.data += f"\n\tKAPI_STATE_TRANS_COUNT({len(transitions)})\n"
+
+ for idx, trans in enumerate(transitions):
+ self.data += f"\n\tKAPI_STATE_TRANS({idx}, {self._format_macro_param(trans['target'])}, "
+ self.data += f"{self._format_macro_param(trans['from'])}, {self._format_macro_param(trans['to'])},\n"
+ self.data += f"\t\t\t {self._format_macro_param(trans['desc'])})\n"
+
+ if trans.get('condition'):
+ self.data += f"\t\tKAPI_STATE_TRANS_COND({self._format_macro_param(trans['condition'])})\n"
+
+ self.data += "\t},\n"
+
+ def _process_capabilities(self, sections):
+ """Process capability specifications"""
+ cap_section = self._get_raw_section(sections, 'capability')
+ if not cap_section:
+ return
+
+ lines = cap_section.strip().split('\n')
+ capabilities = []
+ i = 0
+
+ while i < len(lines):
+ line = lines[i].strip()
+ # Skip empty lines and subfield lines (they'll be parsed with their parent)
+ if not line or line.startswith(('allows:', 'without:', 'condition:', 'priority:', 'type:', 'desc:')):
+ i += 1
+ continue
+
+ cap_info = {'line': line}
+
+ # Parse subfields
+ subfields, next_i = self._parse_subfields(
+ lines, i, _CAPABILITY_SUBFIELDS)
+ cap_info.update(subfields)
+ capabilities.append(cap_info)
+ i = next_i
+
+ if capabilities:
+ # Filter out "none" capabilities (no capability required)
+ valid_caps = [cap for cap in capabilities if cap['line'].strip().lower() != 'none']
+
+ if not valid_caps:
+ return
+
+ self.data += f"\n\tKAPI_CAPABILITY_COUNT({len(valid_caps)})\n"
+
+ for idx, cap in enumerate(valid_caps):
+ line = cap['line']
+ parts = line.split(',', 2)
+
+ # Two forms are accepted:
+ # 1. "CAP_NAME" with type/desc as subfields
+ # 2. "CAP_NAME, TYPE, description"
+ if len(parts) >= 2:
+ # Comma-separated form
+ cap_name = parts[0].strip()
+ cap_type = parts[1].strip()
+ cap_desc = parts[2].strip() if len(parts) > 2 else cap.get('desc', cap_name)
+ else:
+ # Subfield form: capability name on main line
+ cap_name = line.strip()
+ cap_type = cap.get('type', 'KAPI_CAP_PERFORM_OPERATION')
+ cap_desc = cap.get('desc', cap_name)
+
+ # Map capability type aliases to KAPI_CAP_* enum values.
+ cap_type_map = {
+ 'required': 'KAPI_CAP_PERFORM_OPERATION',
+ 'bypass': 'KAPI_CAP_BYPASS_CHECK',
+ 'grant': 'KAPI_CAP_GRANT_PERMISSION',
+ 'override': 'KAPI_CAP_OVERRIDE_RESTRICTION',
+ 'access': 'KAPI_CAP_ACCESS_RESOURCE',
+ 'modify': 'KAPI_CAP_MODIFY_BEHAVIOR',
+ 'limit': 'KAPI_CAP_INCREASE_LIMIT',
+ 'bypass_check': 'KAPI_CAP_BYPASS_CHECK',
+ 'increase_limit': 'KAPI_CAP_INCREASE_LIMIT',
+ 'override_restriction': 'KAPI_CAP_OVERRIDE_RESTRICTION',
+ 'grant_permission': 'KAPI_CAP_GRANT_PERMISSION',
+ 'modify_behavior': 'KAPI_CAP_MODIFY_BEHAVIOR',
+ 'access_resource': 'KAPI_CAP_ACCESS_RESOURCE',
+ 'perform_operation': 'KAPI_CAP_PERFORM_OPERATION',
+ }
+ cap_type = cap_type_map.get(cap_type, cap_type)
+
+ # Any BYPASS variant maps to KAPI_CAP_BYPASS_CHECK
+ if 'BYPASS' in cap_type and cap_type != 'KAPI_CAP_BYPASS_CHECK':
+ cap_type = 'KAPI_CAP_BYPASS_CHECK'
+
+ # Ensure cap_type is a valid enum
+ valid_types = [
+ 'KAPI_CAP_BYPASS_CHECK', 'KAPI_CAP_INCREASE_LIMIT',
+ 'KAPI_CAP_OVERRIDE_RESTRICTION', 'KAPI_CAP_GRANT_PERMISSION',
+ 'KAPI_CAP_MODIFY_BEHAVIOR', 'KAPI_CAP_ACCESS_RESOURCE',
+ 'KAPI_CAP_PERFORM_OPERATION'
+ ]
+ if cap_type not in valid_types:
+ cap_type = 'KAPI_CAP_PERFORM_OPERATION'
+
+ self.data += f"\n\tKAPI_CAPABILITY({idx}, {cap_name}, {self._format_macro_param(cap_desc)}, {cap_type})\n"
+
+ for key, macro in [
+ ('allows', 'KAPI_CAP_ALLOWS'),
+ ('without', 'KAPI_CAP_WITHOUT'),
+ ('condition', 'KAPI_CAP_CONDITION'),
+ ('priority', 'KAPI_CAP_PRIORITY'),
+ ]:
+ if cap.get(key):
+ value = self._format_macro_param(cap[key]) if key != 'priority' else cap[key]
+ self.data += f"\t\t{macro}({value})\n"
+
+ self.data += "\t},\n"
+
+ # Skip output methods for non-function types
+ def out_enum(self, fname, name, args): pass
+ def out_typedef(self, fname, name, args): pass
+ def out_struct(self, fname, name, args): pass
+ def out_doc(self, fname, name, args): pass
diff --git a/tools/lib/python/kdoc/kdoc_files.py b/tools/lib/python/kdoc/kdoc_files.py
index ed82b6e6ab25b..5b81a1e4cc9e9 100644
--- a/tools/lib/python/kdoc/kdoc_files.py
+++ b/tools/lib/python/kdoc/kdoc_files.py
@@ -94,13 +94,14 @@ class KdocConfig():
"""
def __init__(self, verbose=False, werror=False, wreturn=False,
wshort_desc=False, wcontents_before_sections=False,
- logger=None):
+ logger=None, apispec=False):
self.verbose = verbose
self.werror = werror
self.wreturn = wreturn
self.wshort_desc = wshort_desc
self.wcontents_before_sections = wcontents_before_sections
+ self.apispec = apispec
if logger:
self.log = logger
@@ -159,6 +160,10 @@ class KernelFiles():
``yaml_content``
Defines what will be inside the YAML file.
+ ``apispec``
+ If True, also parse the kernel API specification sections used
+ by the ``-apispec`` output. Default: False.
+
Note:
There are two type of parsers defined here:
@@ -232,7 +237,8 @@ class KernelFiles():
def __init__(self, verbose=False, out_style=None, xforms=None,
werror=False, wreturn=False, wshort_desc=False,
wcontents_before_sections=False,
- yaml_file=None, yaml_content=None, logger=None):
+ yaml_file=None, yaml_content=None, logger=None,
+ apispec=False):
"""
Initialize startup variables and parse all files.
"""
@@ -271,7 +277,7 @@ class KernelFiles():
# used to send control configuration to KernelDoc class. As such,
# those variables are read-only inside the KernelDoc.
self.config = KdocConfig(verbose, werror, wreturn, wshort_desc,
- wcontents_before_sections, logger)
+ wcontents_before_sections, logger, apispec)
# Override log warning, as we want to count errors
self.config.warning = self.warning
diff --git a/tools/lib/python/kdoc/kdoc_parser.py b/tools/lib/python/kdoc/kdoc_parser.py
index d9ad1ddc87dd1..a29fdccda8aee 100644
--- a/tools/lib/python/kdoc/kdoc_parser.py
+++ b/tools/lib/python/kdoc/kdoc_parser.py
@@ -30,6 +30,23 @@ from kdoc.kdoc_item import KdocItem
# Allow whitespace at end of comment start.
doc_start = KernRe(r'^/\*\*\s*$', cache=False)
+# Sections that are allowed to be duplicated for API specifications
+# These represent lists of items (multiple errors, signals, etc.)
+ALLOWED_DUPLICATE_SECTIONS = {
+ 'param', '@param',
+ 'error', '@error',
+ 'signal', '@signal',
+ 'lock', '@lock',
+ 'side-effect', '@side-effect',
+ 'state-trans', '@state-trans',
+ 'capability', '@capability',
+ 'constraint', '@constraint',
+ 'validation-group', '@validation-group',
+ 'validation-rule', '@validation-rule',
+ 'validation-flag', '@validation-flag',
+ 'struct-field', '@struct-field',
+}
+
doc_end = KernRe(r'\*/', cache=False)
doc_com = KernRe(r'\s*\*\s*', cache=False)
doc_com_body = KernRe(r'\s*\* ?', cache=False)
@@ -48,6 +65,71 @@ doc_sect = doc_com + \
KernRe(r'\s*(@[.\w]+|@\.\.\.|' + known_section_names + r')\s*:([^:].*)?$',
flags=re.I, cache=False)
+# API specification section names (for KAPI spec framework), only
+# recognized when generating -apispec output
+# Format: (base_name, has_count_variant, has_other_variants)
+# Sections with has_count_variant=True need negative lookahead in kapi_doc_sect
+# to avoid matching 'error' when 'error-count' is intended
+_kapi_base_sections = [
+ # (name, needs_lookahead, additional_variants)
+ ('api-type', False, []),
+ ('api-version', False, []),
+ ('param', True, []), # has param-count
+ ('struct', True, ['struct-type', 'struct-field', 'struct-field-[a-z\\-]+']),
+ ('validation-group', False, []),
+ ('validation-policy', False, []),
+ ('validation-flag', False, []),
+ ('validation-rule', False, []),
+ ('error', True, ['error-code', 'error-condition']),
+ ('capability', True, []),
+ ('signal', True, []),
+ ('lock', True, []),
+ ('context-flags', False, []),
+ ('contexts', False, []),
+ ('return', True, ['return-type', 'return-check', 'return-check-type',
+ 'return-success', 'return-desc']),
+ ('long-desc', False, []),
+ ('constraint', True, []),
+ ('side-effect', True, []),
+ ('state-trans', True, []),
+]
+
+def _build_kapi_patterns():
+ """Build KAPI section patterns from the base definitions."""
+ validation_parts = [] # For kapi_known_sections (simple validation)
+ parsing_parts = [] # For kapi_doc_sect (with negative lookaheads)
+
+ for name, has_count, variants in _kapi_base_sections:
+ # Add base name (with optional @ prefix)
+ validation_parts.append(f'@?{name}')
+ if has_count:
+ # Need negative lookahead to not match 'name-count' or 'name-*'
+ parsing_parts.append(f'@?{name}(?!-)')
+ validation_parts.append(f'@?{name}-count')
+ parsing_parts.append(f'@?{name}-count')
+ else:
+ parsing_parts.append(f'@?{name}')
+
+ # Add variants
+ for variant in variants:
+ validation_parts.append(f'@?{variant}')
+ parsing_parts.append(f'@?{variant}')
+
+ # Add catch-all for kapi-* extensions
+ validation_parts.append(r'@?kapi-.*')
+ parsing_parts.append(r'@?kapi-.*')
+
+ return '|'.join(validation_parts), '|'.join(parsing_parts)
+
+_kapi_validation_pattern, _kapi_parsing_pattern = _build_kapi_patterns()
+
+kapi_known_sections = KernRe(known_section_names + '|' +
+ _kapi_validation_pattern, flags=re.I)
+kapi_doc_sect = doc_com + \
+ KernRe(r'\s*(@[.\w\-]+|@\.\.\.|' + known_section_names + '|' +
+ _kapi_parsing_pattern + r')\s*:([^:].*)?$',
+ flags=re.I, cache=False)
+
doc_content = doc_com_body + KernRe(r'(.*)', cache=False)
doc_inline_start = KernRe(r'^\s*/\*\*\s*$', cache=False)
doc_inline_sect = KernRe(r'\s*\*\s*(@\s*[\w][\w\.]*\s*):(.*)', cache=False)
@@ -214,7 +296,9 @@ class KernelEntry:
else:
if name in self.sections and self.sections[name] != "":
# Only warn on user-specified duplicate section names
- if name != SECTION_DEFAULT:
+ # Skip warning for sections that are expected to have duplicates
+ # (like error, param, signal, etc. for API specifications)
+ if name != SECTION_DEFAULT and name not in ALLOWED_DUPLICATE_SECTIONS:
self.emit_msg(self.new_start_line,
f"duplicate section name '{name}'")
# Treat as a new paragraph - add a blank line
@@ -255,6 +339,13 @@ class KernelDoc:
self.xforms = xforms
self.store_src = store_src
+ if self.config.apispec:
+ self.known_sections = kapi_known_sections
+ self.doc_sect = kapi_doc_sect
+ else:
+ self.known_sections = known_sections
+ self.doc_sect = doc_sect
+
tokenizer_set_log(self.config.log, f"{self.fname}: CMatch: ")
# Initial state for the state machines
@@ -611,7 +702,7 @@ class KernelDoc:
"""
for section in self.entry.sections:
if section not in self.entry.parameterlist and \
- not known_sections.search(section):
+ not self.known_sections.search(section):
hint = self.get_suggestions_hint(section, self.entry.parameterlist)
if decl_type == 'function':
dname = f"{decl_type} parameter"
@@ -1314,12 +1405,12 @@ class KernelDoc:
"""
Helper function to determine if a new section is being started.
"""
- if doc_sect.search(line):
+ if self.doc_sect.search(line):
self.state = state.BODY
#
# Pick out the name of our new section, tweaking it if need be.
#
- newsection = doc_sect.group(1)
+ newsection = self.doc_sect.group(1)
if newsection.lower() == 'description':
newsection = 'Description'
elif newsection.lower() == 'context':
@@ -1334,7 +1425,7 @@ class KernelDoc:
#
# Initialize the contents, and get the new section going.
#
- newcontents = doc_sect.group(2)
+ newcontents = self.doc_sect.group(2)
if not newcontents:
newcontents = ""
self.dump_section()
--
2.53.0
^ permalink raw reply related [flat|nested] 21+ messages in thread* [PATCH v5 04/11] tools/kapi: add kernel API specification extraction tool
2026-10-08 8:49 [PATCH v5 00/11] Kernel API Specification Framework Sasha Levin
` (2 preceding siblings ...)
2026-10-08 8:49 ` [PATCH v5 03/11] kernel/api: add debugfs interface for kernel " Sasha Levin
@ 2026-10-08 8:49 ` Sasha Levin
2026-10-08 8:49 ` [PATCH v5 05/11] kernel/api: add API specification for sys_open Sasha Levin
` (6 subsequent siblings)
10 siblings, 0 replies; 21+ messages in thread
From: Sasha Levin @ 2026-10-08 8:49 UTC (permalink / raw)
To: linux-api, linux-kernel
Cc: Sasha Levin, 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, Gabriele Paoloni, Mauro Carvalho Chehab,
Christian Brauner, Alexander Viro, Andrew Morton, Masahiro Yamada,
Shuah Khan, Arnd Bergmann, Nathan Chancellor, Steven Rostedt,
Masami Hiramatsu, Mathieu Desnoyers
The kapi tool extracts and renders kernel API specifications from
three input sources and emits them in one of three output formats:
Input modes:
--source PATH parse kerneldoc blocks from a C source file or
directory
--vmlinux PATH decode the `.kapi_specs` ELF section from a
compiled kernel binary
--debugfs PATH read the spec dumps exposed under
/sys/kernel/debug/kapi/ on a running kernel
Output formats: plain, json, rst
The tool is written in Rust and builds with cargo from the crates
pinned in Cargo.lock; the resulting binary has no runtime
dependencies. It ships alongside the kernel to give documentation
tools, static analyzers, and IDE integrations a single entry point for
querying the spec data produced by the framework.
Assisted-by: LLM
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
Documentation/dev-tools/kernel-api-spec.rst | 74 +-
tools/kapi/.gitignore | 3 +
tools/kapi/Cargo.lock | 679 ++++
tools/kapi/Cargo.toml | 20 +
tools/kapi/Makefile | 33 +
tools/kapi/README.md | 33 +
| 1704 +++++++++
| 3335 +++++++++++++++++
| 442 +++
| 531 +++
| 461 +++
| 1160 ++++++
tools/kapi/src/formatter/json.rs | 659 ++++
tools/kapi/src/formatter/mod.rs | 220 ++
tools/kapi/src/formatter/plain.rs | 679 ++++
tools/kapi/src/formatter/rst.rs | 802 ++++
tools/kapi/src/main.rs | 123 +
17 files changed, 10886 insertions(+), 72 deletions(-)
create mode 100644 tools/kapi/.gitignore
create mode 100644 tools/kapi/Cargo.lock
create mode 100644 tools/kapi/Cargo.toml
create mode 100644 tools/kapi/Makefile
create mode 100644 tools/kapi/README.md
create mode 100644 tools/kapi/src/extractor/debugfs.rs
create mode 100644 tools/kapi/src/extractor/kerneldoc_parser.rs
create mode 100644 tools/kapi/src/extractor/mod.rs
create mode 100644 tools/kapi/src/extractor/source_parser.rs
create mode 100644 tools/kapi/src/extractor/vmlinux/binary_utils.rs
create mode 100644 tools/kapi/src/extractor/vmlinux/mod.rs
create mode 100644 tools/kapi/src/formatter/json.rs
create mode 100644 tools/kapi/src/formatter/mod.rs
create mode 100644 tools/kapi/src/formatter/plain.rs
create mode 100644 tools/kapi/src/formatter/rst.rs
create mode 100644 tools/kapi/src/main.rs
diff --git a/Documentation/dev-tools/kernel-api-spec.rst b/Documentation/dev-tools/kernel-api-spec.rst
index fa8aabb92a503..95b72060f8ef8 100644
--- a/Documentation/dev-tools/kernel-api-spec.rst
+++ b/Documentation/dev-tools/kernel-api-spec.rst
@@ -31,7 +31,8 @@ The framework aims to:
common programming errors during development and testing.
3. **Support Tooling**: Export API specifications in machine-readable formats for
- use by static analyzers, documentation generators, and development tools.
+ use by static analyzers, documentation generators, and development tools. See
+ `The kapi Tool`_.
4. **Formalize Contracts**: Explicitly document API contracts including parameter
constraints, execution contexts, locking requirements, and side effects.
@@ -573,77 +574,6 @@ The tool supports all KAPI specification types:
- System calls (kerneldoc annotations)
- Kernel functions (kerneldoc annotations with KAPI tags)
-IDE Integration
----------------
-
-Modern IDEs can use the specification data for:
-
-- Parameter hints
-- Type checking
-- Context validation
-- Error code documentation
-
-Best Practices
-==============
-
-Writing Specifications
-----------------------
-
-1. **Be Comprehensive**: Document all parameters, errors, and side effects
-2. **Keep Updated**: Update specs when API behavior changes
-3. **Use Examples**: Include usage examples in descriptions
-4. **Validate Constraints**: Define realistic constraints for parameters
-5. **Document Context**: Clearly specify allowed execution contexts
-
-Maintenance
------------
-
-1. **Version Specifications**: Increment version when API changes
-2. **Deprecation**: Mark deprecated APIs and suggest replacements
-3. **Cross-reference**: Link related APIs in descriptions
-4. **Test Specifications**: Verify specs match implementation
-
-Common Patterns
----------------
-
-**Optional Parameters**:
-
-.. code-block:: c
-
- /**
- * @optional_arg: Optional argument (may be NULL)
- *
- * param: optional_arg
- * type: KAPI_TYPE_PTR
- * flags: KAPI_PARAM_IN | KAPI_PARAM_OPTIONAL
- */
-
-**Buffer with Size Parameter**:
-
-.. code-block:: c
-
- /**
- * @buf: User-space buffer
- *
- * param: buf
- * type: KAPI_TYPE_USER_PTR
- * flags: KAPI_PARAM_OUT | KAPI_PARAM_USER
- * constraint-type: KAPI_CONSTRAINT_BUFFER
- * size-param: 2
- */
-
-**Callback Functions**:
-
-.. code-block:: c
-
- /**
- * @callback: Callback function
- *
- * param: callback
- * type: KAPI_TYPE_FUNC_PTR
- * flags: KAPI_PARAM_IN
- */
-
Troubleshooting
===============
diff --git a/tools/kapi/.gitignore b/tools/kapi/.gitignore
new file mode 100644
index 0000000000000..fa95032ed6c2a
--- /dev/null
+++ b/tools/kapi/.gitignore
@@ -0,0 +1,3 @@
+# Rust build artifacts
+/target/
+**/*.rs.bk
diff --git a/tools/kapi/Cargo.lock b/tools/kapi/Cargo.lock
new file mode 100644
index 0000000000000..23d4ef8b910d2
--- /dev/null
+++ b/tools/kapi/Cargo.lock
@@ -0,0 +1,679 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "goblin"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "983a6aafb3b12d4c41ea78d39e189af4298ce747353945ff5105b54a056e5cd9"
+dependencies = [
+ "log",
+ "plain",
+ "scroll",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.0",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "kapi"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "goblin",
+ "regex",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "walkdir",
+]
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libc"
+version = "0.2.185"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "plain"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scroll"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add"
+dependencies = [
+ "scroll_derive",
+]
+
+[[package]]
+name = "scroll_derive"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom",
+ "once_cell",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasip2"
+version = "1.0.3+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+dependencies = [
+ "wit-bindgen 0.57.1",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen 0.51.0",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/tools/kapi/Cargo.toml b/tools/kapi/Cargo.toml
new file mode 100644
index 0000000000000..159e65d324468
--- /dev/null
+++ b/tools/kapi/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "kapi"
+version = "0.1.0"
+edition = "2021"
+rust-version = "1.85"
+authors = ["Sasha Levin <sashal@kernel.org>"]
+description = "Tool for extracting and displaying kernel API specifications"
+license = "GPL-2.0"
+
+[dependencies]
+goblin = "0.10"
+clap = { version = "4.4", features = ["derive"] }
+anyhow = "1.0"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+regex = "1.10"
+walkdir = "2.4"
+
+[dev-dependencies]
+tempfile = "3.8"
diff --git a/tools/kapi/Makefile b/tools/kapi/Makefile
new file mode 100644
index 0000000000000..d4234538e4eee
--- /dev/null
+++ b/tools/kapi/Makefile
@@ -0,0 +1,33 @@
+# SPDX-License-Identifier: GPL-2.0
+# Makefile wrapper for the kapi tool (Rust userspace binary).
+#
+# See Documentation/dev-tools/kernel-api-spec.rst for details.
+
+PREFIX ?= /usr/local
+
+.PHONY: all build release debug clean install test fmt clippy
+
+all: release
+
+release:
+ cargo build --release
+
+build: release
+
+debug:
+ cargo build
+
+test:
+ cargo test
+
+fmt:
+ cargo fmt --all -- --check
+
+clippy:
+ cargo clippy --all-targets --all-features -- -D warnings
+
+clean:
+ cargo clean
+
+install: release
+ install -D -m 0755 target/release/kapi $(DESTDIR)$(PREFIX)/bin/kapi
diff --git a/tools/kapi/README.md b/tools/kapi/README.md
new file mode 100644
index 0000000000000..3839f3ff2964a
--- /dev/null
+++ b/tools/kapi/README.md
@@ -0,0 +1,33 @@
+# kapi — Kernel API Specification Extractor
+
+Userspace utility that extracts and displays kernel API specifications from
+three sources:
+
+- `--source PATH` — parse kerneldoc blocks in a C source file or tree
+- `--vmlinux PATH` — decode the `.kapi_specs` ELF section of a compiled vmlinux
+- `--debugfs PATH` — read the live specs from `/sys/kernel/debug/kapi/` on a
+ running kernel (this is also the mode used when no input option is given, in
+ which case PATH is `/sys/kernel/debug`)
+
+Output formats: `plain` (default), `json`, `rst`.
+
+See `Documentation/dev-tools/kernel-api-spec.rst` for the full user guide,
+including the kerneldoc DSL reference and the surrounding framework design.
+
+## Build
+
+```
+make -C tools/kapi
+```
+
+(wraps `cargo build --release`; the binary is produced at
+`tools/kapi/target/release/kapi`).
+
+## Usage
+
+```
+tools/kapi/target/release/kapi --help
+tools/kapi/target/release/kapi --source fs/open.c sys_open
+tools/kapi/target/release/kapi --vmlinux vmlinux -f json
+tools/kapi/target/release/kapi --debugfs /sys/kernel/debug
+```
--git a/tools/kapi/src/extractor/debugfs.rs b/tools/kapi/src/extractor/debugfs.rs
new file mode 100644
index 0000000000000..0a7e41e1c40ca
--- /dev/null
+++ b/tools/kapi/src/extractor/debugfs.rs
@@ -0,0 +1,1704 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+
+use crate::formatter::OutputFormatter;
+use anyhow::{bail, Context, Result};
+use serde::Deserialize;
+use std::fs;
+use std::io::Write;
+use std::path::PathBuf;
+
+use super::{
+ display_api_spec, ApiExtractor, ApiSpec, CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec,
+ ParamSpec, ReturnSpec, SignalMaskSpec, StateTransitionSpec, StructFieldSpec, StructSpec,
+};
+
+// Schema matching what kapi_export_json() in the kernel emits. The kernel
+// serialises several enum-like fields as hex strings ("0x%x") or token
+// strings ("exact", "process"); we keep them as Option<String> here and
+// interpret them during conversion.
+#[derive(Deserialize)]
+struct KernelApiJson {
+ name: String,
+ #[serde(default)]
+ api_type: Option<String>,
+ #[serde(default)]
+ version: Option<u32>,
+ #[serde(default)]
+ description: Option<String>,
+ #[serde(default)]
+ long_description: Option<String>,
+ #[serde(default)]
+ context_flags: Option<String>,
+ #[serde(default)]
+ examples: Option<String>,
+ #[serde(default)]
+ notes: Option<String>,
+ #[serde(default)]
+ capabilities: Option<Vec<KernelCapabilityJson>>,
+ #[serde(default)]
+ parameters: Option<Vec<KernelParamJson>>,
+ #[serde(default)]
+ errors: Option<Vec<KernelErrorJson>>,
+ #[serde(default, rename = "return")]
+ return_spec: Option<KernelReturnJson>,
+ #[serde(default)]
+ locks: Option<Vec<KernelLockJson>>,
+ #[serde(default)]
+ constraints: Option<Vec<KernelConstraintJson>>,
+ #[serde(default)]
+ signals: Option<Vec<KernelSignalJson>>,
+ #[serde(default)]
+ side_effects: Option<Vec<KernelSideEffectJson>>,
+ #[serde(default)]
+ state_transitions: Option<Vec<KernelStateTransitionJson>>,
+ #[serde(default)]
+ signal_masks: Option<Vec<KernelSignalMaskJson>>,
+ #[serde(default)]
+ struct_specs: Option<Vec<KernelStructJson>>,
+}
+
+#[derive(Deserialize)]
+struct KernelStateTransitionJson {
+ #[serde(default)]
+ object: Option<String>,
+ #[serde(default)]
+ from_state: Option<String>,
+ #[serde(default)]
+ to_state: Option<String>,
+ #[serde(default)]
+ condition: Option<String>,
+ #[serde(default)]
+ description: Option<String>,
+}
+
+#[derive(Deserialize)]
+struct KernelSignalMaskJson {
+ #[serde(default)]
+ name: Option<String>,
+ #[serde(default)]
+ description: Option<String>,
+ #[serde(default)]
+ signals: Vec<i32>,
+}
+
+#[derive(Deserialize)]
+struct KernelStructFieldJson {
+ #[serde(default)]
+ name: Option<String>,
+ #[serde(rename = "type", default)]
+ type_name: Option<String>,
+ #[serde(default)]
+ type_class: Option<String>,
+ #[serde(default)]
+ offset: usize,
+ #[serde(default)]
+ size: usize,
+ #[serde(default)]
+ flags: Option<String>,
+ #[serde(default)]
+ constraint_type: Option<String>,
+ #[serde(default)]
+ min_value: i64,
+ #[serde(default)]
+ max_value: i64,
+ #[serde(default)]
+ valid_mask: Option<String>,
+ #[serde(default)]
+ description: Option<String>,
+}
+
+#[derive(Deserialize)]
+struct KernelStructJson {
+ #[serde(default)]
+ name: Option<String>,
+ #[serde(default)]
+ size: usize,
+ #[serde(default)]
+ alignment: usize,
+ #[serde(default)]
+ description: Option<String>,
+ #[serde(default)]
+ fields: Vec<KernelStructFieldJson>,
+}
+
+#[derive(Deserialize)]
+struct KernelConstraintJson {
+ name: String,
+ #[serde(default)]
+ description: Option<String>,
+ #[serde(default)]
+ expression: Option<String>,
+}
+
+#[derive(Deserialize)]
+struct KernelSignalJson {
+ #[serde(default)]
+ signal_num: i32,
+ #[serde(default)]
+ signal_name: Option<String>,
+ #[serde(default)]
+ direction: Option<String>,
+ #[serde(default)]
+ action: u32,
+ #[serde(default)]
+ target: Option<String>,
+ #[serde(default)]
+ condition: Option<String>,
+ #[serde(default)]
+ description: Option<String>,
+ #[serde(default)]
+ restartable: bool,
+ #[serde(default)]
+ sa_flags_required: Option<String>,
+ #[serde(default)]
+ sa_flags_forbidden: Option<String>,
+ #[serde(default)]
+ error_on_signal: i32,
+ #[serde(default)]
+ transform_to: i32,
+ #[serde(default)]
+ timing: Option<String>,
+ #[serde(default)]
+ priority: u32,
+ #[serde(default)]
+ interruptible: bool,
+ #[serde(default)]
+ queue_behavior: Option<String>,
+ #[serde(default)]
+ state_required: Option<String>,
+ #[serde(default)]
+ state_forbidden: Option<String>,
+}
+
+#[derive(Deserialize)]
+struct KernelSideEffectJson {
+ #[serde(rename = "type", default)]
+ type_hex: Option<String>,
+ #[serde(default)]
+ target: Option<String>,
+ #[serde(default)]
+ condition: Option<String>,
+ #[serde(default)]
+ description: Option<String>,
+ #[serde(default)]
+ reversible: bool,
+}
+
+#[derive(Deserialize)]
+struct KernelParamJson {
+ name: String,
+ #[serde(rename = "type", default)]
+ type_name: Option<String>,
+ #[serde(default)]
+ type_class: Option<String>,
+ #[serde(default)]
+ description: Option<String>,
+ #[serde(default)]
+ flags: Option<String>,
+ #[serde(default)]
+ constraint_type: Option<String>,
+ #[serde(default)]
+ constraint_desc: Option<String>,
+ #[serde(default)]
+ min_value: Option<i64>,
+ #[serde(default)]
+ max_value: Option<i64>,
+ #[serde(default)]
+ valid_mask: Option<String>,
+ #[serde(default)]
+ enum_values: Vec<i64>,
+ #[serde(default)]
+ size: Option<u64>,
+ #[serde(default)]
+ alignment: Option<u64>,
+ #[serde(default)]
+ size_param_idx: Option<u32>,
+}
+
+#[derive(Deserialize)]
+struct KernelErrorJson {
+ #[serde(rename = "code")]
+ error_code: i32,
+ #[serde(default)]
+ name: Option<String>,
+ #[serde(default)]
+ condition: Option<String>,
+ #[serde(default)]
+ description: Option<String>,
+}
+
+#[derive(Deserialize)]
+struct KernelReturnJson {
+ #[serde(rename = "type", default)]
+ type_name: Option<String>,
+ #[serde(default)]
+ type_class: Option<String>,
+ #[serde(default)]
+ check_type: Option<String>,
+ #[serde(default)]
+ description: Option<String>,
+ #[serde(default)]
+ success_value: Option<i64>,
+ #[serde(default)]
+ success_min: Option<i64>,
+ #[serde(default)]
+ success_max: Option<i64>,
+ #[serde(default)]
+ error_values: Vec<i64>,
+}
+
+#[derive(Deserialize)]
+struct KernelLockJson {
+ name: String,
+ #[serde(rename = "type", default)]
+ lock_type: Option<String>,
+ #[serde(default)]
+ scope: Option<String>,
+ #[serde(default)]
+ description: Option<String>,
+}
+
+#[derive(Deserialize)]
+struct KernelCapabilityJson {
+ capability: i32,
+ name: String,
+ action: String,
+ allows: String,
+ without_cap: String,
+ check_condition: Option<String>,
+ priority: Option<u8>,
+ alternatives: Option<Vec<i32>>,
+}
+
+/// Free-text fields of the text dump that may span several lines.
+#[derive(Clone, Copy)]
+enum TextField {
+ Description,
+ LongDescription,
+ Examples,
+ Notes,
+}
+
+impl TextField {
+ /// Recognise the header line of a text field, returning the field and
+ /// whatever follows the colon on the same line.
+ fn start(line: &str) -> Option<(Self, &str)> {
+ [
+ ("Description:", Self::Description),
+ ("Long description:", Self::LongDescription),
+ ("Examples:", Self::Examples),
+ ("Notes:", Self::Notes),
+ ]
+ .into_iter()
+ .find_map(|(header, field)| {
+ line.strip_prefix(header)
+ .map(|rest| (field, rest.trim_start()))
+ })
+ }
+
+ fn store(self, spec: &mut ApiSpec, lines: &[String]) {
+ let text = lines.join("\n");
+ let text = text.trim_matches('\n').trim_end().to_string();
+ let slot = match self {
+ Self::Description => &mut spec.description,
+ Self::LongDescription => &mut spec.long_description,
+ Self::Examples => &mut spec.examples,
+ Self::Notes => &mut spec.notes,
+ };
+ *slot = Some(text);
+ }
+}
+
+/// Extractor for kernel API specifications from debugfs
+pub struct DebugfsExtractor {
+ debugfs_path: PathBuf,
+}
+
+impl DebugfsExtractor {
+ /// Create a new debugfs extractor with the specified debugfs path
+ pub fn new(debugfs_path: Option<String>) -> Result<Self> {
+ let path = match debugfs_path {
+ Some(p) => PathBuf::from(p),
+ None => PathBuf::from("/sys/kernel/debug"),
+ };
+
+ // Check if the debugfs path exists
+ if !path.exists() {
+ bail!("Debugfs path does not exist: {}", path.display());
+ }
+
+ // Check if kapi directory exists
+ let kapi_path = path.join("kapi");
+ if !kapi_path.exists() {
+ bail!(
+ "Kernel API debugfs interface not found at: {}",
+ kapi_path.display()
+ );
+ }
+
+ Ok(Self { debugfs_path: path })
+ }
+
+ /// Parse the list file to get all available API names
+ fn parse_list_file(&self) -> Result<Vec<String>> {
+ let list_path = self.debugfs_path.join("kapi/list");
+ let content = fs::read_to_string(&list_path)
+ .with_context(|| format!("Failed to read {}", list_path.display()))?;
+
+ let mut apis = Vec::new();
+ let mut in_list = false;
+
+ for line in content.lines() {
+ if line.contains("===") {
+ in_list = true;
+ continue;
+ }
+
+ if in_list && line.starts_with("Total:") {
+ break;
+ }
+
+ if in_list && !line.trim().is_empty() {
+ // Extract API name from lines like "sys_read - Read from a file descriptor"
+ if let Some(name) = line.split(" - ").next() {
+ apis.push(name.trim().to_string());
+ }
+ }
+ }
+
+ Ok(apis)
+ }
+
+ /// Convert context flags (emitted by the kernel as a hex string like
+ /// "0x21") into the token list consumed by the formatter.
+ fn parse_context_flags(flags: &str) -> Vec<String> {
+ let mut result = Vec::new();
+ let bits = flags
+ .strip_prefix("0x")
+ .or_else(|| flags.strip_prefix("0X"))
+ .unwrap_or(flags);
+ let Ok(flags) = u32::from_str_radix(bits, 16) else {
+ return result;
+ };
+
+ // These values should match KAPI_CTX_* flags from kernel
+ if flags & (1 << 0) != 0 {
+ result.push("KAPI_CTX_PROCESS".to_string());
+ }
+ if flags & (1 << 1) != 0 {
+ result.push("KAPI_CTX_SOFTIRQ".to_string());
+ }
+ if flags & (1 << 2) != 0 {
+ result.push("KAPI_CTX_HARDIRQ".to_string());
+ }
+ if flags & (1 << 3) != 0 {
+ result.push("KAPI_CTX_NMI".to_string());
+ }
+ if flags & (1 << 4) != 0 {
+ result.push("KAPI_CTX_ATOMIC".to_string());
+ }
+ if flags & (1 << 5) != 0 {
+ result.push("KAPI_CTX_SLEEPABLE".to_string());
+ }
+ if flags & (1 << 6) != 0 {
+ result.push("KAPI_CTX_PREEMPT_DISABLED".to_string());
+ }
+ if flags & (1 << 7) != 0 {
+ result.push("KAPI_CTX_IRQ_DISABLED".to_string());
+ }
+
+ result
+ }
+
+ /// Parse a hex-string like "0x123" into u64, returning 0 on failure.
+ fn parse_hex_u64(value: &str) -> u64 {
+ let bits = value
+ .strip_prefix("0x")
+ .or_else(|| value.strip_prefix("0X"))
+ .unwrap_or(value);
+ u64::from_str_radix(bits, 16).unwrap_or(0)
+ }
+
+ /// Parse a hex-string like "0x123" into u32, returning 0 on failure.
+ fn parse_hex_u32(value: &str) -> u32 {
+ u32::try_from(Self::parse_hex_u64(value)).unwrap_or(0)
+ }
+
+ /// Map the type-class token emitted by the kernel (param_type_to_string)
+ /// back to the numeric enum kapi_param_type.
+ fn parse_type_class(token: &str) -> u32 {
+ match token {
+ "void" => 0,
+ "int" => 1,
+ "uint" => 2,
+ "pointer" => 3,
+ "struct" => 4,
+ "union" => 5,
+ "enum" => 6,
+ "function_pointer" => 7,
+ "array" => 8,
+ "file_descriptor" => 9,
+ "user_pointer" => 10,
+ "pathname" => 11,
+ "custom" => 12,
+ _ => 0,
+ }
+ }
+
+ /// Map the constraint-type token emitted by the kernel
+ /// (constraint_type_to_string) back to the numeric enum
+ /// kapi_constraint_type.
+ fn parse_constraint_type(token: &str) -> u32 {
+ match token {
+ "none" => 0,
+ "range" => 1,
+ "mask" => 2,
+ "enum" => 3,
+ "alignment" => 4,
+ "power_of_two" => 5,
+ "page_aligned" => 6,
+ "nonzero" => 7,
+ "user_string" => 8,
+ "user_path" => 9,
+ "user_ptr" => 10,
+ "buffer" => 11,
+ "custom" => 12,
+ _ => 0,
+ }
+ }
+
+ /// Derive the API type from the symbol name, the same way the vmlinux
+ /// extractor does.
+ fn api_type_from_name(name: &str) -> &'static str {
+ if name.starts_with("sys_") {
+ "syscall"
+ } else if name.ends_with("_ioctl") {
+ "ioctl"
+ } else if name.contains("sysfs") {
+ "sysfs"
+ } else {
+ "function"
+ }
+ }
+
+ /// Map the check-type token emitted by the kernel
+ /// (return_check_type_to_string) back to the u32 enum the formatter wants.
+ fn parse_check_type(token: &str) -> u32 {
+ match token {
+ "exact" => 0,
+ "range" => 1,
+ "error_check" => 2,
+ "file_descriptor" => 3,
+ "custom" => 4,
+ "no_return" => 5,
+ _ => 0,
+ }
+ }
+
+ /// Map the lock-type token emitted by the kernel (lock_type_to_string).
+ fn parse_lock_type(token: &str) -> u32 {
+ match token {
+ "none" => 0,
+ "mutex" => 1,
+ "spinlock" => 2,
+ "rwlock" => 3,
+ "seqlock" => 4,
+ "rcu" => 5,
+ "semaphore" => 6,
+ "custom" => 7,
+ _ => 0,
+ }
+ }
+
+ /// Map the lock-scope token emitted by the kernel (lock_scope_to_string).
+ fn parse_lock_scope(token: &str) -> u32 {
+ match token {
+ "internal" => 0,
+ "acquires" => 1,
+ "releases" => 2,
+ "caller_held" => 3,
+ _ => 0,
+ }
+ }
+
+ /// Map the signal-timing token (e.g. "during") back to the u32 enum that
+ /// the source-side parser produces. Mirrors
+ /// KerneldocParser::parse_signal_timing in kerneldoc_parser.rs so the
+ /// debugfs path and the source path agree.
+ fn parse_signal_timing(token: &str) -> u32 {
+ match token.trim().to_ascii_lowercase().as_str() {
+ "before" => 0,
+ "during" => 1,
+ "after" => 2,
+ _ => 0,
+ }
+ }
+
+ /// Convert the capability action token emitted by the kernel
+ /// (capability_action_to_string) to the KAPI_CAP_* spelling used by the
+ /// other extractors.
+ fn parse_capability_action(action: &str) -> String {
+ match action {
+ "bypass_check" => "KAPI_CAP_BYPASS_CHECK".to_string(),
+ "increase_limit" => "KAPI_CAP_INCREASE_LIMIT".to_string(),
+ "override_restriction" => "KAPI_CAP_OVERRIDE_RESTRICTION".to_string(),
+ "grant_permission" => "KAPI_CAP_GRANT_PERMISSION".to_string(),
+ "modify_behavior" => "KAPI_CAP_MODIFY_BEHAVIOR".to_string(),
+ "access_resource" => "KAPI_CAP_ACCESS_RESOURCE".to_string(),
+ "perform_operation" => "KAPI_CAP_PERFORM_OPERATION".to_string(),
+ _ => action.to_string(),
+ }
+ }
+
+ /// Try to parse as JSON first
+ fn try_parse_json(&self, content: &str) -> Result<ApiSpec, serde_json::Error> {
+ let json_data: KernelApiJson = serde_json::from_str(content)?;
+ // The kernel-side kapi_json_str() emits NULL char * as the empty
+ // string "", so normalise empty -> None to match the ApiSpec
+ // convention used by --source / --vmlinux.
+ fn opt_str(s: Option<String>) -> Option<String> {
+ s.filter(|v| !v.is_empty())
+ }
+ let api_type = json_data
+ .api_type
+ .unwrap_or_else(|| Self::api_type_from_name(&json_data.name).to_string());
+
+ let mut spec = ApiSpec {
+ name: json_data.name,
+ api_type,
+ description: opt_str(json_data.description),
+ long_description: opt_str(json_data.long_description),
+ version: json_data.version.map(|v| v.to_string()),
+ context_flags: json_data
+ .context_flags
+ .as_deref()
+ .map_or_else(Vec::new, Self::parse_context_flags),
+ param_count: None,
+ error_count: None,
+ examples: opt_str(json_data.examples),
+ notes: opt_str(json_data.notes),
+ subsystem: None, // Not in the JSON format
+ sysfs_path: None, // Not in the JSON format
+ permissions: None, // Not in the JSON format
+ capabilities: vec![],
+ parameters: vec![],
+ return_spec: None,
+ errors: vec![],
+ signals: vec![],
+ signal_masks: vec![],
+ side_effects: vec![],
+ state_transitions: vec![],
+ constraints: vec![],
+ locks: vec![],
+ struct_specs: vec![],
+ };
+
+ // Convert capabilities
+ if let Some(caps) = json_data.capabilities {
+ for cap in caps {
+ spec.capabilities.push(CapabilitySpec {
+ capability: cap.capability,
+ name: cap.name,
+ action: Self::parse_capability_action(&cap.action),
+ allows: cap.allows,
+ without_cap: cap.without_cap,
+ check_condition: opt_str(cap.check_condition),
+ priority: cap.priority,
+ alternatives: cap.alternatives.unwrap_or_default(),
+ });
+ }
+ }
+
+ // Convert parameters.
+ if let Some(params) = json_data.parameters {
+ for (i, p) in params.into_iter().enumerate() {
+ let flags = p.flags.as_deref().map_or(0, Self::parse_hex_u32);
+ let mut param = ParamSpec {
+ index: i as u32,
+ name: p.name,
+ type_name: p.type_name.unwrap_or_default(),
+ description: p.description.unwrap_or_default(),
+ flags,
+ param_type: p.type_class.as_deref().map_or(0, Self::parse_type_class),
+ constraint_type: p
+ .constraint_type
+ .as_deref()
+ .map_or(0, Self::parse_constraint_type),
+ constraint: opt_str(p.constraint_desc),
+ min_value: p.min_value,
+ max_value: p.max_value,
+ valid_mask: p.valid_mask.as_deref().map(Self::parse_hex_u64),
+ enum_values: p.enum_values.iter().map(i64::to_string).collect(),
+ size: p.size.map(|v| v as u32),
+ alignment: p.alignment.map(|v| v as u32),
+ size_param_idx: p.size_param_idx,
+ };
+ param.keep_used_numbers();
+ spec.parameters.push(param);
+ }
+ if !spec.parameters.is_empty() {
+ spec.param_count = Some(spec.parameters.len() as u32);
+ }
+ }
+
+ // Convert errors
+ if let Some(errors) = json_data.errors {
+ for e in errors {
+ spec.errors.push(ErrorSpec {
+ error_code: e.error_code,
+ name: e.name.unwrap_or_default(),
+ condition: e.condition.unwrap_or_default(),
+ description: e.description.unwrap_or_default(),
+ });
+ }
+ if !spec.errors.is_empty() {
+ spec.error_count = Some(spec.errors.len() as u32);
+ }
+ }
+
+ // Convert return spec
+ if let Some(ret) = json_data.return_spec {
+ let check_type = ret.check_type.as_deref().map_or(0, Self::parse_check_type);
+ let return_type = ret.type_class.as_deref().map_or(0, Self::parse_type_class);
+ let type_name = ret.type_name.unwrap_or_default();
+ let success_value = ret.success_value.unwrap_or(0);
+ // A spec without a return block exports as an all-zero return
+ // spec; report it as absent like the vmlinux extractor does.
+ if !(type_name.is_empty() && return_type == 0 && check_type == 0 && success_value == 0)
+ {
+ let mut ret_spec = ReturnSpec {
+ type_name,
+ description: ret.description.unwrap_or_default(),
+ return_type,
+ check_type,
+ success_value: ret.success_value,
+ success_min: ret.success_min,
+ success_max: ret.success_max,
+ error_values: ret
+ .error_values
+ .into_iter()
+ .filter_map(|v| i32::try_from(v).ok())
+ .collect(),
+ };
+ ret_spec.keep_used_success_fields();
+ spec.return_spec = Some(ret_spec);
+ }
+ }
+
+ // Convert locks
+ if let Some(locks) = json_data.locks {
+ for l in locks {
+ let lock_type = l.lock_type.as_deref().map_or(0, Self::parse_lock_type);
+ let scope = l.scope.as_deref().map_or(0, Self::parse_lock_scope);
+ spec.locks.push(LockSpec {
+ lock_name: l.name,
+ lock_type,
+ scope,
+ description: l.description.unwrap_or_default(),
+ });
+ }
+ }
+
+ // Convert constraints. Empty strings emitted from kapi_json_str()
+ // for NULL char * fields normalise back to None to match --source.
+ if let Some(constraints) = json_data.constraints {
+ for c in constraints {
+ spec.constraints.push(ConstraintSpec {
+ name: c.name,
+ description: c.description.unwrap_or_default(),
+ expression: c.expression.filter(|v| !v.is_empty()),
+ });
+ }
+ }
+
+ // Convert signals.
+ if let Some(signals) = json_data.signals {
+ for s in signals {
+ let direction = s.direction.as_deref().map_or(0, Self::parse_hex_u32);
+ let sa_flags_required = s
+ .sa_flags_required
+ .as_deref()
+ .map_or(0, Self::parse_hex_u32);
+ let sa_flags_forbidden = s
+ .sa_flags_forbidden
+ .as_deref()
+ .map_or(0, Self::parse_hex_u32);
+ let state_required = s.state_required.as_deref().map_or(0, Self::parse_hex_u32);
+ let state_forbidden = s.state_forbidden.as_deref().map_or(0, Self::parse_hex_u32);
+ let timing = s.timing.as_deref().map_or(0, Self::parse_signal_timing);
+ spec.signals.push(super::SignalSpec {
+ signal_num: s.signal_num,
+ signal_name: s.signal_name.unwrap_or_default(),
+ direction,
+ action: s.action,
+ target: opt_str(s.target),
+ condition: opt_str(s.condition),
+ description: opt_str(s.description),
+ timing,
+ priority: s.priority,
+ restartable: s.restartable,
+ interruptible: s.interruptible,
+ queue: opt_str(s.queue_behavior),
+ sa_flags: 0,
+ sa_flags_required,
+ sa_flags_forbidden,
+ state_required,
+ state_forbidden,
+ error_on_signal: if s.error_on_signal != 0 {
+ Some(s.error_on_signal)
+ } else {
+ None
+ },
+ transform_to: if s.transform_to != 0 {
+ // Kernel JSON already carries the numeric value.
+ Some(s.transform_to)
+ } else {
+ None
+ },
+ });
+ }
+ }
+
+ // Convert side effects.
+ if let Some(effects) = json_data.side_effects {
+ for e in effects {
+ let effect_type = e.type_hex.as_deref().map_or(0, Self::parse_hex_u32);
+ spec.side_effects.push(super::SideEffectSpec {
+ effect_type,
+ target: e.target.unwrap_or_default(),
+ condition: e.condition.filter(|v| !v.is_empty()),
+ description: e.description.unwrap_or_default(),
+ reversible: e.reversible,
+ });
+ }
+ }
+
+ if let Some(transitions) = json_data.state_transitions {
+ for t in transitions {
+ spec.state_transitions.push(StateTransitionSpec {
+ object: t.object.unwrap_or_default(),
+ from_state: t.from_state.unwrap_or_default(),
+ to_state: t.to_state.unwrap_or_default(),
+ condition: opt_str(t.condition),
+ description: t.description.unwrap_or_default(),
+ });
+ }
+ }
+
+ if let Some(masks) = json_data.signal_masks {
+ for m in masks {
+ spec.signal_masks.push(SignalMaskSpec {
+ name: m.name.unwrap_or_default(),
+ description: m.description.unwrap_or_default(),
+ signals: m.signals,
+ });
+ }
+ }
+
+ if let Some(structs) = json_data.struct_specs {
+ for st in structs {
+ let fields: Vec<StructFieldSpec> = st
+ .fields
+ .into_iter()
+ .map(|f| StructFieldSpec {
+ name: f.name.unwrap_or_default(),
+ field_type: f.type_class.as_deref().map_or(0, Self::parse_type_class),
+ type_name: f.type_name.unwrap_or_default(),
+ offset: f.offset,
+ size: f.size,
+ flags: f.flags.as_deref().map_or(0, Self::parse_hex_u32),
+ constraint_type: f
+ .constraint_type
+ .as_deref()
+ .map_or(0, Self::parse_constraint_type),
+ min_value: f.min_value,
+ max_value: f.max_value,
+ valid_mask: f.valid_mask.as_deref().map_or(0, Self::parse_hex_u64),
+ description: f.description.unwrap_or_default(),
+ })
+ .collect();
+ spec.struct_specs.push(StructSpec {
+ name: st.name.unwrap_or_default(),
+ size: st.size,
+ alignment: st.alignment,
+ field_count: fields.len() as u32,
+ fields,
+ description: st.description.unwrap_or_default(),
+ });
+ }
+ }
+
+ Ok(spec)
+ }
+
+ /// Parse a single API specification file
+ fn parse_spec_file(&self, api_name: &str) -> Result<ApiSpec> {
+ // Prefer the JSON endpoint; fall back to the plain-text dump under
+ // kapi/specs/ if it is missing or does not parse.
+ let json_path = self
+ .debugfs_path
+ .join(format!("kapi/specs-json/{}", api_name));
+ match fs::read_to_string(&json_path) {
+ Ok(content) => match self.try_parse_json(&content) {
+ Ok(spec) => return Ok(spec),
+ Err(e) => eprintln!(
+ "Warning: invalid JSON in {}: {}; using the text dump",
+ json_path.display(),
+ e
+ ),
+ },
+ Err(e) if e.kind() != std::io::ErrorKind::NotFound => eprintln!(
+ "Warning: cannot read {}: {}; using the text dump",
+ json_path.display(),
+ e
+ ),
+ Err(_) => {}
+ }
+
+ let spec_path = self.debugfs_path.join(format!("kapi/specs/{}", api_name));
+ let content = fs::read_to_string(&spec_path)
+ .with_context(|| format!("Failed to read {}", spec_path.display()))?;
+
+ // The specs/ file may hold JSON as well.
+ if let Ok(spec) = self.try_parse_json(&content) {
+ return Ok(spec);
+ }
+
+ // Fall back to plain text parsing
+ let mut spec = ApiSpec {
+ name: api_name.to_string(),
+ api_type: "unknown".to_string(),
+ description: None,
+ long_description: None,
+ version: None,
+ context_flags: Vec::new(),
+ param_count: None,
+ error_count: None,
+ examples: None,
+ notes: None,
+ subsystem: None,
+ sysfs_path: None,
+ permissions: None,
+ capabilities: vec![],
+ parameters: vec![],
+ return_spec: None,
+ errors: vec![],
+ signals: vec![],
+ signal_masks: vec![],
+ side_effects: vec![],
+ state_transitions: vec![],
+ constraints: vec![],
+ locks: vec![],
+ struct_specs: vec![],
+ };
+
+ // Parse the content
+ let mut text_field: Option<TextField> = None;
+ let mut text_lines: Vec<String> = Vec::new();
+ let mut parsing_capability = false;
+ let mut in_capabilities_section = false;
+ let mut current_capability: Option<CapabilitySpec> = None;
+
+ for line in content.lines() {
+ // The kernel indents every line of a multi-line value, so a value
+ // ends at the first non-blank line that is not indented.
+ if let Some(field) = text_field {
+ if line.is_empty() {
+ text_lines.push(String::new());
+ continue;
+ }
+ if let Some(rest) = line.strip_prefix(" ") {
+ text_lines.push(rest.to_string());
+ continue;
+ }
+ field.store(&mut spec, &text_lines);
+ text_field = None;
+ }
+
+ // Handle capability sections
+ if line.starts_with("Capabilities (") {
+ in_capabilities_section = true;
+ continue;
+ }
+ // Any other top-level section header ends the capabilities section
+ // so that " pending_signals (0):" inside "Signal handling (1):"
+ // isn't mis-parsed as a capability entry.
+ if !line.starts_with(' ') && !line.is_empty() && line.ends_with(':') {
+ in_capabilities_section = false;
+ }
+ if in_capabilities_section
+ && line.starts_with(" ")
+ && line.contains(" (")
+ && line.ends_with("):")
+ {
+ // Start of a capability entry like " CAP_IPC_LOCK (14):"
+ if let Some(cap) = current_capability.take() {
+ spec.capabilities.push(cap);
+ }
+
+ let parts: Vec<&str> = line.trim().split(" (").collect();
+ if parts.len() == 2 {
+ let cap_name = parts[0].to_string();
+ let cap_id = parts[1].trim_end_matches("):").parse().unwrap_or(0);
+ current_capability = Some(CapabilitySpec {
+ capability: cap_id,
+ name: cap_name,
+ action: String::new(),
+ allows: String::new(),
+ without_cap: String::new(),
+ check_condition: None,
+ priority: None,
+ alternatives: Vec::new(),
+ });
+ parsing_capability = true;
+ }
+ continue;
+ }
+ if parsing_capability && line.starts_with(" ") {
+ // Parse capability fields
+ if let Some(ref mut cap) = current_capability {
+ if let Some(action) = line.strip_prefix(" Action: ") {
+ cap.action = action.to_string();
+ } else if let Some(allows) = line.strip_prefix(" Allows: ") {
+ cap.allows = allows.to_string();
+ } else if let Some(without) = line.strip_prefix(" Without: ") {
+ cap.without_cap = without.to_string();
+ } else if let Some(cond) = line.strip_prefix(" Condition: ") {
+ cap.check_condition = Some(cond.to_string());
+ } else if let Some(prio) = line.strip_prefix(" Priority: ") {
+ cap.priority = prio.parse().ok();
+ } else if let Some(alts) = line.strip_prefix(" Alternatives: ") {
+ cap.alternatives =
+ alts.split(", ").filter_map(|s| s.parse().ok()).collect();
+ }
+ }
+ continue;
+ }
+ if parsing_capability && !line.starts_with(" ") {
+ // End of capabilities section
+ if let Some(cap) = current_capability.take() {
+ spec.capabilities.push(cap);
+ }
+ parsing_capability = false;
+ }
+
+ // Handle section headers
+ if line.starts_with("Parameters (") {
+ if let Some(count_str) = line
+ .strip_prefix("Parameters (")
+ .and_then(|s| s.strip_suffix("):"))
+ {
+ spec.param_count = count_str.parse().ok();
+ }
+ continue;
+ } else if line.starts_with("Errors (") {
+ if let Some(count_str) = line
+ .strip_prefix("Errors (")
+ .and_then(|s| s.strip_suffix("):"))
+ {
+ spec.error_count = count_str.parse().ok();
+ }
+ continue;
+ }
+
+ // Parse regular fields
+ if let Some((field, first)) = TextField::start(line) {
+ text_field = Some(field);
+ text_lines.clear();
+ if !first.is_empty() {
+ text_lines.push(first.to_string());
+ }
+ } else if let Some(version) = line.strip_prefix("Version: ") {
+ spec.version = Some(version.to_string());
+ } else if let Some(flags) = line.strip_prefix("Context flags: ") {
+ spec.context_flags = flags
+ .split_whitespace()
+ .map(|f| format!("KAPI_CTX_{f}"))
+ .collect();
+ } else if let Some(subsys) = line.strip_prefix("Subsystem: ") {
+ spec.subsystem = Some(subsys.to_string());
+ } else if let Some(path) = line.strip_prefix("Sysfs Path: ") {
+ spec.sysfs_path = Some(path.to_string());
+ } else if let Some(perms) = line.strip_prefix("Permissions: ") {
+ spec.permissions = Some(perms.to_string());
+ }
+ }
+
+ if let Some(field) = text_field {
+ field.store(&mut spec, &text_lines);
+ }
+
+ // Handle any remaining capability
+ if let Some(cap) = current_capability.take() {
+ spec.capabilities.push(cap);
+ }
+
+ // Determine API type based on name
+ if api_name.starts_with("sys_") {
+ spec.api_type = "syscall".to_string();
+ } else if api_name.contains("_ioctl") || api_name.starts_with("ioctl_") {
+ spec.api_type = "ioctl".to_string();
+ } else if api_name.contains("sysfs")
+ || api_name.ends_with("_show")
+ || api_name.ends_with("_store")
+ {
+ spec.api_type = "sysfs".to_string();
+ } else {
+ spec.api_type = "function".to_string();
+ }
+
+ Ok(spec)
+ }
+}
+
+impl ApiExtractor for DebugfsExtractor {
+ fn extract_all(&self) -> Result<Vec<ApiSpec>> {
+ let api_names = self.parse_list_file()?;
+ let mut specs = Vec::new();
+
+ for name in api_names {
+ match self.parse_spec_file(&name) {
+ Ok(spec) => specs.push(spec),
+ Err(e) => {
+ eprintln!("Warning: failed to parse API spec '{}': {}", name, e);
+ }
+ }
+ }
+
+ Ok(specs)
+ }
+
+ fn extract_by_name(&self, name: &str) -> Result<Option<ApiSpec>> {
+ let api_names = self.parse_list_file()?;
+
+ if api_names.contains(&name.to_string()) {
+ Ok(Some(self.parse_spec_file(name)?))
+ } else {
+ Ok(None)
+ }
+ }
+
+ fn display_api_details(
+ &self,
+ api_name: &str,
+ formatter: &mut dyn OutputFormatter,
+ writer: &mut dyn Write,
+ ) -> Result<()> {
+ if let Some(spec) = self.extract_by_name(api_name)? {
+ display_api_spec(&spec, formatter, writer)?;
+ } else {
+ writeln!(writer, "API '{api_name}' not found in debugfs")?;
+ }
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ // Shortened sys_read dump as emitted by kapi_export_json().
+ const SYS_READ_JSON: &str = r#"{
+ "name": "sys_read",
+ "version": 1,
+ "description": "Read data from a file descriptor",
+ "long_description": "",
+ "context_flags": "0x21",
+ "parameters": [
+ {
+ "name": "fd",
+ "type": "unsigned int fd",
+ "type_class": "file_descriptor",
+ "flags": "0x1",
+ "description": "File descriptor to read from ",
+ "constraint_type": "range",
+ "constraint_desc": "Must be a valid, open file descriptor",
+ "min_value": 0,
+ "max_value": 2147483647,
+ "valid_mask": "0x0",
+ "enum_values": [],
+ "size": 0,
+ "alignment": 0,
+ "size_param_idx": null,
+ "size_multiplier": 0
+ },
+ {
+ "name": "buf",
+ "type": "char __user * buf",
+ "type_class": "user_pointer",
+ "flags": "0x42",
+ "description": "User-space buffer to read data into ",
+ "constraint_type": "buffer",
+ "constraint_desc": "Must point to a writable user-space region",
+ "min_value": 0,
+ "max_value": 0,
+ "valid_mask": "0x0",
+ "enum_values": [],
+ "size": 0,
+ "alignment": 0,
+ "size_param_idx": 2,
+ "size_multiplier": 0
+ },
+ {
+ "name": "count",
+ "type": "size_t count",
+ "type_class": "uint",
+ "flags": "0x1",
+ "description": "Maximum number of bytes to read ",
+ "constraint_type": "none",
+ "constraint_desc": "",
+ "min_value": 0,
+ "max_value": 0,
+ "valid_mask": "0x0",
+ "enum_values": [],
+ "size": 0,
+ "alignment": 0,
+ "size_param_idx": null,
+ "size_multiplier": 0
+ }
+ ],
+ "return": {
+ "type": "KAPI_TYPE_INT",
+ "type_class": "int",
+ "check_type": "range",
+ "success_value": 0,
+ "success_min": 0,
+ "success_max": 9223372036854775807,
+ "error_values": [],
+ "description": "Number of bytes read"
+ },
+ "errors": [
+ { "code": -9, "name": "EBADF", "condition": "Bad file descriptor",
+ "description": "fd is not valid" }
+ ],
+ "locks": [
+ { "name": "file->f_pos_lock", "type": "mutex", "scope": "internal",
+ "description": "Position lock" }
+ ],
+ "capabilities": [
+ {
+ "capability": 1,
+ "name": "CAP_DAC_OVERRIDE",
+ "action": "bypass_check",
+ "allows": "Bypass read permission checks",
+ "without_cap": "Standard DAC checks are enforced",
+ "check_condition": "",
+ "priority": 0
+ }
+ ],
+ "constraints": [
+ { "name": "MAX_RW_COUNT",
+ "description": "Count is clamped", "expression": "min(count, MAX_RW_COUNT)" }
+ ],
+ "signals": [
+ {
+ "signal_num": 0,
+ "signal_name": "Any signal",
+ "direction": "0x1",
+ "action": 6,
+ "target": "",
+ "condition": "When blocked waiting for data",
+ "description": "May be interrupted",
+ "restartable": true,
+ "sa_flags_required": "0x0",
+ "sa_flags_forbidden": "0x0",
+ "error_on_signal": -4,
+ "transform_to": 0,
+ "timing": "during",
+ "priority": 0,
+ "interruptible": false,
+ "queue_behavior": "",
+ "state_required": "0x0",
+ "state_forbidden": "0x0"
+ }
+ ],
+ "side_effects": [
+ { "type": "0x10", "target": "file->f_pos", "condition": "",
+ "description": "Offset advances", "reversible": false }
+ ],
+ "state_transitions": [],
+ "signal_masks": [],
+ "struct_specs": [],
+ "examples": "",
+ "notes": ""
+}
+"#;
+
+ // Synthetic dump exercising the fields sys_read leaves empty: enum
+ // values, error values, struct specs, signal masks, state transitions.
+ const FULL_JSON: &str = r#"{
+ "name": "tmp_full",
+ "version": 1,
+ "description": "Full \"feature\" test\tline\nbreak\u0001",
+ "long_description": "",
+ "context_flags": "0x91",
+ "parameters": [
+ {
+ "name": "mode",
+ "type": "int mode",
+ "type_class": "enum",
+ "flags": "0x1",
+ "description": "Enumerated parameter",
+ "constraint_type": "enum",
+ "constraint_desc": "",
+ "min_value": 0,
+ "max_value": 0,
+ "valid_mask": "0x0",
+ "enum_values": [1, 2, -3, 1000000000000],
+ "size": 4096,
+ "alignment": 64,
+ "size_param_idx": null,
+ "size_multiplier": 0
+ },
+ {
+ "name": "bits",
+ "type": "u64 bits",
+ "type_class": "uint",
+ "flags": "0x1",
+ "description": "Masked parameter",
+ "constraint_type": "mask",
+ "constraint_desc": "",
+ "min_value": 0,
+ "max_value": 0,
+ "valid_mask": "0xffffffffffffffff",
+ "enum_values": [],
+ "size": 0,
+ "alignment": 0,
+ "size_param_idx": null,
+ "size_multiplier": 0
+ }
+ ],
+ "return": {
+ "type": "long",
+ "type_class": "int",
+ "check_type": "error_check",
+ "success_value": 0,
+ "success_min": 0,
+ "success_max": 0,
+ "error_values": [-1, -22, -4095],
+ "description": "Error-checked return"
+ },
+ "errors": [],
+ "locks": [
+ { "name": "tmp_lock", "type": "seqlock", "scope": "acquires", "description": "a seqlock" }
+ ],
+ "capabilities": [
+ {
+ "capability": 21,
+ "name": "CAP_SYS_ADMIN",
+ "action": "perform_operation",
+ "allows": "do it",
+ "without_cap": "cannot",
+ "check_condition": "always checked",
+ "priority": 3,
+ "alternatives": [21, 17]
+ }
+ ],
+ "constraints": [],
+ "signals": [],
+ "side_effects": [],
+ "state_transitions": [
+ { "object": "obj", "from_state": "a", "to_state": "b", "condition": "when asked",
+ "description": "moves a to b" }
+ ],
+ "signal_masks": [
+ { "name": "blocked", "description": "Blocked while running", "signals": [2, 15, 3] }
+ ],
+ "struct_specs": [
+ {
+ "name": "tmp_struct",
+ "size": 16,
+ "alignment": 8,
+ "description": "Test structure",
+ "fields": [
+ {
+ "name": "flags",
+ "type": "u32",
+ "type_class": "uint",
+ "offset": 0,
+ "size": 4,
+ "flags": "0x1",
+ "constraint_type": "mask",
+ "min_value": 0,
+ "max_value": 0,
+ "valid_mask": "0xff",
+ "enum_values": "",
+ "description": "Flag bits"
+ },
+ {
+ "name": "kind",
+ "type": "enum k",
+ "type_class": "enum",
+ "offset": 8,
+ "size": 4,
+ "flags": "0x0",
+ "constraint_type": "range",
+ "min_value": -5,
+ "max_value": 5,
+ "valid_mask": "0x0",
+ "enum_values": "",
+ "description": "Kind"
+ }
+ ]
+ }
+ ],
+ "examples": "",
+ "notes": "note"
+}
+"#;
+
+ // Dump without the optional per-parameter constraint fields.
+ const MINIMAL_JSON: &str = r#"{
+ "name": "sys_read",
+ "version": 1,
+ "description": "Read data from a file descriptor",
+ "long_description": "Reads.",
+ "context_flags": "0x21",
+ "parameters": [
+ {
+ "name": "buf",
+ "type": "char __user * buf",
+ "type_class": "user_pointer",
+ "flags": "0x42",
+ "description": "User-space buffer to read data into "
+ }
+ ],
+ "return": {
+ "type": "KAPI_TYPE_INT",
+ "type_class": "int",
+ "check_type": "range",
+ "success_min": 0,
+ "success_max": 9223372036854775807,
+ "description": "Bytes read"
+ },
+ "errors": [],
+ "locks": [],
+ "capabilities": [
+ {
+ "capability": 1,
+ "name": "CAP_DAC_OVERRIDE",
+ "action": "bypass_check",
+ "allows": "a",
+ "without_cap": "b",
+ "check_condition": "c",
+ "priority": 0
+ }
+ ],
+ "constraints": [],
+ "signals": [],
+ "side_effects": [],
+ "examples": "",
+ "notes": ""
+}
+"#;
+
+ fn extractor() -> DebugfsExtractor {
+ DebugfsExtractor {
+ debugfs_path: PathBuf::new(),
+ }
+ }
+
+ #[test]
+ fn json_sys_read_carries_type_and_constraint_data() {
+ let spec = extractor().try_parse_json(SYS_READ_JSON).unwrap();
+
+ assert_eq!(spec.api_type, "syscall");
+ assert_eq!(spec.version.as_deref(), Some("1"));
+ assert_eq!(spec.long_description, None);
+ assert_eq!(
+ spec.context_flags,
+ ["KAPI_CTX_PROCESS", "KAPI_CTX_SLEEPABLE"]
+ );
+ assert_eq!(spec.param_count, Some(3));
+
+ let fd = &spec.parameters[0];
+ assert_eq!((fd.param_type, fd.constraint_type), (9, 1));
+ assert_eq!((fd.min_value, fd.max_value), (Some(0), Some(2147483647)));
+ assert_eq!(fd.valid_mask, None);
+ assert_eq!(fd.size_param_idx, None);
+ assert_eq!(
+ fd.constraint.as_deref(),
+ Some("Must be a valid, open file descriptor")
+ );
+
+ let buf = &spec.parameters[1];
+ assert_eq!((buf.param_type, buf.constraint_type), (10, 11));
+ assert_eq!(buf.flags, 0x42);
+ assert_eq!(buf.size_param_idx, Some(2));
+ assert_eq!(
+ (buf.min_value, buf.max_value, buf.valid_mask),
+ (None, None, None)
+ );
+ assert_eq!((buf.size, buf.alignment), (None, None));
+ assert_eq!(
+ spec.parameters[buf.size_param_idx.unwrap() as usize].name,
+ "count"
+ );
+
+ let count = &spec.parameters[2];
+ assert_eq!((count.param_type, count.constraint_type), (2, 0));
+ assert_eq!(count.constraint, None);
+ assert_eq!(count.description, "Maximum number of bytes to read ");
+
+ let ret = spec.return_spec.as_ref().unwrap();
+ assert_eq!((ret.return_type, ret.check_type), (1, 1));
+ assert_eq!(ret.success_value, None);
+ assert_eq!(ret.success_min, Some(0));
+ assert_eq!(ret.success_max, Some(i64::MAX));
+ assert!(ret.error_values.is_empty());
+
+ assert_eq!(spec.errors[0].error_code, -9);
+ assert_eq!((spec.locks[0].lock_type, spec.locks[0].scope), (1, 0));
+ assert_eq!(spec.capabilities[0].action, "KAPI_CAP_BYPASS_CHECK");
+ assert_eq!(spec.capabilities[0].check_condition, None);
+ assert_eq!(spec.signals[0].timing, 1);
+ assert_eq!(spec.signals[0].error_on_signal, Some(-4));
+ assert_eq!(spec.signals[0].target, None);
+ assert_eq!(spec.side_effects[0].effect_type, 0x10);
+ assert_eq!(spec.side_effects[0].condition, None);
+ assert_eq!(spec.constraints.len(), 1);
+ }
+
+ #[test]
+ fn json_full_carries_enum_error_struct_and_transition_data() {
+ let spec = extractor().try_parse_json(FULL_JSON).unwrap();
+
+ assert_eq!(
+ spec.description.as_deref(),
+ Some("Full \"feature\" test\tline\nbreak\u{1}")
+ );
+ assert_eq!(
+ spec.context_flags,
+ [
+ "KAPI_CTX_PROCESS",
+ "KAPI_CTX_ATOMIC",
+ "KAPI_CTX_IRQ_DISABLED"
+ ]
+ );
+
+ let mode = &spec.parameters[0];
+ assert_eq!((mode.param_type, mode.constraint_type), (6, 3));
+ assert_eq!(mode.enum_values, ["1", "2", "-3", "1000000000000"]);
+ assert_eq!((mode.size, mode.alignment), (Some(4096), Some(64)));
+ assert_eq!(
+ (mode.min_value, mode.max_value, mode.valid_mask),
+ (None, None, None)
+ );
+
+ let bits = &spec.parameters[1];
+ assert_eq!(bits.constraint_type, 2);
+ assert_eq!(bits.valid_mask, Some(u64::MAX));
+ assert_eq!((bits.min_value, bits.max_value), (None, None));
+ assert_eq!((bits.size, bits.alignment), (None, None));
+
+ let ret = spec.return_spec.as_ref().unwrap();
+ assert_eq!(ret.check_type, 2);
+ assert_eq!(ret.error_values, [-1, -22, -4095]);
+
+ assert_eq!(spec.capabilities[0].alternatives, [21, 17]);
+ assert_eq!(spec.capabilities[0].priority, Some(3));
+ assert_eq!((spec.locks[0].lock_type, spec.locks[0].scope), (4, 1));
+
+ let trans = &spec.state_transitions[0];
+ assert_eq!(
+ (trans.object.as_str(), trans.from_state.as_str()),
+ ("obj", "a")
+ );
+ assert_eq!(trans.to_state, "b");
+ assert_eq!(trans.condition.as_deref(), Some("when asked"));
+
+ assert_eq!(spec.signal_masks[0].name, "blocked");
+ assert_eq!(spec.signal_masks[0].signals, [2, 15, 3]);
+
+ let st = &spec.struct_specs[0];
+ assert_eq!(
+ (st.name.as_str(), st.size, st.alignment),
+ ("tmp_struct", 16, 8)
+ );
+ assert_eq!(st.field_count, 2);
+ assert_eq!(st.description, "Test structure");
+ assert_eq!(
+ (st.fields[0].field_type, st.fields[0].constraint_type),
+ (2, 2)
+ );
+ assert_eq!(st.fields[0].valid_mask, 0xff);
+ assert_eq!(st.fields[1].offset, 8);
+ assert_eq!((st.fields[1].min_value, st.fields[1].max_value), (-5, 5));
+ }
+
+ #[test]
+ fn json_without_constraint_fields_still_parses() {
+ let spec = extractor().try_parse_json(MINIMAL_JSON).unwrap();
+
+ let buf = &spec.parameters[0];
+ assert_eq!(buf.param_type, 10);
+ assert_eq!(buf.constraint_type, 0);
+ assert_eq!(buf.min_value, None);
+ assert_eq!(buf.valid_mask, None);
+ assert_eq!(buf.size_param_idx, None);
+ assert!(buf.enum_values.is_empty());
+
+ let ret = spec.return_spec.as_ref().unwrap();
+ assert_eq!((ret.return_type, ret.check_type), (1, 1));
+ assert_eq!(ret.success_value, None);
+ assert_eq!(ret.success_max, Some(i64::MAX));
+ assert_eq!(spec.capabilities[0].action, "KAPI_CAP_BYPASS_CHECK");
+ assert!(spec.state_transitions.is_empty());
+ assert!(spec.struct_specs.is_empty());
+ }
+
+ #[test]
+ fn enum_tokens_match_kernel_numbering() {
+ let classes = [
+ "void",
+ "int",
+ "uint",
+ "pointer",
+ "struct",
+ "union",
+ "enum",
+ "function_pointer",
+ "array",
+ "file_descriptor",
+ "user_pointer",
+ "pathname",
+ "custom",
+ ];
+ for (n, token) in classes.iter().enumerate() {
+ assert_eq!(
+ DebugfsExtractor::parse_type_class(token),
+ n as u32,
+ "{token}"
+ );
+ }
+
+ let constraints = [
+ "none",
+ "range",
+ "mask",
+ "enum",
+ "alignment",
+ "power_of_two",
+ "page_aligned",
+ "nonzero",
+ "user_string",
+ "user_path",
+ "user_ptr",
+ "buffer",
+ "custom",
+ ];
+ for (n, token) in constraints.iter().enumerate() {
+ assert_eq!(
+ DebugfsExtractor::parse_constraint_type(token),
+ n as u32,
+ "{token}"
+ );
+ }
+ }
+
+ #[test]
+ fn api_type_follows_symbol_name() {
+ assert_eq!(DebugfsExtractor::api_type_from_name("sys_read"), "syscall");
+ assert_eq!(DebugfsExtractor::api_type_from_name("foo_ioctl"), "ioctl");
+ assert_eq!(DebugfsExtractor::api_type_from_name("kmalloc"), "function");
+ }
+
+ #[test]
+ fn truncated_json_is_rejected() {
+ let cut = &SYS_READ_JSON[..SYS_READ_JSON.len() / 2];
+ assert!(extractor().try_parse_json(cut).is_err());
+ }
+
+ fn debugfs_with(json: &str, text: &str) -> tempfile::TempDir {
+ let dir = tempfile::tempdir().unwrap();
+ let kapi = dir.path().join("kapi");
+ fs::create_dir_all(kapi.join("specs")).unwrap();
+ fs::create_dir_all(kapi.join("specs-json")).unwrap();
+ fs::write(
+ kapi.join("list"),
+ "Available Kernel API Specifications\n\
+ ===================================\n\n\
+ sys_read - Read data from a file descriptor\n\n\
+ Total: 1 specifications\n",
+ )
+ .unwrap();
+ fs::write(kapi.join("specs-json/sys_read"), json).unwrap();
+ fs::write(kapi.join("specs/sys_read"), text).unwrap();
+ dir
+ }
+
+ #[test]
+ fn reads_json_endpoint_from_debugfs_tree() {
+ let dir = debugfs_with(SYS_READ_JSON, "Name: sys_read\n");
+ let ex = DebugfsExtractor::new(Some(dir.path().to_string_lossy().into_owned())).unwrap();
+ let spec = ex.extract_by_name("sys_read").unwrap().unwrap();
+
+ assert_eq!(spec.parameters[1].constraint_type, 11);
+ assert_eq!(spec.parameters[1].size_param_idx, Some(2));
+ }
+
+ #[test]
+ fn truncated_json_endpoint_falls_back_to_text_dump() {
+ let text = "Name: sys_read\n\
+ Version: 1\n\
+ Description: Read data from a file descriptor\n\
+ Context flags: PROCESS SLEEPABLE \n";
+ let dir = debugfs_with(&SYS_READ_JSON[..SYS_READ_JSON.len() / 2], text);
+ let ex = DebugfsExtractor::new(Some(dir.path().to_string_lossy().into_owned())).unwrap();
+ let spec = ex.extract_by_name("sys_read").unwrap().unwrap();
+
+ assert!(spec.parameters.is_empty());
+ assert_eq!(
+ spec.context_flags,
+ ["KAPI_CTX_PROCESS", "KAPI_CTX_SLEEPABLE"]
+ );
+ }
+
+ #[test]
+ fn text_dump_multi_line_values_stay_in_their_field() {
+ let text = "Kernel API Specification\n\
+ ========================\n\n\
+ Name: sys_read\n\
+ Version: 1\n\
+ Description: Read data from a file descriptor\n\
+ Long description:\n \
+ First paragraph.\n\n \
+ Notes: not a header\n \
+ Version: 9\n\
+ Context flags: PROCESS SLEEPABLE \n\n\
+ Parameters (1):\n \
+ [0] fd:\n \
+ description: File descriptor\n\n\
+ Examples:\n \
+ read(fd, buf, n);\n \
+ close(fd);\n\n\
+ Notes:\n \
+ one\n\n \
+ two\n\n";
+ let dir = debugfs_with("", text);
+ let ex = DebugfsExtractor::new(Some(dir.path().to_string_lossy().into_owned())).unwrap();
+ let spec = ex.extract_by_name("sys_read").unwrap().unwrap();
+
+ assert_eq!(spec.version.as_deref(), Some("1"));
+ assert_eq!(
+ spec.description.as_deref(),
+ Some("Read data from a file descriptor")
+ );
+ assert_eq!(
+ spec.long_description.as_deref(),
+ Some("First paragraph.\n\nNotes: not a header\nVersion: 9")
+ );
+ assert_eq!(
+ spec.context_flags,
+ ["KAPI_CTX_PROCESS", "KAPI_CTX_SLEEPABLE"]
+ );
+ assert_eq!(spec.param_count, Some(1));
+ assert_eq!(
+ spec.examples.as_deref(),
+ Some("read(fd, buf, n);\nclose(fd);")
+ );
+ assert_eq!(spec.notes.as_deref(), Some("one\n\ntwo"));
+ }
+}
--git a/tools/kapi/src/extractor/kerneldoc_parser.rs b/tools/kapi/src/extractor/kerneldoc_parser.rs
new file mode 100644
index 0000000000000..b8fae748470b0
--- /dev/null
+++ b/tools/kapi/src/extractor/kerneldoc_parser.rs
@@ -0,0 +1,3335 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+
+use super::{
+ ApiSpec, CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, ParamSpec, ReturnSpec,
+ SideEffectSpec, SignalSpec, StateTransitionSpec,
+};
+use anyhow::Result;
+use std::collections::HashMap;
+
+/// Kerneldoc parser that extracts KAPI annotations
+pub struct KerneldocParser;
+
+/// What block are we currently inside?
+#[derive(Debug, Clone, PartialEq)]
+enum BlockContext {
+ None,
+ Param(String), // param: <name>
+ Error(String), // error: <name>
+ Signal, // signal: <name>
+ Capability, // capability: <name>
+ SideEffect, // side-effect: <type>
+ StateTransition, // state-trans: ...
+ Constraint, // constraint: <name>
+ Lock, // lock: <name>
+ Return, // return:
+}
+
+/// Evaluate an integer literal exactly as the compiler does on LP64 (int is
+/// 32 bits, long and long long are 64): decimal, 0x/0X hex, 0b/0B binary or
+/// leading-0 octal, an optional u/U/l/L suffix and an optional leading '-'.
+/// The literal's C type (C11 6.4.4.1) decides how '-' wraps, so `-1U` is
+/// 4294967295 and `-0x80000000` is 2147483648. The result is the 64-bit
+/// two's complement pattern the value has once stored into an s64/u64
+/// field. Returns `None` for anything that requires cpp-level constant
+/// resolution (e.g. symbolic masks like `O_RDONLY | O_WRONLY`). Callers must
+/// treat that case as "value unknown" and leave the downstream slot unset,
+/// not store it as 0, which would wrongly assert that zero bits are valid.
+fn eval_c_int_literal(s: &str) -> Option<u64> {
+ let t = s.trim();
+ let (neg, t) = match t.strip_prefix('-') {
+ Some(rest) => (true, rest.trim_start()),
+ None => (false, t),
+ };
+ let body = t.trim_end_matches(['u', 'U', 'l', 'L']);
+ let suffix = &t[body.len()..];
+ let unsigned = match suffix.matches(['u', 'U']).count() {
+ 0 => false,
+ 1 => true,
+ _ => return None,
+ };
+ let longs = suffix.matches(['l', 'L']).count();
+ if longs > 2 {
+ return None;
+ }
+
+ let (digits, radix) =
+ if let Some(h) = body.strip_prefix("0x").or_else(|| body.strip_prefix("0X")) {
+ (h, 16)
+ } else if let Some(b) = body.strip_prefix("0b").or_else(|| body.strip_prefix("0B")) {
+ (b, 2)
+ } else if body.len() > 1 && body.starts_with('0') {
+ (&body[1..], 8)
+ } else {
+ (body, 10)
+ };
+ if digits.is_empty() || !digits.chars().all(|c| c.is_digit(radix)) {
+ return None;
+ }
+ let v = u64::from_str_radix(digits, radix).ok()?;
+
+ // Decimal literals without 'u' only take signed types; GCC treats one
+ // too large for long long as unsigned.
+ let value = if longs == 0 && !unsigned && v <= i32::MAX as u64 {
+ let x = v as i32;
+ (if neg { x.wrapping_neg() } else { x }) as i64 as u64
+ } else if longs == 0 && (unsigned || radix != 10) && v <= u32::MAX as u64 {
+ let x = v as u32;
+ u64::from(if neg { x.wrapping_neg() } else { x })
+ } else if !unsigned && v <= i64::MAX as u64 {
+ let x = v as i64;
+ (if neg { x.wrapping_neg() } else { x }) as u64
+ } else if neg {
+ v.wrapping_neg()
+ } else {
+ v
+ };
+ Some(value)
+}
+
+fn parse_u64_literal(s: &str) -> Option<u64> {
+ eval_c_int_literal(s)
+}
+
+fn parse_i64_literal(s: &str) -> Option<i64> {
+ eval_c_int_literal(s).map(|v| v as i64)
+}
+
+/// Split a return `success:` value into (exact value, range minimum,
+/// range maximum). `>= N` is the range [N, i64::MAX]; `N`, `= N` and
+/// `== N` are an exact value, and any value that is not `>= N` leaves
+/// the default range [0, i64::MAX] a range check falls back to. Mirrors
+/// `_return_success_macro()` in kdoc_apispec.py.
+fn parse_return_success(text: &str) -> (Option<i64>, Option<i64>, Option<i64>) {
+ let t = text.trim();
+ if let Some(min) = t.strip_prefix(">=") {
+ return (
+ None,
+ Some(parse_i64_literal(min).unwrap_or(0)),
+ Some(i64::MAX),
+ );
+ }
+ let exact = t
+ .strip_prefix("==")
+ .or_else(|| t.strip_prefix('='))
+ .unwrap_or(t);
+ (parse_i64_literal(exact), Some(0), Some(i64::MAX))
+}
+
+/// Canonicalise a capability `type:` value to its KAPI_CAP_* spelling.
+fn canon_kapi_cap_action(s: &str) -> String {
+ let t = s.trim();
+ if t.starts_with("KAPI_CAP_") {
+ return t.to_string();
+ }
+ match t.to_ascii_lowercase().as_str() {
+ "bypass_check" => "KAPI_CAP_BYPASS_CHECK".to_string(),
+ "increase_limit" => "KAPI_CAP_INCREASE_LIMIT".to_string(),
+ "override_restriction" => "KAPI_CAP_OVERRIDE_RESTRICTION".to_string(),
+ "grant_permission" => "KAPI_CAP_GRANT_PERMISSION".to_string(),
+ "modify_behavior" => "KAPI_CAP_MODIFY_BEHAVIOR".to_string(),
+ "access_resource" => "KAPI_CAP_ACCESS_RESOURCE".to_string(),
+ "perform_operation" => "KAPI_CAP_PERFORM_OPERATION".to_string(),
+ _ => t.to_string(),
+ }
+}
+
+/// Types whose semantics imply `KAPI_PARAM_USER` on the param, so
+/// `type: user_ptr, input` doesn't need a separate `user` flag.
+fn type_implies_user_flag(tok: &str) -> bool {
+ matches!(tok.trim(), "KAPI_TYPE_USER_PTR" | "KAPI_TYPE_PATH")
+ || matches!(
+ tok.trim().to_ascii_lowercase().as_str(),
+ "user_ptr" | "uptr" | "path"
+ )
+}
+
+/// Subfield names each block type consumes. An indented line opens a
+/// new subfield only when it starts with one of these followed by ':';
+/// any other line continues the previous subfield. Must stay in sync
+/// with the `_*_SUBFIELDS` sets in tools/lib/python/kdoc/kdoc_apispec.py.
+fn block_subfield_keys(block: &BlockContext) -> &'static [&'static str] {
+ match block {
+ BlockContext::Param(_) => &[
+ "type",
+ "flags",
+ "size",
+ "constraint-type",
+ "constraint",
+ "cdesc",
+ "range",
+ "mask",
+ "valid-mask",
+ "valid-values",
+ "alignment",
+ "size-param",
+ "struct-type",
+ "arch-mask",
+ "desc",
+ "description",
+ ],
+ BlockContext::Error(_) => &["desc", "condition"],
+ BlockContext::Signal => &[
+ "direction",
+ "action",
+ "condition",
+ "desc",
+ "errno",
+ "timing",
+ "priority",
+ "restartable",
+ "interruptible",
+ "number",
+ "target",
+ "queue",
+ "queue_behavior",
+ "transform",
+ "transform_to",
+ "transform-to",
+ "sa_flags_required",
+ "sa-flags-required",
+ "sa_flags_forbidden",
+ "sa-flags-forbidden",
+ "state_required",
+ "state-required",
+ "state_forbidden",
+ "state-forbidden",
+ ],
+ BlockContext::Capability => &["type", "allows", "without", "condition", "priority", "desc"],
+ BlockContext::SideEffect => &["target", "desc", "condition", "reversible"],
+ BlockContext::StateTransition => &["object", "from", "to", "condition", "desc"],
+ BlockContext::Constraint => &["desc", "expr"],
+ BlockContext::Lock => &[
+ "type",
+ "scope",
+ "acquired",
+ "released",
+ "held-on-entry",
+ "held-on-exit",
+ "desc",
+ ],
+ BlockContext::Return => &[
+ "type",
+ "check-type",
+ "success",
+ "success-range",
+ "error-values",
+ "desc",
+ ],
+ BlockContext::None => &[],
+ }
+}
+
+/// True if `line` starts with one of `keys` followed by ':'.
+fn starts_subfield(line: &str, keys: &[&str]) -> bool {
+ line.split_once(':')
+ .is_some_and(|(key, _)| keys.contains(&key.trim()))
+}
+
+fn is_indented_line(line: &str) -> bool {
+ line.starts_with(" ") || line.starts_with('\t')
+}
+
+/// Fold free-form prose into paragraphs. Blank lines separate
+/// paragraphs ("\n\n"); wrapped lines inside a paragraph are joined
+/// with spaces, except that a line starting with "- " always begins a
+/// new line so bullet lists survive. Mirrors `_fold_paragraphs()` in
+/// kdoc_apispec.py.
+fn fold_paragraphs(lines: &[&str]) -> String {
+ let mut paragraphs: Vec<Vec<String>> = Vec::new();
+ let mut current: Vec<String> = Vec::new();
+ for line in lines {
+ let line = line.trim();
+ if line.is_empty() {
+ if !current.is_empty() {
+ paragraphs.push(std::mem::take(&mut current));
+ }
+ } else if line.starts_with("- ") || current.is_empty() {
+ current.push(line.to_string());
+ } else if let Some(last) = current.last_mut() {
+ last.push(' ');
+ last.push_str(line);
+ }
+ }
+ if !current.is_empty() {
+ paragraphs.push(current);
+ }
+ paragraphs
+ .iter()
+ .map(|p| p.join("\n"))
+ .collect::<Vec<_>>()
+ .join("\n\n")
+ .replace('\t', " ")
+}
+
+fn expand_tabs(line: &str) -> String {
+ let mut out = String::with_capacity(line.len());
+ let mut col = 0;
+ for c in line.chars() {
+ if c == '\t' {
+ let pad = 8 - col % 8;
+ out.extend(std::iter::repeat_n(' ', pad));
+ col += pad;
+ } else {
+ out.push(c);
+ col += 1;
+ }
+ }
+ out
+}
+
+/// Keep every line of a block on its own line. The indentation shared
+/// by the continuation lines is removed so relative indentation
+/// (nested code) is preserved; runs of blank lines collapse into one.
+/// Mirrors `_fold_lines()` in kdoc_apispec.py.
+fn fold_lines(lines: &[&str]) -> String {
+ let mut lines: Vec<String> = lines
+ .iter()
+ .map(|l| expand_tabs(l).trim_end().to_string())
+ .collect();
+ while lines.first().is_some_and(|l| l.is_empty()) {
+ lines.remove(0);
+ }
+ while lines.last().is_some_and(|l| l.is_empty()) {
+ lines.pop();
+ }
+
+ let indent = |l: &str| l.len() - l.trim_start().len();
+ let base = lines
+ .iter()
+ .skip(1)
+ .filter(|l| !l.is_empty())
+ .map(|l| indent(l))
+ .min()
+ .unwrap_or(0);
+
+ let mut out: Vec<&str> = Vec::new();
+ for line in &lines {
+ if line.is_empty() {
+ if out.last().is_some_and(|l| l.is_empty()) {
+ continue;
+ }
+ out.push("");
+ } else {
+ out.push(&line[base.min(indent(line))..]);
+ }
+ }
+ out.join("\n")
+}
+
+impl KerneldocParser {
+ pub fn new() -> Self {
+ KerneldocParser
+ }
+
+ pub fn parse_kerneldoc(
+ &self,
+ doc: &str,
+ name: &str,
+ api_type: &str,
+ signature: Option<&str>,
+ ) -> Result<ApiSpec> {
+ let mut spec = ApiSpec {
+ name: name.to_string(),
+ api_type: api_type.to_string(),
+ ..Default::default()
+ };
+
+ let lines: Vec<&str> = doc.lines().collect();
+
+ // Extract main description from function name line
+ if let Some(first_line) = lines.first() {
+ if let Some((_, desc)) = first_line.split_once(" - ") {
+ spec.description = Some(desc.trim().to_string());
+ }
+ }
+
+ // Extract type names from SYSCALL_DEFINE signature
+ let type_map = if let Some(sig) = signature {
+ self.extract_types_from_signature(sig)
+ } else {
+ HashMap::new()
+ };
+
+ // Keep track of parameters we've seen (from @param lines)
+ let mut param_map: HashMap<String, ParamSpec> = HashMap::new();
+
+ // Current block being parsed
+ let mut block = BlockContext::None;
+
+ // Temporary storage for current block items
+ let mut current_lock: Option<LockSpec> = None;
+ let mut current_signal: Option<SignalSpec> = None;
+ // Pending symbolic `transform-to:` token. Captured when the parser
+ // sees a non-numeric value, but only reported if the final
+ // `transform_to` after all lines in the signal block is still
+ // unresolved. A later numeric `transform-to:` clears this so we
+ // don't warn about a value that was subsequently overridden.
+ let mut pending_transform_warning: Option<String> = None;
+ let mut current_capability: Option<CapabilitySpec> = None;
+ let mut current_side_effect: Option<SideEffectSpec> = None;
+ let mut current_constraint: Option<ConstraintSpec> = None;
+ let mut current_error: Option<ErrorSpec> = None;
+ let mut current_return: Option<ReturnSpec> = None;
+ let mut current_state_trans: Option<StateTransitionSpec> = None;
+
+ let mut i = 0;
+
+ while i < lines.len() {
+ let line = lines[i];
+ let trimmed = line.trim();
+
+ // Skip empty lines
+ if trimmed.is_empty() {
+ i += 1;
+ continue;
+ }
+
+ // Check if this is an indented continuation line (part of current block)
+ let is_indented = is_indented_line(line);
+
+ // If indented and we're in a block, parse as block attribute.
+ // A subfield line absorbs the lines that follow it until the
+ // next known subfield key, so a value such as
+ // `constraint-type: mask(FOO | BAR |` ... `| BAZ)` or a
+ // wrapped `condition:` arrives as a single logical line.
+ if is_indented && block != BlockContext::None {
+ let keys = block_subfield_keys(&block);
+ let mut logical = trimmed.to_string();
+ if starts_subfield(trimmed, keys) {
+ let mut j = i + 1;
+ while j < lines.len() {
+ let next = lines[j];
+ let next_trim = next.trim();
+ if next_trim.is_empty() {
+ j += 1;
+ continue;
+ }
+ if !is_indented_line(next) || starts_subfield(next_trim, keys) {
+ break;
+ }
+ logical.push(' ');
+ logical.push_str(next_trim);
+ i = j;
+ j += 1;
+ }
+ }
+ self.parse_block_attribute(
+ &logical,
+ &block,
+ &mut param_map,
+ &mut current_error,
+ &mut current_signal,
+ &mut pending_transform_warning,
+ &mut current_capability,
+ &mut current_side_effect,
+ &mut current_constraint,
+ &mut current_lock,
+ &mut current_return,
+ &mut current_state_trans,
+ );
+ i += 1;
+ continue;
+ }
+
+ // Not indented or not in block: flush current block if any.
+ // If a symbolic `transform-to:` was captured and no later
+ // numeric line cleared it, surface the warning now; by
+ // construction `transform_to` is None in that case.
+ if matches!(block, BlockContext::Signal) {
+ if let Some(raw) = pending_transform_warning.take() {
+ eprintln!(
+ "kapi: warning: transform-to: {raw:?} is symbolic; \
+ source-mode cannot resolve signal numbers portably. \
+ Use --vmlinux or --debugfs to get the resolved value.",
+ );
+ }
+ }
+ self.flush_block(
+ &mut block,
+ &mut spec,
+ &mut current_error,
+ &mut current_signal,
+ &mut current_capability,
+ &mut current_side_effect,
+ &mut current_constraint,
+ &mut current_lock,
+ &mut current_return,
+ &mut current_state_trans,
+ );
+
+ // Parse top-level annotations
+ if let Some(rest) = trimmed.strip_prefix("@") {
+ // @param: description (standard kerneldoc parameter)
+ if let Some((param_name, desc)) = rest.split_once(':') {
+ let param_name = param_name.trim();
+ let desc = desc.trim();
+ if !param_name.contains('-') {
+ let idx = param_map.len() as u32;
+ let type_name = type_map.get(param_name).cloned().unwrap_or_default();
+ param_map.insert(
+ param_name.to_string(),
+ ParamSpec {
+ index: idx,
+ name: param_name.to_string(),
+ type_name,
+ description: desc.to_string(),
+ flags: 0,
+ param_type: 0,
+ constraint_type: 0,
+ constraint: None,
+ min_value: None,
+ max_value: None,
+ valid_mask: None,
+ enum_values: vec![],
+ size: None,
+ alignment: None,
+ size_param_idx: None,
+ },
+ );
+ }
+ }
+ } else if let Some(rest) = trimmed.strip_prefix("long-desc:") {
+ let (section, next_i) = self.collect_section(&lines, i, rest);
+ let val = fold_paragraphs(§ion);
+ spec.long_description = Some(val).filter(|v| !v.is_empty());
+ i = next_i;
+ continue;
+ } else if let Some(rest) = trimmed.strip_prefix("context-flags:") {
+ spec.context_flags = self.parse_context_flags(rest.trim());
+ } else if let Some(rest) = trimmed.strip_prefix("contexts:") {
+ // Short form: "contexts: process, sleepable"
+ spec.context_flags = self.parse_context_list(rest.trim());
+ } else if let Some(rest) = trimmed.strip_prefix("param-count:") {
+ spec.param_count = rest.trim().parse().ok();
+ }
+ // Block-start annotations
+ else if let Some(rest) = trimmed.strip_prefix("param:") {
+ let param_name = rest.trim().to_string();
+ block = BlockContext::Param(param_name.clone());
+ // Ensure param exists in map
+ if !param_map.contains_key(¶m_name) {
+ let idx = param_map.len() as u32;
+ let type_name = type_map
+ .get(param_name.as_str())
+ .cloned()
+ .unwrap_or_default();
+ param_map.insert(
+ param_name.clone(),
+ ParamSpec {
+ index: idx,
+ name: param_name,
+ type_name,
+ description: String::new(),
+ flags: 0,
+ param_type: 0,
+ constraint_type: 0,
+ constraint: None,
+ min_value: None,
+ max_value: None,
+ valid_mask: None,
+ enum_values: vec![],
+ size: None,
+ alignment: None,
+ size_param_idx: None,
+ },
+ );
+ }
+ } else if let Some(rest) = trimmed.strip_prefix("error:") {
+ // error: NAME, condition
+ let parts: Vec<&str> = rest.splitn(2, ',').map(|s| s.trim()).collect();
+ if !parts.is_empty() {
+ let error_name = parts[0].to_string();
+ let condition = if parts.len() >= 2 {
+ parts[1].to_string()
+ } else {
+ String::new()
+ };
+ let error_code = self.error_name_to_code(&error_name);
+ current_error = Some(ErrorSpec {
+ error_code,
+ name: error_name.clone(),
+ condition,
+ description: String::new(),
+ });
+ block = BlockContext::Error(error_name);
+ }
+ } else if let Some(rest) = trimmed.strip_prefix("signal:") {
+ let signal_name = rest.trim().to_string();
+ current_signal = Some(SignalSpec {
+ signal_num: 0,
+ signal_name,
+ direction: 1,
+ action: 0,
+ target: None,
+ condition: None,
+ description: None,
+ restartable: false,
+ timing: 0,
+ priority: 0,
+ interruptible: false,
+ queue: None,
+ sa_flags: 0,
+ sa_flags_required: 0,
+ sa_flags_forbidden: 0,
+ state_required: 0,
+ state_forbidden: 0,
+ error_on_signal: None,
+ transform_to: None,
+ });
+ block = BlockContext::Signal;
+ } else if let Some(rest) = trimmed.strip_prefix("capability:") {
+ let parts: Vec<&str> = rest.split(',').map(|s| s.trim()).collect();
+ if !parts.is_empty() {
+ let cap_name = parts[0].to_string();
+ let cap_value = self.parse_capability_value(&cap_name);
+ // If we have 3 parts, it's flat format: capability: CAP, action, name
+ let (action, name) = if parts.len() >= 3 {
+ (parts[1].to_string(), parts[2].to_string())
+ } else {
+ (String::new(), cap_name.clone())
+ };
+ current_capability = Some(CapabilitySpec {
+ capability: cap_value,
+ name,
+ action,
+ allows: String::new(),
+ without_cap: String::new(),
+ check_condition: None,
+ priority: Some(0),
+ alternatives: vec![],
+ });
+ block = BlockContext::Capability;
+ }
+ } else if let Some(rest) = trimmed.strip_prefix("side-effect:") {
+ // Could be flat format (comma-separated) or block start
+ let rest = rest.trim();
+ // Check if it's the flat format with commas
+ let comma_parts: Vec<&str> = rest.splitn(3, ',').map(|s| s.trim()).collect();
+ if comma_parts.len() >= 3 {
+ // Flat format: side-effect: TYPE, target, desc
+ let mut effect = SideEffectSpec {
+ effect_type: self.parse_effect_type(comma_parts[0]),
+ target: comma_parts[1].to_string(),
+ condition: None,
+ description: comma_parts[2].to_string(),
+ reversible: false,
+ };
+ if comma_parts[2].contains("reversible=yes") {
+ effect.reversible = true;
+ }
+ spec.side_effects.push(effect);
+ } else {
+ // Block format: side-effect: TYPE
+ current_side_effect = Some(SideEffectSpec {
+ effect_type: self.parse_effect_type(rest),
+ target: String::new(),
+ condition: None,
+ description: String::new(),
+ reversible: false,
+ });
+ block = BlockContext::SideEffect;
+ }
+ } else if let Some(rest) = trimmed.strip_prefix("state-trans:") {
+ // Flat form: state-trans: OBJECT, FROM, TO, DESCRIPTION
+ // (the description may itself contain commas). Block
+ // form: a bare OBJECT followed by from:/to:/condition:/
+ // desc: subfields.
+ if rest.contains(',') {
+ let mut parts = rest.splitn(4, ',').map(|s| s.trim());
+ let mut next_part = || parts.next().unwrap_or_default().to_string();
+ spec.state_transitions.push(StateTransitionSpec {
+ object: next_part(),
+ from_state: next_part(),
+ to_state: next_part(),
+ condition: None,
+ description: next_part(),
+ });
+ } else {
+ current_state_trans = Some(StateTransitionSpec {
+ object: rest.trim().to_string(),
+ from_state: String::new(),
+ to_state: String::new(),
+ condition: None,
+ description: String::new(),
+ });
+ }
+ block = BlockContext::StateTransition;
+ } else if let Some(rest) = trimmed.strip_prefix("constraint:") {
+ let rest = rest.trim();
+ // Could be flat format: constraint: name, desc
+ // Or block format: constraint: name
+ let parts: Vec<&str> = rest.splitn(2, ',').map(|s| s.trim()).collect();
+ if parts.len() >= 2 {
+ // Flat format
+ current_constraint = Some(ConstraintSpec {
+ name: parts[0].to_string(),
+ description: parts[1].to_string(),
+ expression: None,
+ });
+ } else {
+ // Block format
+ current_constraint = Some(ConstraintSpec {
+ name: rest.to_string(),
+ description: String::new(),
+ expression: None,
+ });
+ }
+ block = BlockContext::Constraint;
+ } else if let Some(rest) = trimmed.strip_prefix("lock:") {
+ let rest = rest.trim();
+ // Could be flat: lock: name, type
+ // Or block: lock: name
+ let parts: Vec<&str> = rest.split(',').map(|s| s.trim()).collect();
+ if parts.len() >= 2 {
+ current_lock = Some(LockSpec {
+ lock_name: parts[0].to_string(),
+ lock_type: self.parse_lock_type(parts[1]),
+ scope: super::KAPI_LOCK_INTERNAL,
+ description: String::new(),
+ });
+ } else {
+ current_lock = Some(LockSpec {
+ lock_name: rest.to_string(),
+ lock_type: 0,
+ scope: super::KAPI_LOCK_INTERNAL,
+ description: String::new(),
+ });
+ }
+ block = BlockContext::Lock;
+ }
+ // Other top-level annotations
+ else if let Some(rest) = trimmed.strip_prefix("return:") {
+ let rest = rest.trim();
+ if rest.is_empty() {
+ // Block format
+ current_return = Some(ReturnSpec {
+ type_name: String::new(),
+ description: String::new(),
+ return_type: 0,
+ check_type: 0,
+ success_value: None,
+ success_min: None,
+ success_max: None,
+ error_values: vec![],
+ });
+ block = BlockContext::Return;
+ }
+ } else if let Some(rest) = trimmed.strip_prefix("examples:") {
+ let (section, next_i) = self.collect_section(&lines, i, rest);
+ let val = fold_lines(§ion);
+ spec.examples = Some(val).filter(|v| !v.is_empty());
+ i = next_i;
+ continue;
+ } else if let Some(rest) = trimmed.strip_prefix("notes:") {
+ let (section, next_i) = self.collect_section(&lines, i, rest);
+ let val = fold_paragraphs(§ion);
+ spec.notes = Some(val).filter(|v| !v.is_empty());
+ i = next_i;
+ continue;
+ }
+
+ i += 1;
+ }
+
+ // Flush any remaining block. Emit a pending symbolic
+ // `transform-to:` warning if the final state still has no
+ // resolved numeric value (see per-line loop for rationale).
+ if matches!(block, BlockContext::Signal) {
+ if let Some(raw) = pending_transform_warning.take() {
+ eprintln!(
+ "kapi: warning: transform-to: {raw:?} is symbolic; \
+ source-mode cannot resolve signal numbers portably. \
+ Use --vmlinux or --debugfs to get the resolved value.",
+ );
+ }
+ }
+ self.flush_block(
+ &mut block,
+ &mut spec,
+ &mut current_error,
+ &mut current_signal,
+ &mut current_capability,
+ &mut current_side_effect,
+ &mut current_constraint,
+ &mut current_lock,
+ &mut current_return,
+ &mut current_state_trans,
+ );
+
+ // Convert param_map to vec preserving order
+ let mut params: Vec<ParamSpec> = param_map.into_values().collect();
+ params.sort_by_key(|p| p.index);
+
+ // If the spec carries an explicit param-count, warn when it
+ // disagrees with the number of param: blocks we actually saw.
+ if let Some(claimed) = spec.param_count {
+ if claimed as usize != params.len() {
+ eprintln!(
+ "kapi: {}: param-count: {} disagrees with {} param: block(s)",
+ name,
+ claimed,
+ params.len(),
+ );
+ }
+ }
+
+ for param in &mut params {
+ param.drop_unset_string_limits();
+ }
+ spec.parameters = params;
+
+ Ok(spec)
+ }
+
+ /// Parse an indented attribute line within a block
+ #[allow(clippy::too_many_arguments)]
+ fn parse_block_attribute(
+ &self,
+ trimmed: &str,
+ block: &BlockContext,
+ param_map: &mut HashMap<String, ParamSpec>,
+ current_error: &mut Option<ErrorSpec>,
+ current_signal: &mut Option<SignalSpec>,
+ pending_transform_warning: &mut Option<String>,
+ current_capability: &mut Option<CapabilitySpec>,
+ current_side_effect: &mut Option<SideEffectSpec>,
+ current_constraint: &mut Option<ConstraintSpec>,
+ current_lock: &mut Option<LockSpec>,
+ current_return: &mut Option<ReturnSpec>,
+ current_state_trans: &mut Option<StateTransitionSpec>,
+ ) {
+ match block {
+ BlockContext::Param(param_name) => {
+ if let Some(param) = param_map.get_mut(param_name) {
+ if let Some(rest) = trimmed.strip_prefix("type:") {
+ // Accept either:
+ // type: KAPI_TYPE_UINT (long, single token)
+ // type: uint (short, single token)
+ // type: uint, input (short, type + flags)
+ // type: path, input (short, type + flags)
+ // Single-token inputs leave flags alone; they are
+ // set by a separate `flags:` line.
+ //
+ // User-space pointer types (user_ptr, path) imply
+ // KAPI_PARAM_USER, so specs don't need to repeat
+ // `user` after the type.
+ let mut parts = rest.split(',').map(str::trim);
+ let type_token = parts.next();
+ if let Some(ty) = type_token {
+ param.param_type = self.parse_param_type(ty);
+ }
+ for flag in parts {
+ param.flags |= self.parse_param_flag_token(flag);
+ }
+ if type_token.map(type_implies_user_flag).unwrap_or(false) {
+ param.flags |= 1 << 6; // KAPI_PARAM_USER
+ }
+ } else if let Some(rest) = trimmed.strip_prefix("flags:") {
+ param.flags = self.parse_param_flags(rest.trim());
+ } else if let Some(rest) = trimmed.strip_prefix("constraint-type:") {
+ // Accepts `KAPI_CONSTRAINT_*` enum tokens or
+ // function-call expressions like `range(0, 4096)`
+ // / `mask(0xff)` / `buffer(2)` that also populate
+ // the matching numeric fields on `param`.
+ let text = rest.trim();
+ if !self.apply_constraint_expr(param, text) {
+ param.constraint_type = self.parse_constraint_type(text);
+ }
+ } else if let Some(rest) = trimmed.strip_prefix("valid-mask:") {
+ // Symbolic mask values need cpp-level resolution;
+ // leave that to the binary reader.
+ let _ = rest;
+ } else if let Some(rest) = trimmed
+ .strip_prefix("cdesc:")
+ .or_else(|| trimmed.strip_prefix("constraint:"))
+ {
+ // Free-text constraint description.
+ param.constraint = Some(rest.trim().to_string());
+ } else if let Some(rest) = trimmed.strip_prefix("range:") {
+ let parts: Vec<&str> = rest.split(',').map(|s| s.trim()).collect();
+ if parts.len() >= 2 {
+ param.min_value = parts[0].parse().ok();
+ param.max_value = parts[1].parse().ok();
+ param.constraint_type = 1; // KAPI_CONSTRAINT_RANGE
+ }
+ } else if let Some(rest) = trimmed.strip_prefix("size-param:") {
+ param.size_param_idx = rest.trim().parse().ok();
+ } else if let Some(rest) = trimmed.strip_prefix("description:") {
+ param.description = rest.trim().to_string();
+ } else if let Some(rest) = trimmed.strip_prefix("desc:") {
+ param.description = rest.trim().to_string();
+ } else if !trimmed.contains(':') || trimmed.starts_with(" ") {
+ // Continuation of the previous attribute's value.
+ if let Some(c) = param.constraint.as_mut() {
+ c.push(' ');
+ c.push_str(trimmed);
+ }
+ }
+ }
+ }
+ BlockContext::Error(_) => {
+ if let Some(error) = current_error.as_mut() {
+ if let Some(rest) = trimmed.strip_prefix("desc:") {
+ let text = rest.trim().to_string();
+ if error.description.is_empty() {
+ error.description = text;
+ } else {
+ error.description.push(' ');
+ error.description.push_str(&text);
+ }
+ } else if let Some(rest) = trimmed.strip_prefix("condition:") {
+ error.condition = rest.trim().to_string();
+ } else {
+ // Continuation of description
+ if !error.description.is_empty() {
+ error.description.push(' ');
+ error.description.push_str(trimmed);
+ }
+ }
+ }
+ }
+ BlockContext::Signal => {
+ if let Some(signal) = current_signal.as_mut() {
+ if let Some(rest) = trimmed.strip_prefix("direction:") {
+ signal.direction = self.parse_signal_direction(rest.trim());
+ } else if let Some(rest) = trimmed.strip_prefix("action:") {
+ signal.action = self.parse_signal_action(rest.trim());
+ } else if let Some(rest) = trimmed.strip_prefix("condition:") {
+ signal.condition = Some(rest.trim().to_string());
+ } else if let Some(rest) = trimmed.strip_prefix("desc:") {
+ let text = rest.trim().to_string();
+ if signal.description.is_none() {
+ signal.description = Some(text);
+ } else if let Some(d) = signal.description.as_mut() {
+ d.push(' ');
+ d.push_str(&text);
+ }
+ } else if let Some(rest) = trimmed.strip_prefix("errno:") {
+ // `error:` cannot be used here because kerneldoc
+ // promotes it to a top-level section header.
+ //
+ // Accepted forms:
+ // errno: -4 -> numeric literal, stored as-is
+ // errno: -EINTR -> kernel convention; resolve
+ // the symbol and negate
+ // errno: EINTR -> bare symbol; resolved value
+ // is already negative
+ let value = rest.trim();
+ signal.error_on_signal = if let Ok(code) = value.parse::<i32>() {
+ Some(code)
+ } else if let Some(name) = value.strip_prefix('-') {
+ // `error_name_to_code` already returns the negated
+ // code (e.g. "EINTR" -> -4), so `-EINTR` resolves
+ // to -4 too; the leading `-` on the symbolic form
+ // is kernel-source convention, not a second negation.
+ Some(self.error_name_to_code(name))
+ } else {
+ Some(self.error_name_to_code(value))
+ };
+ } else if let Some(rest) = trimmed.strip_prefix("timing:") {
+ signal.timing = self.parse_signal_timing(rest.trim());
+ } else if let Some(rest) = trimmed.strip_prefix("restartable:") {
+ let val = rest.trim().to_lowercase();
+ signal.restartable = matches!(val.as_str(), "yes" | "true" | "1");
+ } else if let Some(rest) = trimmed.strip_prefix("interruptible:") {
+ let val = rest.trim().to_lowercase();
+ signal.interruptible = matches!(val.as_str(), "yes" | "true" | "1");
+ } else if let Some(rest) = trimmed.strip_prefix("priority:") {
+ signal.priority = rest.trim().parse().unwrap_or(0);
+ } else if let Some(rest) = trimmed.strip_prefix("target:") {
+ signal.target = Some(rest.trim().to_string());
+ } else if let Some(rest) = trimmed
+ .strip_prefix("queue:")
+ .or_else(|| trimmed.strip_prefix("queue_behavior:"))
+ {
+ signal.queue = Some(rest.trim().to_string());
+ } else if let Some(rest) = trimmed.strip_prefix("number:") {
+ signal.signal_num = rest.trim().parse().unwrap_or(0);
+ } else if let Some(rest) = trimmed
+ .strip_prefix("transform-to:")
+ .or_else(|| trimmed.strip_prefix("transform_to:"))
+ .or_else(|| trimmed.strip_prefix("transform:"))
+ {
+ // transform-to: takes a signal constant (e.g.
+ // SIGKILL) or a numeric literal. Only a numeric
+ // literal fills `transform_to`; symbolic values
+ // cannot be resolved portably in userspace
+ // because signal numbers are arch-dependent and
+ // we have no access to the target arch's
+ // <asm/signal.h>. Report such cases to stderr so
+ // they are not silently lost, and point the user
+ // at --vmlinux / --debugfs, which consult the
+ // compiled struct where the C preprocessor has
+ // already baked in the correct value.
+ //
+ // Assign unconditionally so the last line in
+ // the kerneldoc wins and an intended symbolic
+ // override doesn't silently leave a stale
+ // numeric value from an earlier line. The
+ // warning is deferred until flush_block() so a
+ // subsequent numeric line can cancel it; if the
+ // last line was still symbolic we report it
+ // then.
+ let v = rest.trim();
+ let parsed = v.parse::<i32>().ok();
+ signal.transform_to = parsed;
+ if parsed.is_some() {
+ *pending_transform_warning = None;
+ } else if !v.is_empty() {
+ *pending_transform_warning = Some(v.to_string());
+ }
+ } else if let Some(rest) = trimmed
+ .strip_prefix("sa-flags-required:")
+ .or_else(|| trimmed.strip_prefix("sa_flags_required:"))
+ {
+ signal.sa_flags_required = self.parse_hex_or_bitmask(rest.trim());
+ } else if let Some(rest) = trimmed
+ .strip_prefix("sa-flags-forbidden:")
+ .or_else(|| trimmed.strip_prefix("sa_flags_forbidden:"))
+ {
+ signal.sa_flags_forbidden = self.parse_hex_or_bitmask(rest.trim());
+ } else if let Some(rest) = trimmed
+ .strip_prefix("state-required:")
+ .or_else(|| trimmed.strip_prefix("state_required:"))
+ {
+ signal.state_required = self.parse_signal_state_mask(rest.trim());
+ } else if let Some(rest) = trimmed
+ .strip_prefix("state-forbidden:")
+ .or_else(|| trimmed.strip_prefix("state_forbidden:"))
+ {
+ signal.state_forbidden = self.parse_signal_state_mask(rest.trim());
+ } else {
+ // Continuation of description
+ if let Some(d) = signal.description.as_mut() {
+ d.push(' ');
+ d.push_str(trimmed);
+ }
+ }
+ }
+ }
+ BlockContext::Capability => {
+ if let Some(cap) = current_capability.as_mut() {
+ if let Some(rest) = trimmed.strip_prefix("type:") {
+ cap.action = canon_kapi_cap_action(rest.trim());
+ } else if let Some(rest) = trimmed.strip_prefix("allows:") {
+ cap.allows = rest.trim().to_string();
+ } else if let Some(rest) = trimmed.strip_prefix("without:") {
+ cap.without_cap = rest.trim().to_string();
+ } else if let Some(rest) = trimmed.strip_prefix("condition:") {
+ cap.check_condition = Some(rest.trim().to_string());
+ } else if let Some(rest) = trimmed.strip_prefix("priority:") {
+ cap.priority = rest.trim().parse().ok();
+ }
+ }
+ }
+ BlockContext::SideEffect => {
+ if let Some(effect) = current_side_effect.as_mut() {
+ if let Some(rest) = trimmed.strip_prefix("target:") {
+ effect.target = rest.trim().to_string();
+ } else if let Some(rest) = trimmed.strip_prefix("condition:") {
+ effect.condition = Some(rest.trim().to_string());
+ } else if let Some(rest) = trimmed.strip_prefix("desc:") {
+ let text = rest.trim().to_string();
+ if effect.description.is_empty() {
+ effect.description = text;
+ } else {
+ effect.description.push(' ');
+ effect.description.push_str(&text);
+ }
+ } else if let Some(rest) = trimmed.strip_prefix("reversible:") {
+ let val = rest.trim().to_lowercase();
+ effect.reversible = matches!(val.as_str(), "yes" | "true" | "1");
+ } else {
+ // Continuation of description
+ if !effect.description.is_empty() {
+ effect.description.push(' ');
+ effect.description.push_str(trimmed);
+ }
+ }
+ }
+ }
+ BlockContext::Constraint => {
+ if let Some(constraint) = current_constraint.as_mut() {
+ if let Some(rest) = trimmed.strip_prefix("desc:") {
+ let text = rest.trim().to_string();
+ if constraint.description.is_empty() {
+ constraint.description = text;
+ } else {
+ constraint.description.push(' ');
+ constraint.description.push_str(&text);
+ }
+ } else if let Some(rest) = trimmed.strip_prefix("expr:") {
+ constraint.expression = Some(rest.trim().to_string());
+ } else {
+ // Continuation of description
+ if !constraint.description.is_empty() {
+ constraint.description.push(' ');
+ constraint.description.push_str(trimmed);
+ }
+ }
+ }
+ }
+ BlockContext::Lock => {
+ if let Some(lock) = current_lock.as_mut() {
+ if let Some(rest) = trimmed.strip_prefix("type:") {
+ lock.lock_type = self.parse_lock_type(rest.trim());
+ } else if let Some(rest) = trimmed.strip_prefix("scope:") {
+ lock.scope = match rest.trim() {
+ "internal" => super::KAPI_LOCK_INTERNAL,
+ "acquires" => super::KAPI_LOCK_ACQUIRES,
+ "releases" => super::KAPI_LOCK_RELEASES,
+ "caller_held" => super::KAPI_LOCK_CALLER_HELD,
+ _ => super::KAPI_LOCK_INTERNAL,
+ };
+ } else if let Some(rest) = trimmed.strip_prefix("desc:") {
+ let text = rest.trim().to_string();
+ if lock.description.is_empty() {
+ lock.description = text;
+ } else {
+ lock.description.push(' ');
+ lock.description.push_str(&text);
+ }
+ } else if let Some(rest) = trimmed.strip_prefix("acquired:") {
+ // Same rule as kdoc_apispec.py: a lock that is both
+ // acquired and released stays KAPI_LOCK_INTERNAL.
+ if matches!(rest.trim().to_lowercase().as_str(), "true" | "yes") {
+ lock.scope = if lock.scope == super::KAPI_LOCK_RELEASES {
+ super::KAPI_LOCK_INTERNAL
+ } else {
+ super::KAPI_LOCK_ACQUIRES
+ };
+ }
+ } else if let Some(rest) = trimmed.strip_prefix("released:") {
+ if matches!(rest.trim().to_lowercase().as_str(), "true" | "yes") {
+ lock.scope = if lock.scope == super::KAPI_LOCK_ACQUIRES {
+ super::KAPI_LOCK_INTERNAL
+ } else {
+ super::KAPI_LOCK_RELEASES
+ };
+ }
+ } else {
+ // Continuation of description
+ if !lock.description.is_empty() {
+ lock.description.push(' ');
+ lock.description.push_str(trimmed);
+ }
+ }
+ }
+ }
+ BlockContext::Return => {
+ if let Some(ret) = current_return.as_mut() {
+ if let Some(rest) = trimmed.strip_prefix("type:") {
+ let raw = rest.trim();
+ ret.type_name = raw.to_string();
+ ret.return_type = self.parse_param_type(raw);
+ } else if let Some(rest) = trimmed.strip_prefix("check-type:") {
+ ret.check_type = self.parse_return_check_type(rest.trim());
+ } else if let Some(rest) = trimmed.strip_prefix("success:") {
+ (ret.success_value, ret.success_min, ret.success_max) =
+ parse_return_success(rest.trim());
+ } else if let Some(rest) = trimmed.strip_prefix("desc:") {
+ let text = rest.trim().to_string();
+ if ret.description.is_empty() {
+ ret.description = text;
+ } else {
+ ret.description.push(' ');
+ ret.description.push_str(&text);
+ }
+ } else {
+ // Continuation of description
+ if !ret.description.is_empty() {
+ ret.description.push(' ');
+ ret.description.push_str(trimmed);
+ }
+ }
+ }
+ }
+ BlockContext::StateTransition => {
+ if let Some(trans) = current_state_trans.as_mut() {
+ if let Some(rest) = trimmed.strip_prefix("object:") {
+ trans.object = rest.trim().to_string();
+ } else if let Some(rest) = trimmed.strip_prefix("from:") {
+ trans.from_state = rest.trim().to_string();
+ } else if let Some(rest) = trimmed.strip_prefix("to:") {
+ trans.to_state = rest.trim().to_string();
+ } else if let Some(rest) = trimmed.strip_prefix("condition:") {
+ trans.condition = Some(rest.trim().to_string());
+ } else if let Some(rest) = trimmed.strip_prefix("desc:") {
+ trans.description = rest.trim().to_string();
+ }
+ }
+ }
+ BlockContext::None => {}
+ }
+ }
+
+ /// Flush the current block, pushing items into the spec
+ #[allow(clippy::too_many_arguments)]
+ fn flush_block(
+ &self,
+ block: &mut BlockContext,
+ spec: &mut ApiSpec,
+ current_error: &mut Option<ErrorSpec>,
+ current_signal: &mut Option<SignalSpec>,
+ current_capability: &mut Option<CapabilitySpec>,
+ current_side_effect: &mut Option<SideEffectSpec>,
+ current_constraint: &mut Option<ConstraintSpec>,
+ current_lock: &mut Option<LockSpec>,
+ current_return: &mut Option<ReturnSpec>,
+ current_state_trans: &mut Option<StateTransitionSpec>,
+ ) {
+ match block {
+ BlockContext::Error(_) => {
+ if let Some(error) = current_error.take() {
+ spec.errors.push(error);
+ }
+ }
+ BlockContext::Signal => {
+ if let Some(signal) = current_signal.take() {
+ spec.signals.push(signal);
+ }
+ }
+ BlockContext::Capability => {
+ if let Some(cap) = current_capability.take() {
+ spec.capabilities.push(cap);
+ }
+ }
+ BlockContext::SideEffect => {
+ if let Some(effect) = current_side_effect.take() {
+ spec.side_effects.push(effect);
+ }
+ }
+ BlockContext::Constraint => {
+ if let Some(constraint) = current_constraint.take() {
+ spec.constraints.push(constraint);
+ }
+ }
+ BlockContext::Lock => {
+ if let Some(lock) = current_lock.take() {
+ spec.locks.push(lock);
+ }
+ }
+ BlockContext::Return => {
+ if let Some(mut ret) = current_return.take() {
+ ret.keep_used_success_fields();
+ spec.return_spec = Some(ret);
+ }
+ }
+ BlockContext::StateTransition => {
+ if let Some(trans) = current_state_trans.take() {
+ spec.state_transitions.push(trans);
+ }
+ }
+ _ => {}
+ }
+ *block = BlockContext::None;
+ }
+
+ /// Extract parameter type names from SYSCALL_DEFINE signature
+ fn extract_types_from_signature(&self, sig: &str) -> HashMap<String, String> {
+ let mut types = HashMap::new();
+
+ // Find content between outermost parens
+ let content = if let Some(start) = sig.find('(') {
+ let end = sig.rfind(')').unwrap_or(sig.len());
+ &sig[start + 1..end]
+ } else {
+ return types;
+ };
+
+ // Split by comma and process type/name pairs
+ // SYSCALL_DEFINE format: (syscall_name, type1, name1, type2, name2, ...)
+ let parts: Vec<&str> = content.split(',').map(|s| s.trim()).collect();
+
+ // Skip first part (syscall name), then process pairs
+ let mut i = 1;
+ while i + 1 < parts.len() {
+ let type_part = parts[i].trim();
+ let name_part = parts[i + 1].trim();
+
+ // Build the type_name string: "type name"
+ let type_name = format!("{} {}", type_part, name_part);
+ types.insert(name_part.to_string(), type_name);
+
+ i += 2;
+ }
+
+ types
+ }
+
+ /// Collect the raw lines of a free-form section: the text after the
+ /// key plus every following indented or blank line, up to the next
+ /// annotation line.
+ fn collect_section<'a>(
+ &self,
+ lines: &[&'a str],
+ start_idx: usize,
+ first_part: &'a str,
+ ) -> (Vec<&'a str>, usize) {
+ let mut section = vec![first_part];
+ let mut i = start_idx + 1;
+
+ while i < lines.len() {
+ let line = lines[i];
+ if self.is_annotation_line(line) {
+ break;
+ }
+ if !line.trim().is_empty() && !is_indented_line(line) {
+ break;
+ }
+ section.push(line);
+ i += 1;
+ }
+
+ (section, i)
+ }
+
+ fn is_annotation_line(&self, line: &str) -> bool {
+ let trimmed = line.trim_start();
+ if !trimmed.contains(':') {
+ return false;
+ }
+ let annotations = [
+ "param:",
+ "param-count:",
+ "error:",
+ "lock:",
+ "signal:",
+ "side-effect:",
+ "state-trans:",
+ "capability:",
+ "constraint:",
+ "return:",
+ "examples:",
+ "notes:",
+ "context-",
+ "long-desc:",
+ "api-type:",
+ ];
+
+ for ann in &annotations {
+ if trimmed.starts_with(ann) {
+ return true;
+ }
+ }
+ false
+ }
+
+ /// Parse a constraint expression and apply it to `param`.
+ /// Shapes:
+ /// NAME (e.g. "user_path", "nonzero")
+ /// NAME ( ARG (, ARG)* ) (e.g. "range(0, 4096)", "buffer(2)")
+ /// Returns true if the expression matched a known constraint kind,
+ /// populating `param`'s numeric fields. Returns false if the text
+ /// is free-form, leaving `param` untouched.
+ fn apply_constraint_expr(&self, param: &mut ParamSpec, text: &str) -> bool {
+ let t = text.trim();
+ if t.is_empty() {
+ return false;
+ }
+ // Split NAME ( ARGS ); no nesting, no escaping.
+ let (name, args): (&str, Option<&str>) = match (t.find('('), t.rfind(')')) {
+ (Some(lp), Some(rp)) if rp > lp => (t[..lp].trim(), Some(t[lp + 1..rp].trim())),
+ _ => (t, None),
+ };
+ // Bail out on anything that looks like free text (spaces inside the
+ // name part).
+ if name.contains(char::is_whitespace) || name.is_empty() {
+ return false;
+ }
+ let name_lc = name.to_ascii_lowercase();
+ let split_args = || -> Vec<String> {
+ args.map(|a| a.split(',').map(|s| s.trim().to_string()).collect())
+ .unwrap_or_default()
+ };
+ match name_lc.as_str() {
+ "range" => {
+ let a = split_args();
+ if a.len() != 2 {
+ return false;
+ }
+ param.min_value = parse_i64_literal(&a[0]);
+ param.max_value = parse_i64_literal(&a[1]);
+ param.constraint_type = 1; // KAPI_CONSTRAINT_RANGE
+ true
+ }
+ "mask" => {
+ let a = split_args();
+ if a.len() != 1 {
+ return false;
+ }
+ // Symbolic masks (e.g. "O_RDONLY | O_WRONLY | ...") can't
+ // be resolved at parse time; leave valid_mask as None so
+ // downstream consumers treat the mask as unknown, matching
+ // the long-form `valid-mask:` handler (which also leaves
+ // the slot untouched when the value isn't a literal).
+ param.valid_mask = parse_u64_literal(&a[0]);
+ param.constraint_type = 2; // KAPI_CONSTRAINT_MASK
+ true
+ }
+ "enum" => {
+ let a = split_args();
+ if a.is_empty() {
+ return false;
+ }
+ // --vmlinux and --debugfs report each value in decimal, so
+ // numeric literals are normalised; symbolic names stay.
+ param.enum_values = a
+ .into_iter()
+ .map(|v| parse_i64_literal(&v).map_or(v, |n| n.to_string()))
+ .collect();
+ param.constraint_type = 3; // KAPI_CONSTRAINT_ENUM
+ true
+ }
+ "alignment" | "align" => {
+ let a = split_args();
+ if a.len() != 1 {
+ return false;
+ }
+ param.alignment = parse_u64_literal(&a[0]).and_then(|n| u32::try_from(n).ok());
+ param.constraint_type = 4; // KAPI_CONSTRAINT_ALIGNMENT
+ true
+ }
+ "power_of_two" => {
+ if args.is_some() {
+ return false;
+ }
+ param.constraint_type = 5; // KAPI_CONSTRAINT_POWER_OF_TWO
+ true
+ }
+ "page_aligned" => {
+ if args.is_some() {
+ return false;
+ }
+ param.constraint_type = 6; // KAPI_CONSTRAINT_PAGE_ALIGNED
+ true
+ }
+ "nonzero" => {
+ if args.is_some() {
+ return false;
+ }
+ param.constraint_type = 7; // KAPI_CONSTRAINT_NONZERO
+ true
+ }
+ "user_string" => {
+ // Optional size argument: user_string(N)
+ if let Some(arg) = args {
+ if let Some(n) = parse_u64_literal(arg).and_then(|n| u32::try_from(n).ok()) {
+ param.size = Some(n);
+ }
+ }
+ param.constraint_type = 8; // KAPI_CONSTRAINT_USER_STRING
+ true
+ }
+ "user_path" => {
+ if args.is_some() {
+ return false;
+ }
+ param.constraint_type = 9; // KAPI_CONSTRAINT_USER_PATH
+ true
+ }
+ "user_ptr" => {
+ if args.is_some() {
+ return false;
+ }
+ param.constraint_type = 10; // KAPI_CONSTRAINT_USER_PTR
+ true
+ }
+ "buffer" => {
+ // buffer(size_param_idx): capture the index into
+ // param.size_param_idx so it matches the long-form
+ // `size-param: N` handler below (and the C struct
+ // field populated by KAPI_PARAM_SIZE_PARAM()).
+ let a = split_args();
+ if a.len() != 1 {
+ return false;
+ }
+ param.size_param_idx = a[0].parse().ok();
+ param.constraint_type = 11; // KAPI_CONSTRAINT_BUFFER
+ true
+ }
+ "custom" => {
+ // custom(fn_name): record function name as free-text constraint
+ // so downstream tooling can wire it up.
+ if let Some(arg) = args {
+ param.constraint = Some(arg.trim().to_string());
+ }
+ param.constraint_type = 12; // KAPI_CONSTRAINT_CUSTOM
+ true
+ }
+ _ => false,
+ }
+ }
+
+ fn parse_context_flags(&self, flags: &str) -> Vec<String> {
+ flags
+ .split('|')
+ .map(|f| self.ctx_alias(f.trim()).to_string())
+ .filter(|f| !f.is_empty())
+ .collect()
+ }
+
+ /// Parse a comma-separated short-form context list
+ /// (e.g. "process, sleepable" -> ["KAPI_CTX_PROCESS", "KAPI_CTX_SLEEPABLE"]).
+ /// Tokens that already look like KAPI_CTX_* are passed through.
+ fn parse_context_list(&self, flags: &str) -> Vec<String> {
+ flags
+ .split(',')
+ .map(|f| self.ctx_alias(f.trim()).to_string())
+ .filter(|f| !f.is_empty())
+ .collect()
+ }
+
+ /// Canonicalise a single context token to its KAPI_CTX_* spelling.
+ /// Short aliases are case-insensitive. Unknown tokens pass through
+ /// verbatim so mixed/long-form input keeps working.
+ fn ctx_alias(&self, tok: &str) -> String {
+ let t = tok.trim();
+ if t.is_empty() {
+ return String::new();
+ }
+ match t.to_ascii_lowercase().as_str() {
+ "process" => "KAPI_CTX_PROCESS".to_string(),
+ "softirq" => "KAPI_CTX_SOFTIRQ".to_string(),
+ "hardirq" => "KAPI_CTX_HARDIRQ".to_string(),
+ "nmi" => "KAPI_CTX_NMI".to_string(),
+ "atomic" => "KAPI_CTX_ATOMIC".to_string(),
+ "sleepable" => "KAPI_CTX_SLEEPABLE".to_string(),
+ "preempt_disabled" => "KAPI_CTX_PREEMPT_DISABLED".to_string(),
+ "irq_disabled" => "KAPI_CTX_IRQ_DISABLED".to_string(),
+ _ => t.to_string(),
+ }
+ }
+
+ fn error_name_to_code(&self, name: &str) -> i32 {
+ match name {
+ "EPERM" => -1,
+ "ENOENT" => -2,
+ "ESRCH" => -3,
+ "EINTR" => -4,
+ "EIO" => -5,
+ "ENXIO" => -6,
+ "E2BIG" => -7,
+ "ENOEXEC" => -8,
+ "EBADF" => -9,
+ "ECHILD" => -10,
+ "EAGAIN" | "EWOULDBLOCK" => -11,
+ "ENOMEM" => -12,
+ "EACCES" => -13,
+ "EFAULT" => -14,
+ "ENOTBLK" => -15,
+ "EBUSY" => -16,
+ "EEXIST" => -17,
+ "EXDEV" => -18,
+ "ENODEV" => -19,
+ "ENOTDIR" => -20,
+ "EISDIR" => -21,
+ "EINVAL" => -22,
+ "ENFILE" => -23,
+ "EMFILE" => -24,
+ "ENOTTY" => -25,
+ "ETXTBSY" => -26,
+ "EFBIG" => -27,
+ "ENOSPC" => -28,
+ "ESPIPE" => -29,
+ "EROFS" => -30,
+ "EMLINK" => -31,
+ "EPIPE" => -32,
+ "EDOM" => -33,
+ "ERANGE" => -34,
+ "EDEADLK" => -35,
+ "ENAMETOOLONG" => -36,
+ "ENOLCK" => -37,
+ "ENOSYS" => -38,
+ "ENOTEMPTY" => -39,
+ "ELOOP" => -40,
+ "ENOMSG" => -42,
+ "ENODATA" => -61,
+ "ENOLINK" => -67,
+ "EPROTO" => -71,
+ "EOVERFLOW" => -75,
+ "ELIBBAD" => -80,
+ "EILSEQ" => -84,
+ "ENOTSOCK" => -88,
+ "EDESTADDRREQ" => -89,
+ "EMSGSIZE" => -90,
+ "EPROTOTYPE" => -91,
+ "ENOPROTOOPT" => -92,
+ "EPROTONOSUPPORT" => -93,
+ "EOPNOTSUPP" | "ENOTSUP" => -95,
+ "EADDRINUSE" => -98,
+ "EADDRNOTAVAIL" => -99,
+ "ENETDOWN" => -100,
+ "ENETUNREACH" => -101,
+ "ENETRESET" => -102,
+ "ECONNABORTED" => -103,
+ "ECONNRESET" => -104,
+ "ENOBUFS" => -105,
+ "EISCONN" => -106,
+ "ENOTCONN" => -107,
+ "ETIMEDOUT" => -110,
+ "ECONNREFUSED" => -111,
+ "EALREADY" => -114,
+ "EINPROGRESS" => -115,
+ "ESTALE" => -116,
+ "EDQUOT" => -122,
+ "ENOMEDIUM" => -123,
+ "ENOKEY" => -126,
+ "EHWPOISON" => -133,
+ "ERESTARTSYS" => -512,
+ _ => 0,
+ }
+ }
+
+ /// Map a KAPI_TYPE_* token (or its short-form alias) to the numeric
+ /// value declared in `enum kapi_param_type` in
+ /// `include/linux/kernel_api_spec.h`.
+ fn parse_param_type(&self, type_str: &str) -> u32 {
+ let s = type_str.trim();
+ match s {
+ "KAPI_TYPE_VOID" => 0,
+ "KAPI_TYPE_INT" => 1,
+ "KAPI_TYPE_UINT" => 2,
+ "KAPI_TYPE_PTR" => 3,
+ "KAPI_TYPE_STRUCT" => 4,
+ "KAPI_TYPE_UNION" => 5,
+ "KAPI_TYPE_ENUM" => 6,
+ "KAPI_TYPE_FUNC_PTR" => 7,
+ "KAPI_TYPE_ARRAY" => 8,
+ "KAPI_TYPE_FD" => 9,
+ "KAPI_TYPE_USER_PTR" => 10,
+ "KAPI_TYPE_PATH" => 11,
+ "KAPI_TYPE_CUSTOM" => 12,
+ _ => match s.to_ascii_lowercase().as_str() {
+ "void" => 0,
+ "int" => 1,
+ "uint" => 2,
+ "ptr" => 3,
+ "struct" => 4,
+ "union" => 5,
+ "enum" => 6,
+ "func_ptr" => 7,
+ "array" => 8,
+ "fd" => 9,
+ "user_ptr" | "uptr" => 10,
+ "path" => 11,
+ "custom" => 12,
+ _ => 0,
+ },
+ }
+ }
+
+ /// Map a KAPI_CONSTRAINT_* token to the numeric value declared in
+ /// `enum kapi_constraint_type` in `include/linux/kernel_api_spec.h`.
+ fn parse_constraint_type(&self, type_str: &str) -> u32 {
+ let s = type_str.trim();
+ match s {
+ "KAPI_CONSTRAINT_NONE" => 0,
+ "KAPI_CONSTRAINT_RANGE" => 1,
+ "KAPI_CONSTRAINT_MASK" => 2,
+ "KAPI_CONSTRAINT_ENUM" => 3,
+ "KAPI_CONSTRAINT_ALIGNMENT" => 4,
+ "KAPI_CONSTRAINT_POWER_OF_TWO" => 5,
+ "KAPI_CONSTRAINT_PAGE_ALIGNED" => 6,
+ "KAPI_CONSTRAINT_NONZERO" => 7,
+ "KAPI_CONSTRAINT_USER_STRING" => 8,
+ "KAPI_CONSTRAINT_USER_PATH" => 9,
+ "KAPI_CONSTRAINT_USER_PTR" => 10,
+ "KAPI_CONSTRAINT_BUFFER" => 11,
+ "KAPI_CONSTRAINT_CUSTOM" => 12,
+ _ => 0,
+ }
+ }
+
+ fn parse_param_flags(&self, flags: &str) -> u32 {
+ flags
+ .split('|')
+ .map(|f| self.parse_param_flag_token(f.trim()))
+ .fold(0, |acc, bit| acc | bit)
+ }
+
+ /// Parse one flag token (long or short form, case-insensitive for
+ /// short form). Returns 0 for unknown tokens.
+ fn parse_param_flag_token(&self, tok: &str) -> u32 {
+ let t = tok.trim();
+ // KAPI_PARAM_* names and upper-case short forms first.
+ match t {
+ "KAPI_PARAM_IN" | "IN" => return 1,
+ "KAPI_PARAM_OUT" | "OUT" => return 2,
+ "KAPI_PARAM_INOUT" | "INOUT" => return 3,
+ "KAPI_PARAM_OPTIONAL" | "OPTIONAL" => return 1 << 3,
+ "KAPI_PARAM_CONST" | "CONST" => return 1 << 4,
+ "KAPI_PARAM_VOLATILE" | "VOLATILE" => return 1 << 5,
+ "KAPI_PARAM_USER" | "USER" => return 1 << 6,
+ "KAPI_PARAM_DMA" | "DMA" => return 1 << 7,
+ "KAPI_PARAM_ALIGNED" | "ALIGNED" => return 1 << 8,
+ _ => {}
+ }
+ // English short aliases (case-insensitive).
+ match t.to_ascii_lowercase().as_str() {
+ "input" => 1,
+ "output" => 2,
+ "inout" => 3,
+ "optional" => 1 << 3,
+ "const" => 1 << 4,
+ "volatile" => 1 << 5,
+ "user" => 1 << 6,
+ "dma" => 1 << 7,
+ "aligned" => 1 << 8,
+ _ => 0,
+ }
+ }
+
+ /// Map a KAPI_LOCK_* token to the numeric value declared in
+ /// `enum kapi_lock_type` in `include/linux/kernel_api_spec.h`.
+ fn parse_lock_type(&self, type_str: &str) -> u32 {
+ let s = type_str.trim();
+ match s {
+ "KAPI_LOCK_NONE" => 0,
+ "KAPI_LOCK_MUTEX" => 1,
+ "KAPI_LOCK_SPINLOCK" => 2,
+ "KAPI_LOCK_RWLOCK" => 3,
+ "KAPI_LOCK_SEQLOCK" => 4,
+ "KAPI_LOCK_RCU" => 5,
+ "KAPI_LOCK_SEMAPHORE" => 6,
+ "KAPI_LOCK_CUSTOM" => 7,
+ _ => match s.to_ascii_lowercase().as_str() {
+ "none" => 0,
+ "mutex" => 1,
+ "spinlock" => 2,
+ "rwlock" => 3,
+ "seqlock" => 4,
+ "rcu" => 5,
+ "semaphore" => 6,
+ "custom" => 7,
+ _ => 0,
+ },
+ }
+ }
+
+ fn parse_signal_direction(&self, dir: &str) -> u32 {
+ let s = dir.trim();
+ match s {
+ "KAPI_SIGNAL_RECEIVE" => 1,
+ "KAPI_SIGNAL_SEND" => 2,
+ "KAPI_SIGNAL_HANDLE" => 4,
+ "KAPI_SIGNAL_BLOCK" => 8,
+ "KAPI_SIGNAL_IGNORE" => 16,
+ _ => match s.to_ascii_lowercase().as_str() {
+ "receive" => 1,
+ "send" => 2,
+ "handle" => 4,
+ "block" => 8,
+ "ignore" => 16,
+ _ => 0,
+ },
+ }
+ }
+
+ fn parse_signal_action(&self, action: &str) -> u32 {
+ let s = action.trim();
+ match s {
+ "KAPI_SIGNAL_ACTION_DEFAULT" => 0,
+ "KAPI_SIGNAL_ACTION_TERMINATE" => 1,
+ "KAPI_SIGNAL_ACTION_COREDUMP" => 2,
+ "KAPI_SIGNAL_ACTION_STOP" => 3,
+ "KAPI_SIGNAL_ACTION_CONTINUE" => 4,
+ "KAPI_SIGNAL_ACTION_CUSTOM" => 5,
+ "KAPI_SIGNAL_ACTION_RETURN" => 6,
+ "KAPI_SIGNAL_ACTION_RESTART" => 7,
+ "KAPI_SIGNAL_ACTION_QUEUE" => 8,
+ "KAPI_SIGNAL_ACTION_DISCARD" => 9,
+ "KAPI_SIGNAL_ACTION_TRANSFORM" => 10,
+ _ => match s.to_ascii_lowercase().as_str() {
+ "default" => 0,
+ "terminate" => 1,
+ "coredump" => 2,
+ "stop" => 3,
+ "continue" => 4,
+ "custom" => 5,
+ "return" => 6,
+ "restart" => 7,
+ "queue" => 8,
+ "discard" => 9,
+ "transform" => 10,
+ _ => 0,
+ },
+ }
+ }
+
+ fn parse_signal_timing(&self, timing: &str) -> u32 {
+ let s = timing.trim();
+ match s {
+ "KAPI_SIGNAL_TIME_BEFORE" => 0,
+ "KAPI_SIGNAL_TIME_DURING" => 1,
+ "KAPI_SIGNAL_TIME_AFTER" => 2,
+ _ => match s.to_ascii_lowercase().as_str() {
+ "before" => 0,
+ "during" => 1,
+ "after" => 2,
+ _ => 0,
+ },
+ }
+ }
+
+ /// Accept a hex literal ("0x4"), a decimal literal ("4"), or a '|'-separated
+ /// bitmask expression. Unknown tokens contribute 0.
+ fn parse_hex_or_bitmask(&self, value: &str) -> u32 {
+ let v = value.trim();
+ if let Some(hex) = v.strip_prefix("0x").or_else(|| v.strip_prefix("0X")) {
+ if let Ok(n) = u32::from_str_radix(hex, 16) {
+ return n;
+ }
+ }
+ if let Ok(n) = v.parse::<u32>() {
+ return n;
+ }
+ let mut acc = 0u32;
+ for part in v.split(['|', ',']) {
+ let t = part.trim();
+ if t.is_empty() {
+ continue;
+ }
+ if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
+ if let Ok(n) = u32::from_str_radix(hex, 16) {
+ acc |= n;
+ continue;
+ }
+ }
+ if let Ok(n) = t.parse::<u32>() {
+ acc |= n;
+ }
+ }
+ acc
+ }
+
+ /// Parse a '|'-separated list of KAPI_SIGNAL_STATE_* tokens (or short
+ /// names like "RUNNING") and OR their bit values together. Matches the
+ /// BIT(N) definitions in kernel_api_spec.h.
+ fn parse_signal_state_mask(&self, value: &str) -> u32 {
+ let mut acc = 0u32;
+ for part in value.split(['|', ',']) {
+ let t = part.trim().trim_start_matches("KAPI_SIGNAL_STATE_");
+ let bit = match t.to_ascii_uppercase().as_str() {
+ "RUNNING" => 1 << 0,
+ "SLEEPING" => 1 << 1,
+ "STOPPED" => 1 << 2,
+ "TRACED" => 1 << 3,
+ "ZOMBIE" => 1 << 4,
+ "DEAD" => 1 << 5,
+ _ => 0,
+ };
+ acc |= bit;
+ }
+ acc
+ }
+
+ /// Bitmask of `KAPI_EFFECT_*` values joined by '|' or ','.
+ /// Values match `enum kapi_side_effect_type` in
+ /// `include/linux/kernel_api_spec.h`.
+ fn parse_effect_type(&self, type_str: &str) -> u32 {
+ let sep = if type_str.contains('|') || !type_str.contains(',') {
+ '|'
+ } else {
+ ','
+ };
+ let mut result = 0;
+ for flag in type_str.split(sep) {
+ let t = flag.trim();
+ let bit = match t {
+ "KAPI_EFFECT_NONE" => 0,
+ "KAPI_EFFECT_ALLOC_MEMORY" => 1 << 0,
+ "KAPI_EFFECT_FREE_MEMORY" => 1 << 1,
+ "KAPI_EFFECT_MODIFY_STATE" => 1 << 2,
+ "KAPI_EFFECT_SIGNAL_SEND" => 1 << 3,
+ "KAPI_EFFECT_FILE_POSITION" => 1 << 4,
+ "KAPI_EFFECT_LOCK_ACQUIRE" => 1 << 5,
+ "KAPI_EFFECT_LOCK_RELEASE" => 1 << 6,
+ "KAPI_EFFECT_RESOURCE_CREATE" => 1 << 7,
+ "KAPI_EFFECT_RESOURCE_DESTROY" => 1 << 8,
+ "KAPI_EFFECT_SCHEDULE" => 1 << 9,
+ "KAPI_EFFECT_HARDWARE" => 1 << 10,
+ "KAPI_EFFECT_NETWORK" => 1 << 11,
+ "KAPI_EFFECT_FILESYSTEM" => 1 << 12,
+ "KAPI_EFFECT_PROCESS_STATE" => 1 << 13,
+ "KAPI_EFFECT_IRREVERSIBLE" => 1 << 14,
+ _ => match t.to_ascii_lowercase().as_str() {
+ "none" => 0,
+ "alloc_memory" => 1 << 0,
+ "free_memory" => 1 << 1,
+ "modify_state" => 1 << 2,
+ "signal_send" => 1 << 3,
+ "file_position" => 1 << 4,
+ "lock_acquire" => 1 << 5,
+ "lock_release" => 1 << 6,
+ "resource_create" => 1 << 7,
+ "resource_destroy" => 1 << 8,
+ "schedule" => 1 << 9,
+ "hardware" => 1 << 10,
+ "network" => 1 << 11,
+ "filesystem" => 1 << 12,
+ "process_state" => 1 << 13,
+ "irreversible" => 1 << 14,
+ _ => 0,
+ },
+ };
+ result |= bit;
+ }
+ result
+ }
+
+ fn parse_capability_value(&self, cap: &str) -> i32 {
+ match cap {
+ "CAP_CHOWN" => 0,
+ "CAP_DAC_OVERRIDE" => 1,
+ "CAP_DAC_READ_SEARCH" => 2,
+ "CAP_FOWNER" => 3,
+ "CAP_FSETID" => 4,
+ "CAP_KILL" => 5,
+ "CAP_SETGID" => 6,
+ "CAP_SETUID" => 7,
+ "CAP_SETPCAP" => 8,
+ "CAP_LINUX_IMMUTABLE" => 9,
+ "CAP_NET_BIND_SERVICE" => 10,
+ "CAP_NET_BROADCAST" => 11,
+ "CAP_NET_ADMIN" => 12,
+ "CAP_NET_RAW" => 13,
+ "CAP_IPC_LOCK" => 14,
+ "CAP_IPC_OWNER" => 15,
+ "CAP_SYS_MODULE" => 16,
+ "CAP_SYS_RAWIO" => 17,
+ "CAP_SYS_CHROOT" => 18,
+ "CAP_SYS_PTRACE" => 19,
+ "CAP_SYS_PACCT" => 20,
+ "CAP_SYS_ADMIN" => 21,
+ "CAP_SYS_BOOT" => 22,
+ "CAP_SYS_NICE" => 23,
+ "CAP_SYS_RESOURCE" => 24,
+ "CAP_SYS_TIME" => 25,
+ "CAP_SYS_TTY_CONFIG" => 26,
+ "CAP_MKNOD" => 27,
+ "CAP_LEASE" => 28,
+ "CAP_AUDIT_WRITE" => 29,
+ "CAP_AUDIT_CONTROL" => 30,
+ "CAP_SETFCAP" => 31,
+ "CAP_MAC_OVERRIDE" => 32,
+ "CAP_MAC_ADMIN" => 33,
+ "CAP_SYSLOG" => 34,
+ "CAP_WAKE_ALARM" => 35,
+ "CAP_BLOCK_SUSPEND" => 36,
+ "CAP_AUDIT_READ" => 37,
+ "CAP_PERFMON" => 38,
+ "CAP_BPF" => 39,
+ "CAP_CHECKPOINT_RESTORE" => 40,
+ _ => 0,
+ }
+ }
+
+ /// Map a KAPI_RETURN_* token to the numeric value declared in
+ /// `enum kapi_return_check_type` in `include/linux/kernel_api_spec.h`.
+ fn parse_return_check_type(&self, check: &str) -> u32 {
+ let s = check.trim();
+ match s {
+ "KAPI_RETURN_EXACT" => 0,
+ "KAPI_RETURN_RANGE" => 1,
+ "KAPI_RETURN_ERROR_CHECK" => 2,
+ "KAPI_RETURN_FD" => 3,
+ "KAPI_RETURN_CUSTOM" => 4,
+ "KAPI_RETURN_NO_RETURN" => 5,
+ _ => match s.to_ascii_lowercase().as_str() {
+ "exact" => 0,
+ "range" => 1,
+ "error_check" => 2,
+ "fd" => 3,
+ "custom" => 4,
+ "no_return" => 5,
+ _ => 0,
+ },
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn parser() -> KerneldocParser {
+ KerneldocParser::new()
+ }
+
+ #[test]
+ fn parse_minimal_kerneldoc() {
+ let doc = "\
+sys_foo - Do something useful
+context-flags: KAPI_CTX_PROCESS
+param-count: 1
+@fd: The file descriptor
+error: EBADF, Bad file descriptor
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_foo", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.name, "sys_foo");
+ assert_eq!(spec.api_type, "syscall");
+ assert_eq!(spec.description.as_deref(), Some("Do something useful"));
+ assert_eq!(spec.param_count, Some(1));
+ assert_eq!(spec.parameters.len(), 1);
+ assert_eq!(spec.parameters[0].name, "fd");
+ assert_eq!(spec.parameters[0].description, "The file descriptor");
+ assert_eq!(spec.errors.len(), 1);
+ assert_eq!(spec.errors[0].name, "EBADF");
+ assert_eq!(spec.errors[0].error_code, -9);
+ }
+
+ #[test]
+ fn parse_multiple_param_types() {
+ let doc = "\
+sys_bar - Multiple params
+@fd: file descriptor arg
+@buf: user buffer
+@count: byte count
+@flags: option flags
+param: fd
+ type: KAPI_TYPE_FD
+param: buf
+ type: KAPI_TYPE_USER_PTR
+param: count
+ type: KAPI_TYPE_UINT
+param: flags
+ type: KAPI_TYPE_UINT
+";
+ let sig = "(bar, int, fd, char __user *, buf, size_t, count, unsigned long, flags)";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_bar", "syscall", Some(sig))
+ .unwrap();
+
+ assert_eq!(spec.parameters.len(), 4);
+
+ let fd_param = spec.parameters.iter().find(|p| p.name == "fd").unwrap();
+ assert_eq!(fd_param.param_type, 9); // FD (kernel enum)
+
+ let buf_param = spec.parameters.iter().find(|p| p.name == "buf").unwrap();
+ assert_eq!(buf_param.param_type, 10); // USER_PTR (kernel enum)
+ assert_eq!(buf_param.type_name, "char __user * buf");
+
+ let count_param = spec.parameters.iter().find(|p| p.name == "count").unwrap();
+ assert_eq!(count_param.param_type, 2); // UINT
+
+ let flags_param = spec.parameters.iter().find(|p| p.name == "flags").unwrap();
+ assert_eq!(flags_param.param_type, 2); // UINT
+ }
+
+ #[test]
+ fn parse_error_codes_with_descriptions() {
+ let doc = "\
+sys_err - Error test
+error: EBADF
+ desc: Bad file descriptor
+ condition: fd < 0
+error: EFAULT
+ desc: Bad user pointer
+ condition: buf is NULL
+error: EINVAL
+ desc: Invalid argument
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_err", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.errors.len(), 3);
+
+ assert_eq!(spec.errors[0].name, "EBADF");
+ assert_eq!(spec.errors[0].error_code, -9);
+ assert_eq!(spec.errors[0].description, "Bad file descriptor");
+ assert_eq!(spec.errors[0].condition, "fd < 0");
+
+ assert_eq!(spec.errors[1].name, "EFAULT");
+ assert_eq!(spec.errors[1].error_code, -14);
+ assert_eq!(spec.errors[1].description, "Bad user pointer");
+
+ assert_eq!(spec.errors[2].name, "EINVAL");
+ assert_eq!(spec.errors[2].error_code, -22);
+ assert_eq!(spec.errors[2].description, "Invalid argument");
+ }
+
+ #[test]
+ fn parse_context_flags() {
+ let doc = "\
+sys_ctx - Context test
+context-flags: KAPI_CTX_PROCESS|KAPI_CTX_SLEEPABLE
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_ctx", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.context_flags.len(), 2);
+ assert_eq!(spec.context_flags[0], "KAPI_CTX_PROCESS");
+ assert_eq!(spec.context_flags[1], "KAPI_CTX_SLEEPABLE");
+ }
+
+ #[test]
+ fn parse_context_list_short() {
+ // "contexts: process, sleepable" -> KAPI_CTX_PROCESS | SLEEPABLE
+ let doc = "\
+sys_ctx - Context test
+contexts: process, sleepable
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_ctx", "syscall", None)
+ .unwrap();
+
+ assert_eq!(
+ spec.context_flags,
+ vec![
+ "KAPI_CTX_PROCESS".to_string(),
+ "KAPI_CTX_SLEEPABLE".to_string(),
+ ]
+ );
+ }
+
+ #[test]
+ fn parse_context_list_mixed() {
+ // Short tokens intermixed with explicit KAPI_CTX_* still work.
+ let doc = "\
+sys_ctx - Context test
+contexts: process, KAPI_CTX_SLEEPABLE, softirq
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_ctx", "syscall", None)
+ .unwrap();
+
+ assert_eq!(
+ spec.context_flags,
+ vec![
+ "KAPI_CTX_PROCESS".to_string(),
+ "KAPI_CTX_SLEEPABLE".to_string(),
+ "KAPI_CTX_SOFTIRQ".to_string(),
+ ]
+ );
+ }
+
+ #[test]
+ fn parse_context_flags_long_with_short_token() {
+ // Long-form "context-flags:" accepts "|"-joined short aliases.
+ let doc = "\
+sys_ctx - Context test
+context-flags: process | KAPI_CTX_SLEEPABLE
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_ctx", "syscall", None)
+ .unwrap();
+
+ assert_eq!(
+ spec.context_flags,
+ vec![
+ "KAPI_CTX_PROCESS".to_string(),
+ "KAPI_CTX_SLEEPABLE".to_string(),
+ ]
+ );
+ }
+
+ #[test]
+ fn parse_param_type_short_combined() {
+ // "type: uint, input" combines the type and flag aliases.
+ let doc = "\
+sys_t - Short type test
+param: size
+ type: uint, input
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_t", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.parameters.len(), 1);
+ assert_eq!(spec.parameters[0].param_type, 2); // KAPI_TYPE_UINT
+ assert_eq!(spec.parameters[0].flags, 1); // KAPI_PARAM_IN
+ }
+
+ #[test]
+ fn parse_param_type_short_multi_flag() {
+ // "type: path, input, user" sets both the IN and USER flags.
+ let doc = "\
+sys_t - Short type test
+param: filename
+ type: path, input, user
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_t", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.parameters.len(), 1);
+ assert_eq!(spec.parameters[0].param_type, 11); // PATH (kernel enum)
+ assert_eq!(spec.parameters[0].flags, 1 | (1 << 6)); // IN | USER
+ }
+
+ #[test]
+ fn parse_constraint_type_range_expr() {
+ // Short form: "constraint-type: range(0, 4096)" replaces the
+ // two-line long form "constraint-type: KAPI_CONSTRAINT_RANGE"
+ // + "range: 0, 4096".
+ let doc = "\
+sys_c - Constraint test
+param: count
+ type: uint, input
+ constraint-type: range(0, 4096)
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_c", "syscall", None)
+ .unwrap();
+
+ let p = &spec.parameters[0];
+ assert_eq!(p.constraint_type, 1); // KAPI_CONSTRAINT_RANGE
+ assert_eq!(p.min_value, Some(0));
+ assert_eq!(p.max_value, Some(4096));
+ }
+
+ #[test]
+ fn parse_constraint_type_mask_expr() {
+ let doc = "\
+sys_c - Constraint test
+param: flags
+ type: uint, input
+ constraint-type: mask(0xff)
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_c", "syscall", None)
+ .unwrap();
+
+ let p = &spec.parameters[0];
+ assert_eq!(p.constraint_type, 2); // KAPI_CONSTRAINT_MASK
+ assert_eq!(p.valid_mask, Some(0xff));
+ }
+
+ #[test]
+ fn parse_constraint_type_enum_expr() {
+ let doc = "\
+sys_c - Constraint test
+param: mode
+ type: int, input
+ constraint-type: enum(0, 0x10, -3, MODE_FAST)
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_c", "syscall", None)
+ .unwrap();
+
+ let p = &spec.parameters[0];
+ assert_eq!(p.constraint_type, 3); // KAPI_CONSTRAINT_ENUM
+ assert_eq!(p.enum_values, ["0", "16", "-3", "MODE_FAST"]);
+ }
+
+ #[test]
+ fn c_integer_literals() {
+ assert_eq!(parse_u64_literal("0"), Some(0));
+ assert_eq!(parse_u64_literal("010"), Some(8));
+ assert_eq!(parse_u64_literal("0755"), Some(0o755));
+ assert_eq!(parse_u64_literal("0x1F"), Some(31));
+ assert_eq!(parse_u64_literal("0b101"), Some(5));
+ assert_eq!(parse_u64_literal("4096UL"), Some(4096));
+ assert_eq!(parse_u64_literal("0xffULL"), Some(255));
+ assert_eq!(parse_u64_literal("08"), None);
+ assert_eq!(parse_u64_literal("0x"), None);
+ assert_eq!(parse_u64_literal("+5"), None);
+ assert_eq!(parse_u64_literal("PAGE_SIZE"), None);
+ assert_eq!(parse_i64_literal("-010"), Some(-8));
+ assert_eq!(parse_i64_literal("-1U"), Some(4294967295));
+ assert_eq!(parse_i64_literal("-0x80000000"), Some(2147483648));
+ assert_eq!(parse_i64_literal("-2147483648"), Some(-2147483648));
+ assert_eq!(parse_i64_literal("-1UL"), Some(-1));
+ assert_eq!(parse_i64_literal("0xFFFFFFFFFFFFFFFF"), Some(-1));
+ assert_eq!(parse_i64_literal("18446744073709551615"), Some(-1));
+ assert_eq!(parse_i64_literal("-0x8000000000000000"), Some(i64::MIN));
+ assert_eq!(parse_u64_literal("-1"), Some(u64::MAX));
+ assert_eq!(parse_u64_literal("1uu"), None);
+ assert_eq!(parse_u64_literal("1lll"), None);
+ }
+
+ #[test]
+ fn parse_constraint_c_literals() {
+ let doc = "\
+sys_c - Constraint test
+param: mode
+ type: int, input
+ constraint-type: enum(010, 0x10, -07, 0b11, 5U)
+param: perms
+ type: uint, input
+ constraint-type: mask(0755)
+param: len
+ type: uint, input
+ constraint-type: range(01, 0x1000UL)
+param: align
+ type: uint, input
+ constraint-type: alignment(0x10)
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_c", "syscall", None)
+ .unwrap();
+
+ let p = &spec.parameters;
+ assert_eq!(p[0].enum_values, ["8", "16", "-7", "3", "5"]);
+ assert_eq!(p[1].valid_mask, Some(0o755));
+ assert_eq!((p[2].min_value, p[2].max_value), (Some(1), Some(4096)));
+ assert_eq!(p[3].alignment, Some(16));
+ }
+
+ #[test]
+ fn zero_user_string_limits_are_unset() {
+ let doc = "\
+sys_s - String test
+param: name
+ type: user_ptr, input
+ range: 0, 255
+ constraint-type: user_string
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_s", "syscall", None)
+ .unwrap();
+
+ let p = &spec.parameters[0];
+ assert_eq!(p.constraint_type, 8);
+ assert_eq!((p.min_value, p.max_value), (None, Some(255)));
+ }
+
+ #[test]
+ fn user_ptr_type_implies_user_flag() {
+ let doc = "\
+sys_u - Implicit user flag test
+param: buf
+ type: user_ptr, output
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_u", "syscall", None)
+ .unwrap();
+
+ let p = &spec.parameters[0];
+ assert_eq!(p.param_type, 10); // KAPI_TYPE_USER_PTR
+ assert_eq!(
+ p.flags,
+ (1 << 1) | (1 << 6), // OUT | USER
+ "user_ptr type must imply KAPI_PARAM_USER"
+ );
+ }
+
+ #[test]
+ fn fd_type_does_not_imply_user_flag() {
+ // Only user_ptr / path imply KAPI_PARAM_USER. fd, int, uint,
+ // and every other non-user-space type must leave flags alone.
+ let doc = "\
+sys_fd - fd has no implicit user flag
+param: fd
+ type: fd, input
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_fd", "syscall", None)
+ .unwrap();
+
+ let p = &spec.parameters[0];
+ assert_eq!(p.param_type, 9);
+ assert_eq!(p.flags, 1, "fd must not auto-set KAPI_PARAM_USER");
+ }
+
+ #[test]
+ fn path_type_implies_user_flag() {
+ let doc = "\
+sys_p - Path implicit user flag
+param: filename
+ type: path, input
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_p", "syscall", None)
+ .unwrap();
+
+ let p = &spec.parameters[0];
+ assert_eq!(p.param_type, 11);
+ assert_eq!(
+ p.flags,
+ 1 | (1 << 6), // IN | USER
+ "path type must imply KAPI_PARAM_USER"
+ );
+ }
+
+ #[test]
+ fn short_form_enum_equivalence() {
+ // Short-form and long-form renderings of the same spec must
+ // produce identical ApiSpec output across every enum family:
+ // context flags, param type+flags, constraint type, lock type,
+ // signal direction/action/timing, capability action, side-effect
+ // bitmask, return check type.
+ let long = "\
+sys_x - Enum short form test
+context-flags: KAPI_CTX_PROCESS | KAPI_CTX_SLEEPABLE
+
+param: fd
+ type: KAPI_TYPE_FD
+ flags: KAPI_PARAM_IN
+
+lock: files->file_lock
+ type: KAPI_LOCK_SPINLOCK
+ scope: acquires
+ desc: table lock
+
+signal: pending_signals
+ direction: KAPI_SIGNAL_RECEIVE
+ action: KAPI_SIGNAL_ACTION_RETURN
+ timing: KAPI_SIGNAL_TIME_DURING
+ desc: sig
+
+capability: CAP_SYS_ADMIN
+ type: KAPI_CAP_BYPASS_CHECK
+
+return:
+ type: KAPI_TYPE_INT
+ check-type: KAPI_RETURN_FD
+ desc: fd or errno
+
+side-effect: KAPI_EFFECT_RESOURCE_CREATE | KAPI_EFFECT_ALLOC_MEMORY
+ target: t
+ desc: d
+";
+ let short = "\
+sys_x - Enum short form test
+contexts: process, sleepable
+
+param: fd
+ type: fd, input
+
+lock: files->file_lock
+ type: spinlock
+ scope: acquires
+ desc: table lock
+
+signal: pending_signals
+ direction: receive
+ action: return
+ timing: during
+ desc: sig
+
+capability: CAP_SYS_ADMIN
+ type: bypass_check
+
+return:
+ type: int
+ check-type: fd
+ desc: fd or errno
+
+side-effect: resource_create | alloc_memory
+ target: t
+ desc: d
+";
+ let mut sp_l = parser()
+ .parse_kerneldoc(long, "sys_x", "syscall", None)
+ .unwrap();
+ let mut sp_s = parser()
+ .parse_kerneldoc(short, "sys_x", "syscall", None)
+ .unwrap();
+ // The return type name is the human spelling, kept as written.
+ for sp in [&mut sp_l, &mut sp_s] {
+ sp.return_spec.as_mut().unwrap().type_name.clear();
+ }
+ assert_eq!(
+ format!("{:#?}", sp_l),
+ format!("{:#?}", sp_s),
+ "long-form and short-form of every enum family must normalise identically"
+ );
+ }
+
+ #[test]
+ fn parse_buffer_short_captures_size_param_idx() {
+ let doc = "\
+sys_b - Buffer test
+param: buf
+ type: user_ptr, output, user
+ constraint-type: buffer(2)
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_b", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.parameters[0].constraint_type, 11);
+ assert_eq!(spec.parameters[0].size_param_idx, Some(2));
+ }
+
+ #[test]
+ fn buffer_short_and_size_param_long_are_symmetric() {
+ let short = "\
+sys_b - Symmetric buffer test
+param: buf
+ type: user_ptr, output, user
+ constraint-type: buffer(2)
+";
+ let long = "\
+sys_b - Symmetric buffer test
+param: buf
+ type: KAPI_TYPE_USER_PTR
+ flags: KAPI_PARAM_OUT | KAPI_PARAM_USER
+ constraint-type: KAPI_CONSTRAINT_BUFFER
+ size-param: 2
+";
+ let sp_s = parser()
+ .parse_kerneldoc(short, "sys_b", "syscall", None)
+ .unwrap();
+ let sp_l = parser()
+ .parse_kerneldoc(long, "sys_b", "syscall", None)
+ .unwrap();
+ assert_eq!(format!("{:#?}", sp_s), format!("{:#?}", sp_l));
+ }
+
+ #[test]
+ fn parse_constraint_type_bare_user_path() {
+ let doc = "\
+sys_c - Constraint test
+param: filename
+ type: path, input, user
+ constraint-type: user_path
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_c", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.parameters[0].constraint_type, 9); // USER_PATH
+ }
+
+ #[test]
+ fn starts_subfield_requires_known_key() {
+ let keys = block_subfield_keys(&BlockContext::SideEffect);
+ assert!(super::starts_subfield("target: file table", keys));
+ assert!(super::starts_subfield("condition:", keys));
+ assert!(super::starts_subfield("reversible: yes", keys));
+ // A colon inside prose does not open a subfield.
+ assert!(!super::starts_subfield(
+ "this lock mid-operation: the handler",
+ keys
+ ));
+ assert!(!super::starts_subfield("Careful: it is dangerous", keys));
+ // A key that belongs to another block type does not count.
+ assert!(!super::starts_subfield("direction: receive", keys));
+ assert!(!super::starts_subfield("O_RDONLY | O_WRONLY |", keys));
+ assert!(!super::starts_subfield(")", keys));
+ }
+
+ #[test]
+ fn multiline_fold_stops_at_sibling_block_attribute() {
+ // The constraint-type's continuation must not greedily eat the
+ // next subfield of the same block.
+ let doc = "\
+sys_y - Fold stop test
+param: f
+ type: int, input
+ constraint-type: mask(FOO |
+ BAR)
+ cdesc: something about f
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_y", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.parameters.len(), 1);
+ // If the fold over-consumed, `cdesc:` would have been swallowed
+ // into the mask expression and param.constraint_type would be 0.
+ assert_eq!(spec.parameters[0].constraint_type, 2);
+ assert_eq!(
+ spec.parameters[0].constraint.as_deref(),
+ Some("something about f")
+ );
+ }
+
+ #[test]
+ fn parse_constraint_type_mask_expr_multiline() {
+ // Real-world sys_open/flags case: a symbolic mask split across
+ // four continuation lines. The parser must fold the continuation
+ // lines before running the function-call match, otherwise the
+ // constraint type silently decays to 0.
+ let doc = "\
+sys_x - Multi-line mask test
+param: f
+ type: int, input
+ constraint-type: mask(O_RDONLY | O_WRONLY | O_RDWR | O_CREAT | O_EXCL | O_NOCTTY |
+ O_TRUNC | O_APPEND | O_NONBLOCK | O_DSYNC | O_SYNC | FASYNC |
+ O_DIRECT | O_LARGEFILE | O_DIRECTORY | O_NOFOLLOW | O_NOATIME |
+ O_CLOEXEC | O_PATH | O_TMPFILE)
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_x", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.parameters.len(), 1);
+ let p = &spec.parameters[0];
+ assert_eq!(p.constraint_type, 2, "multi-line mask must set MASK type");
+ // Symbolic mask: must stay unresolved rather than becoming Some(0).
+ assert_eq!(
+ p.valid_mask, None,
+ "symbolic mask values must remain None, not Some(0)"
+ );
+ }
+
+ #[test]
+ fn parse_constraint_long_form() {
+ let doc = "\
+sys_c - Constraint test
+param: foo
+ type: uint, input
+ constraint-type: KAPI_CONSTRAINT_MASK
+ valid-mask: 0xff
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_c", "syscall", None)
+ .unwrap();
+
+ let p = &spec.parameters[0];
+ assert_eq!(p.constraint_type, 2); // KAPI_CONSTRAINT_MASK
+ }
+
+ #[test]
+ fn parse_constraint_free_text() {
+ // `constraint:` carries free-text constraint description;
+ // function-call short form lives on `constraint-type:`.
+ let doc = "\
+sys_c - Constraint test
+param: foo
+ type: uint, input
+ constraint: must be a valid page descriptor
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_c", "syscall", None)
+ .unwrap();
+
+ let p = &spec.parameters[0];
+ assert_eq!(p.constraint_type, 0);
+ assert_eq!(
+ p.constraint.as_deref(),
+ Some("must be a valid page descriptor")
+ );
+ }
+
+ #[test]
+ fn parse_description_alias_overrides_kerneldoc() {
+ // `description:` inside a `param:` block is an alias for `desc:`
+ // and overrides the @param description.
+ let doc = "\
+sys_d - Description alias test
+@size: kerneldoc short description
+param: size
+ type: uint, input
+ description: The new long form description.
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_d", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.parameters.len(), 1);
+ assert_eq!(
+ spec.parameters[0].description,
+ "The new long form description."
+ );
+ }
+
+ #[test]
+ fn canonical_equivalence_short_vs_long() {
+ // Two spellings of the same spec must produce identical ApiSpec
+ // JSON.
+ let long = "\
+sys_open - open a file
+context-flags: KAPI_CTX_PROCESS | KAPI_CTX_SLEEPABLE
+
+param: filename
+ type: KAPI_TYPE_PATH
+ flags: KAPI_PARAM_IN | KAPI_PARAM_USER
+ constraint-type: KAPI_CONSTRAINT_USER_PATH
+ desc: Pathname to open
+
+param: count
+ type: KAPI_TYPE_UINT
+ flags: KAPI_PARAM_IN
+ constraint-type: KAPI_CONSTRAINT_RANGE
+ range: 0, 4096
+ desc: Byte count
+";
+ let short = "\
+sys_open - open a file
+contexts: process, sleepable
+
+param: filename
+ type: path, input, user
+ constraint-type: user_path
+ description: Pathname to open
+
+param: count
+ type: uint, input
+ constraint-type: range(0, 4096)
+ description: Byte count
+";
+ let long_spec = parser()
+ .parse_kerneldoc(long, "sys_open", "syscall", None)
+ .unwrap();
+ let short_spec = parser()
+ .parse_kerneldoc(short, "sys_open", "syscall", None)
+ .unwrap();
+
+ // ApiSpec isn't Serialize as a whole, so compare the Debug
+ // rendering, which still proves every field canonicalises
+ // identically.
+ let d_long = format!("{:#?}", long_spec);
+ let d_short = format!("{:#?}", short_spec);
+ assert_eq!(
+ d_long, d_short,
+ "short-form and long-form specs must normalise identically"
+ );
+ }
+
+ #[test]
+ fn parse_capability_block() {
+ let doc = "\
+sys_cap - Capability test
+capability: CAP_SYS_ADMIN
+ type: required
+ allows: Full system administration
+ without: Operation not permitted
+ condition: always
+ priority: 5
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_cap", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.capabilities.len(), 1);
+ let cap = &spec.capabilities[0];
+ assert_eq!(cap.capability, 21); // CAP_SYS_ADMIN
+ assert_eq!(cap.action, "required");
+ assert_eq!(cap.allows, "Full system administration");
+ assert_eq!(cap.without_cap, "Operation not permitted");
+ assert_eq!(cap.check_condition.as_deref(), Some("always"));
+ assert_eq!(cap.priority, Some(5));
+ }
+
+ #[test]
+ fn parse_lock_block() {
+ let doc = "\
+sys_lock - Lock test
+lock: files_lock, KAPI_LOCK_MUTEX
+ scope: acquires
+ desc: Protects file table
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_lock", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.locks.len(), 1);
+ let lock = &spec.locks[0];
+ assert_eq!(lock.lock_name, "files_lock");
+ assert_eq!(lock.lock_type, 1); // MUTEX
+ assert_eq!(lock.scope, super::super::KAPI_LOCK_ACQUIRES);
+ assert_eq!(lock.description, "Protects file table");
+ }
+
+ #[test]
+ fn parse_lock_acquired_released_flags() {
+ let doc = "\
+sys_lock - Lock test
+lock: a_lock, KAPI_LOCK_MUTEX
+ acquired: true
+ released: true
+lock: b_lock, KAPI_LOCK_MUTEX
+ acquired: true
+lock: c_lock, KAPI_LOCK_MUTEX
+ released: yes
+lock: d_lock, KAPI_LOCK_MUTEX
+ acquired: conditional
+ released: false
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_lock", "syscall", None)
+ .unwrap();
+
+ let scopes: Vec<u32> = spec.locks.iter().map(|l| l.scope).collect();
+ assert_eq!(
+ scopes,
+ vec![
+ super::super::KAPI_LOCK_INTERNAL,
+ super::super::KAPI_LOCK_ACQUIRES,
+ super::super::KAPI_LOCK_RELEASES,
+ super::super::KAPI_LOCK_INTERNAL,
+ ]
+ );
+ }
+
+ #[test]
+ fn parse_signal_block() {
+ let doc = "\
+sys_sig - Signal test
+signal: SIGKILL
+ direction: KAPI_SIGNAL_RECEIVE
+ action: KAPI_SIGNAL_ACTION_TERMINATE
+ timing: KAPI_SIGNAL_TIME_DURING
+ priority: 3
+ restartable: yes
+ interruptible: yes
+ desc: Process termination signal
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_sig", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.signals.len(), 1);
+ let sig = &spec.signals[0];
+ assert_eq!(sig.signal_name, "SIGKILL");
+ assert_eq!(sig.direction, 1); // RECEIVE
+ assert_eq!(sig.action, 1); // TERMINATE
+ assert_eq!(sig.timing, 1); // DURING
+ assert_eq!(sig.priority, 3);
+ assert!(sig.restartable);
+ assert!(sig.interruptible);
+ assert_eq!(
+ sig.description.as_deref(),
+ Some("Process termination signal")
+ );
+ }
+
+ #[test]
+ fn parse_signal_errno_shapes() {
+ // All three accepted spellings of the signal errno field must
+ // produce the same negative kernel return code.
+ for (form, label) in [
+ ("errno: -EINTR", "-EINTR symbolic"),
+ ("errno: EINTR", "bare symbolic"),
+ ("errno: -4", "numeric literal"),
+ ] {
+ let doc = format!(
+ "sys_s - Signal errno test\n\
+ signal: SIGINT\n\
+ \x20 direction: receive\n\
+ \x20 action: return\n\
+ \x20 {}\n",
+ form,
+ );
+ let spec = parser()
+ .parse_kerneldoc(&doc, "sys_s", "syscall", None)
+ .unwrap();
+ assert_eq!(spec.signals.len(), 1, "{label}");
+ assert_eq!(
+ spec.signals[0].error_on_signal,
+ Some(-4),
+ "errno form {label:?} must resolve to -EINTR (-4)",
+ );
+ }
+ }
+
+ #[test]
+ fn parse_side_effect_flat() {
+ let doc = "\
+sys_se - Side effect test
+side-effect: KAPI_EFFECT_MODIFY_STATE, file_table, Allocates a new file descriptor
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_se", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.side_effects.len(), 1);
+ let se = &spec.side_effects[0];
+ assert_eq!(se.effect_type, 1 << 2); // KAPI_EFFECT_MODIFY_STATE
+ assert_eq!(se.target, "file_table");
+ assert_eq!(se.description, "Allocates a new file descriptor");
+ }
+
+ #[test]
+ fn parse_side_effect_block() {
+ let doc = "\
+sys_se2 - Side effect block test
+side-effect: KAPI_EFFECT_ALLOC_MEMORY
+ target: kernel_heap
+ desc: Allocates kernel memory
+ reversible: yes
+ condition: size > 0
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_se2", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.side_effects.len(), 1);
+ let se = &spec.side_effects[0];
+ assert_eq!(se.effect_type, 1 << 0); // KAPI_EFFECT_ALLOC_MEMORY
+ assert_eq!(se.target, "kernel_heap");
+ assert_eq!(se.description, "Allocates kernel memory");
+ assert!(se.reversible);
+ assert_eq!(se.condition.as_deref(), Some("size > 0"));
+ }
+
+ #[test]
+ fn parse_empty_doc_no_error() {
+ let doc = "";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_empty", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.name, "sys_empty");
+ assert!(spec.description.is_none());
+ assert!(spec.parameters.is_empty());
+ assert!(spec.errors.is_empty());
+ assert!(spec.signals.is_empty());
+ assert!(spec.capabilities.is_empty());
+ assert!(spec.locks.is_empty());
+ assert!(spec.side_effects.is_empty());
+ assert!(spec.context_flags.is_empty());
+ }
+
+ #[test]
+ fn parse_missing_sections_no_error() {
+ // Only has a description, no KAPI annotations
+ let doc = "\
+sys_simple - Just a simple syscall
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_simple", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.description.as_deref(), Some("Just a simple syscall"));
+ assert!(spec.parameters.is_empty());
+ assert!(spec.errors.is_empty());
+ assert!(spec.context_flags.is_empty());
+ }
+
+ #[test]
+ fn parse_constraint_block() {
+ let doc = "\
+sys_cst - Constraint test
+constraint: valid_fd
+ desc: File descriptor must be valid and open
+ expr: fd >= 0 && fd < NR_OPEN
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_cst", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.constraints.len(), 1);
+ let cst = &spec.constraints[0];
+ assert_eq!(cst.name, "valid_fd");
+ assert_eq!(cst.description, "File descriptor must be valid and open");
+ assert_eq!(cst.expression.as_deref(), Some("fd >= 0 && fd < NR_OPEN"));
+ }
+
+ #[test]
+ fn parse_state_transition_flat() {
+ let doc = "\
+sys_st - State transition test
+state-trans: fd, open, closed, File descriptor is closed
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_st", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.state_transitions.len(), 1);
+ let st = &spec.state_transitions[0];
+ assert_eq!(st.object, "fd");
+ assert_eq!(st.from_state, "open");
+ assert_eq!(st.to_state, "closed");
+ assert_eq!(st.description, "File descriptor is closed");
+ }
+
+ #[test]
+ fn parse_param_block_with_range() {
+ let doc = "\
+sys_rng - Range test
+@count: byte count
+param: count
+ type: KAPI_TYPE_UINT
+ flags: IN
+ range: 0, 4096
+ constraint-type: KAPI_CONSTRAINT_RANGE
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_rng", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.parameters.len(), 1);
+ let p = &spec.parameters[0];
+ assert_eq!(p.name, "count");
+ assert_eq!(p.param_type, 2); // UINT
+ assert_eq!(p.flags, 1); // IN
+ assert_eq!(p.min_value, Some(0));
+ assert_eq!(p.max_value, Some(4096));
+ assert_eq!(p.constraint_type, 1); // RANGE
+ }
+
+ #[test]
+ fn parse_return_block() {
+ let doc = "\
+sys_ret - Return test
+return:
+ type: KAPI_TYPE_INT
+ check-type: KAPI_RETURN_FD
+ success: 0
+ desc: Returns file descriptor on success
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_ret", "syscall", None)
+ .unwrap();
+
+ let ret = spec.return_spec.as_ref().unwrap();
+ assert_eq!(ret.type_name, "KAPI_TYPE_INT");
+ assert_eq!(ret.return_type, 1); // INT
+ assert_eq!(ret.check_type, 3); // FD
+ assert_eq!(ret.success_value, None); // FD checks carry no success value
+ assert_eq!(ret.description, "Returns file descriptor on success");
+ }
+
+ #[test]
+ fn colon_in_continuation_line_does_not_start_subfield() {
+ let doc = "\
+sys_col - Colon continuation test
+param: x
+ type: int
+ cdesc: Must be sane. Note: this is a continuation
+ with a colon: in the middle.
+
+lock: mylock
+ type: mutex
+ desc: Taken at entry; if the caller drops
+ this lock mid-operation: the handler waits
+ until released.
+
+side-effect: modify_state
+ target: stuff
+ condition: only when a
+ special case applies: for example
+ on Tuesdays
+ desc: Does things.
+ Careful: it is dangerous.
+ reversible: no
+
+signal: SIGINT
+ direction: receive
+ condition: while blocked, unless
+ flagged: see below
+ desc: Interrupts the wait.
+
+capability: CAP_SYS_ADMIN
+ type: bypass_check
+ allows: Things
+ that need: privilege
+ condition: Always
+
+constraint: limit
+ desc: A limit
+ of note: nothing.
+ expr: a &&
+ b
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_col", "syscall", None)
+ .unwrap();
+
+ assert_eq!(
+ spec.parameters[0].constraint.as_deref(),
+ Some("Must be sane. Note: this is a continuation with a colon: in the middle.")
+ );
+ assert_eq!(
+ spec.locks[0].description,
+ "Taken at entry; if the caller drops this lock mid-operation: the handler waits until released."
+ );
+ let se = &spec.side_effects[0];
+ assert_eq!(
+ se.condition.as_deref(),
+ Some("only when a special case applies: for example on Tuesdays")
+ );
+ assert_eq!(se.description, "Does things. Careful: it is dangerous.");
+ assert!(!se.reversible);
+ let sig = &spec.signals[0];
+ assert_eq!(
+ sig.condition.as_deref(),
+ Some("while blocked, unless flagged: see below")
+ );
+ assert_eq!(sig.description.as_deref(), Some("Interrupts the wait."));
+ let cap = &spec.capabilities[0];
+ assert_eq!(cap.allows, "Things that need: privilege");
+ assert_eq!(cap.check_condition.as_deref(), Some("Always"));
+ let cst = &spec.constraints[0];
+ assert_eq!(cst.description, "A limit of note: nothing.");
+ assert_eq!(cst.expression.as_deref(), Some("a && b"));
+ }
+
+ #[test]
+ fn multiline_side_effect_condition_is_kept_whole() {
+ let doc = "\
+sys_mc - Multi-line condition test
+side-effect: modify_state
+ target: userfaultfd event queue
+ condition: MADV_DONTNEED, MADV_FREE
+ on a VMA whose userfaultfd context negotiated
+ UFFD_FEATURE_EVENT_REMOVE
+ desc: Generates a notification.
+ The monitor cannot veto it.
+ reversible: no
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_mc", "syscall", None)
+ .unwrap();
+
+ let se = &spec.side_effects[0];
+ assert_eq!(
+ se.condition.as_deref(),
+ Some(
+ "MADV_DONTNEED, MADV_FREE on a VMA whose userfaultfd context \
+ negotiated UFFD_FEATURE_EVENT_REMOVE"
+ )
+ );
+ assert_eq!(
+ se.description,
+ "Generates a notification. The monitor cannot veto it."
+ );
+ }
+
+ #[test]
+ fn blank_line_inside_block_continues_subfield() {
+ let doc = "\
+sys_bl - Blank line test
+lock: l
+ type: mutex
+ desc: First half
+
+ second half.
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_bl", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.locks[0].description, "First half second half.");
+ }
+
+ #[test]
+ fn param_cdesc_is_the_constraint_text() {
+ let doc = "\
+sys_cd - cdesc test
+param: fd
+ type: int, input
+ cdesc: Must be open.
+ Zero is allowed.
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_cd", "syscall", None)
+ .unwrap();
+
+ assert_eq!(
+ spec.parameters[0].constraint.as_deref(),
+ Some("Must be open. Zero is allowed.")
+ );
+ }
+
+ #[test]
+ fn state_transition_block_keeps_condition_separate() {
+ let doc = "\
+sys_stb - State transition block test
+state-trans: file_descriptor
+ from: open
+ to: closed/free
+ condition: Valid fd passed to close
+ desc: The fd becomes unusable.
+ It may be reused.
+
+state-trans: refcount
+ from: n
+ to: n-1
+ desc: Decremented.
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_stb", "syscall", None)
+ .unwrap();
+
+ assert_eq!(spec.state_transitions.len(), 2);
+ let st = &spec.state_transitions[0];
+ assert_eq!(st.object, "file_descriptor");
+ assert_eq!(st.from_state, "open");
+ assert_eq!(st.to_state, "closed/free");
+ assert_eq!(st.condition.as_deref(), Some("Valid fd passed to close"));
+ assert_eq!(st.description, "The fd becomes unusable. It may be reused.");
+ let st = &spec.state_transitions[1];
+ assert_eq!(st.object, "refcount");
+ assert_eq!(st.condition, None);
+ assert_eq!(st.description, "Decremented.");
+ }
+
+ #[test]
+ fn state_transition_flat_description_keeps_commas() {
+ let doc = "\
+sys_stc - Flat state transition test
+state-trans: fd, open, closed, File is closed, and the number is free
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_stc", "syscall", None)
+ .unwrap();
+
+ let st = &spec.state_transitions[0];
+ assert_eq!(st.description, "File is closed, and the number is free");
+ assert_eq!(st.condition, None);
+ }
+
+ #[test]
+ fn return_type_name_is_kept_as_written() {
+ for (written, ty) in [("int", 1), ("long", 0), ("KAPI_TYPE_UINT", 2)] {
+ let doc = format!("sys_rt - Return type test\nreturn:\n type: {written}\n");
+ let spec = parser()
+ .parse_kerneldoc(&doc, "sys_rt", "syscall", None)
+ .unwrap();
+ let ret = spec.return_spec.as_ref().unwrap();
+ assert_eq!(ret.type_name, written);
+ assert_eq!(ret.return_type, ty);
+ }
+ }
+
+ #[test]
+ fn return_success_follows_check_type() {
+ let cases = [
+ // (check-type, success, value, min, max)
+ ("exact", "0", Some(0), None, None),
+ ("exact", "= 0", Some(0), None, None),
+ ("exact", "== -1", Some(-1), None, None),
+ ("exact", "0x10", Some(16), None, None),
+ ("exact", ">= 0", None, None, None),
+ ("range", ">= 0", None, Some(0), Some(i64::MAX)),
+ ("range", ">= 1", None, Some(1), Some(i64::MAX)),
+ ("range", "0", None, Some(0), Some(i64::MAX)),
+ ("fd", ">= 0", None, None, None),
+ ("error_check", "0", None, None, None),
+ ];
+ for (check, success, value, min, max) in cases {
+ let doc = format!(
+ "sys_rs - Return success test\nreturn:\n type: int\n \
+ success: {success}\n check-type: {check}\n desc: ok\n"
+ );
+ let spec = parser()
+ .parse_kerneldoc(&doc, "sys_rs", "syscall", None)
+ .unwrap();
+ let ret = spec.return_spec.as_ref().unwrap();
+ assert_eq!(ret.success_value, value, "{check} {success}");
+ assert_eq!(ret.success_min, min, "{check} {success}");
+ assert_eq!(ret.success_max, max, "{check} {success}");
+ }
+ }
+
+ #[test]
+ fn return_description_continuation_with_colon() {
+ let doc = "\
+sys_rd - Return description test
+return:
+ type: int
+ check-type: exact
+ success: 0
+ desc: Returns zero on success. Note: this is
+ a continuation line with a colon.
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_rd", "syscall", None)
+ .unwrap();
+
+ let ret = spec.return_spec.as_ref().unwrap();
+ assert_eq!(
+ ret.description,
+ "Returns zero on success. Note: this is a continuation line with a colon."
+ );
+ assert_eq!(ret.success_value, Some(0));
+ }
+
+ #[test]
+ fn examples_keep_one_example_per_line() {
+ let doc = "\
+sys_ex - Examples test
+examples: fd = open(\"/etc/passwd\", O_RDONLY); // Read existing file
+ fd = open(\"/tmp/new\", O_WRONLY | O_CREAT, 0644); // Create
+ // Handle short writes:
+ while (total < len) {
+ n = write(fd, buf + total, len - total);
+ if (n < 0) break;
+ }
+
+ close(fd);
+
+notes: After the examples.
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_ex", "syscall", None)
+ .unwrap();
+
+ assert_eq!(
+ spec.examples.as_deref(),
+ Some(
+ "fd = open(\"/etc/passwd\", O_RDONLY); // Read existing file\n\
+ fd = open(\"/tmp/new\", O_WRONLY | O_CREAT, 0644); // Create\n\
+ // Handle short writes:\n\
+ while (total < len) {\n \
+ n = write(fd, buf + total, len - total);\n \
+ if (n < 0) break;\n\
+ }\n\
+ \n\
+ close(fd);"
+ )
+ );
+ assert_eq!(spec.notes.as_deref(), Some("After the examples."));
+ }
+
+ #[test]
+ fn notes_and_long_desc_keep_paragraphs_and_bullets() {
+ let doc = "\
+sys_nt - Notes test
+long-desc: First paragraph wraps
+ over two lines.
+
+ Second paragraph introduces a list:
+ - item one wraps
+ onto a second line
+ - item two
+
+ Last paragraph.
+notes: The behavior varies:
+
+ - Regular files: reads
+ from the current position.
+
+ - Pipes: block.
+
+ Trailing: text.
+";
+ let spec = parser()
+ .parse_kerneldoc(doc, "sys_nt", "syscall", None)
+ .unwrap();
+
+ assert_eq!(
+ spec.long_description.as_deref(),
+ Some(
+ "First paragraph wraps over two lines.\n\n\
+ Second paragraph introduces a list:\n\
+ - item one wraps onto a second line\n\
+ - item two\n\n\
+ Last paragraph."
+ )
+ );
+ assert_eq!(
+ spec.notes.as_deref(),
+ Some(
+ "The behavior varies:\n\n\
+ - Regular files: reads from the current position.\n\n\
+ - Pipes: block.\n\n\
+ Trailing: text."
+ )
+ );
+ }
+
+ #[test]
+ fn fold_lines_handles_leading_blank_and_tabs() {
+ assert_eq!(
+ super::fold_lines(&["", " a();", " b();", " c();", ""]),
+ "a();\nb();\n c();"
+ );
+ assert_eq!(super::fold_lines(&["x();", "\ty();"]), "x();\ny();");
+ assert_eq!(super::fold_lines(&[""]), "");
+ assert_eq!(super::fold_lines(&["a", " b", "", "", " c"]), "a\nb\n\nc");
+ }
+
+ #[test]
+ fn fold_paragraphs_joins_wrapped_lines_only() {
+ assert_eq!(
+ super::fold_paragraphs(&["one", " two", "", "", " three", "- four", "- five"]),
+ "one two\n\nthree\n- four\n- five"
+ );
+ assert_eq!(super::fold_paragraphs(&["", " "]), "");
+ }
+}
--git a/tools/kapi/src/extractor/mod.rs b/tools/kapi/src/extractor/mod.rs
new file mode 100644
index 0000000000000..d7ada57924d7a
--- /dev/null
+++ b/tools/kapi/src/extractor/mod.rs
@@ -0,0 +1,442 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+
+use crate::formatter::OutputFormatter;
+use anyhow::Result;
+use std::io::Write;
+
+pub mod debugfs;
+pub mod kerneldoc_parser;
+pub mod source_parser;
+pub mod vmlinux;
+
+pub use debugfs::DebugfsExtractor;
+pub use source_parser::SourceExtractor;
+pub use vmlinux::VmlinuxExtractor;
+
+/// Capability specification
+#[derive(Debug, Clone, serde::Serialize)]
+pub struct CapabilitySpec {
+ pub capability: i32,
+ pub name: String,
+ pub action: String,
+ pub allows: String,
+ pub without_cap: String,
+ pub check_condition: Option<String>,
+ pub priority: Option<u8>,
+ pub alternatives: Vec<i32>,
+}
+
+/// Parameter specification
+#[derive(Debug, Clone, serde::Serialize)]
+pub struct ParamSpec {
+ pub index: u32,
+ pub name: String,
+ pub type_name: String,
+ pub description: String,
+ pub flags: u32,
+ pub param_type: u32,
+ pub constraint_type: u32,
+ pub constraint: Option<String>,
+ pub min_value: Option<i64>,
+ pub max_value: Option<i64>,
+ pub valid_mask: Option<u64>,
+ pub enum_values: Vec<String>,
+ pub size: Option<u32>,
+ pub alignment: Option<u32>,
+ /// Index of the parameter that carries this parameter's byte count
+ /// (for KAPI_CONSTRAINT_BUFFER). Populated by either
+ /// `size-param: N` (long form) or `constraint-type: buffer(N)`
+ /// (short form).
+ pub size_param_idx: Option<u32>,
+}
+
+/// Constraint type enum values matching kernel enum kapi_constraint_type
+pub const KAPI_CONSTRAINT_RANGE: u32 = 1;
+pub const KAPI_CONSTRAINT_MASK: u32 = 2;
+pub const KAPI_CONSTRAINT_USER_STRING: u32 = 8;
+
+impl ParamSpec {
+ /// Clear the numeric fields the constraint type does not use. The
+ /// compiled struct always carries them, so an unset value reads as 0.
+ pub fn keep_used_numbers(&mut self) {
+ match self.constraint_type {
+ KAPI_CONSTRAINT_RANGE => {}
+ KAPI_CONSTRAINT_USER_STRING => self.drop_unset_string_limits(),
+ _ => {
+ self.min_value = None;
+ self.max_value = None;
+ }
+ }
+ if self.constraint_type != KAPI_CONSTRAINT_MASK {
+ self.valid_mask = None;
+ }
+ self.size = self.size.filter(|&n| n != 0);
+ self.alignment = self.alignment.filter(|&n| n != 0);
+ }
+
+ /// The kernel treats a user_string length limit of 0 as no limit.
+ pub fn drop_unset_string_limits(&mut self) {
+ if self.constraint_type == KAPI_CONSTRAINT_USER_STRING {
+ self.min_value = self.min_value.filter(|&n| n > 0);
+ self.max_value = self.max_value.filter(|&n| n > 0);
+ }
+ }
+}
+
+/// Return value specification
+#[derive(Debug, Clone, serde::Serialize)]
+pub struct ReturnSpec {
+ pub type_name: String,
+ pub description: String,
+ pub return_type: u32,
+ pub check_type: u32,
+ pub success_value: Option<i64>,
+ pub success_min: Option<i64>,
+ pub success_max: Option<i64>,
+ pub error_values: Vec<i32>,
+}
+
+impl ReturnSpec {
+ /// Drop the success fields the check type does not use, the way the
+ /// generated KAPI_RETURN_SUCCESS()/KAPI_RETURN_SUCCESS_RANGE() macros
+ /// only set the one that applies.
+ pub fn keep_used_success_fields(&mut self) {
+ match self.check_type {
+ 0 => {
+ self.success_min = None;
+ self.success_max = None;
+ }
+ 1 => self.success_value = None,
+ _ => {
+ self.success_value = None;
+ self.success_min = None;
+ self.success_max = None;
+ }
+ }
+ }
+}
+
+/// Error specification
+#[derive(Debug, Clone, serde::Serialize)]
+pub struct ErrorSpec {
+ pub error_code: i32,
+ pub name: String,
+ pub condition: String,
+ pub description: String,
+}
+
+/// Signal specification
+#[derive(Debug, Clone, serde::Serialize)]
+pub struct SignalSpec {
+ pub signal_num: i32,
+ pub signal_name: String,
+ pub direction: u32,
+ pub action: u32,
+ pub target: Option<String>,
+ pub condition: Option<String>,
+ pub description: Option<String>,
+ pub timing: u32,
+ pub priority: u32,
+ pub restartable: bool,
+ pub interruptible: bool,
+ pub queue: Option<String>,
+ pub sa_flags: u32,
+ pub sa_flags_required: u32,
+ pub sa_flags_forbidden: u32,
+ pub state_required: u32,
+ pub state_forbidden: u32,
+ pub error_on_signal: Option<i32>,
+ /// Signal number to transform to (e.g. `SIGKILL` -> 9 on x86).
+ /// Always an integer or null in JSON -- the schema never widens to
+ /// a string. Extractors reading the compiled struct (`--vmlinux`,
+ /// `--debugfs`) populate this directly. The source-kerneldoc parser
+ /// populates it only when the `transform-to:` subfield is a numeric
+ /// literal; symbolic signal names are arch-dependent and cannot be
+ /// resolved portably in userspace, so they are reported via an
+ /// stderr warning and leave this field `None`. Consumers that need
+ /// the resolved number for a symbolic spec should use `--vmlinux`
+ /// or `--debugfs` against a kernel built for the target arch.
+ pub transform_to: Option<i32>,
+}
+
+/// Signal mask specification
+#[derive(Debug, Clone, serde::Serialize)]
+pub struct SignalMaskSpec {
+ pub name: String,
+ pub description: String,
+ pub signals: Vec<i32>,
+}
+
+/// Side effect specification
+#[derive(Debug, Clone, serde::Serialize)]
+pub struct SideEffectSpec {
+ pub effect_type: u32,
+ pub target: String,
+ pub condition: Option<String>,
+ pub description: String,
+ pub reversible: bool,
+}
+
+/// State transition specification
+#[derive(Debug, Clone, serde::Serialize)]
+pub struct StateTransitionSpec {
+ pub object: String,
+ pub from_state: String,
+ pub to_state: String,
+ pub condition: Option<String>,
+ pub description: String,
+}
+
+/// Constraint specification
+#[derive(Debug, Clone, serde::Serialize)]
+pub struct ConstraintSpec {
+ pub name: String,
+ pub description: String,
+ pub expression: Option<String>,
+}
+
+/// Lock scope enum values matching kernel enum kapi_lock_scope
+pub const KAPI_LOCK_INTERNAL: u32 = 0;
+pub const KAPI_LOCK_ACQUIRES: u32 = 1;
+pub const KAPI_LOCK_RELEASES: u32 = 2;
+pub const KAPI_LOCK_CALLER_HELD: u32 = 3;
+
+/// Lock specification
+#[derive(Debug, Clone, serde::Serialize)]
+pub struct LockSpec {
+ pub lock_name: String,
+ pub lock_type: u32,
+ pub scope: u32,
+ pub description: String,
+}
+
+/// Struct field specification
+#[derive(Debug, Clone, serde::Serialize)]
+pub struct StructFieldSpec {
+ pub name: String,
+ pub field_type: u32,
+ pub type_name: String,
+ pub offset: usize,
+ pub size: usize,
+ pub flags: u32,
+ pub constraint_type: u32,
+ pub min_value: i64,
+ pub max_value: i64,
+ pub valid_mask: u64,
+ pub description: String,
+}
+
+/// Struct specification
+#[derive(Debug, Clone, serde::Serialize)]
+pub struct StructSpec {
+ pub name: String,
+ pub size: usize,
+ pub alignment: usize,
+ pub field_count: u32,
+ pub fields: Vec<StructFieldSpec>,
+ pub description: String,
+}
+
+/// Common API specification information that all extractors should provide
+#[derive(Debug, Clone, Default)]
+pub struct ApiSpec {
+ pub name: String,
+ pub api_type: String,
+ pub description: Option<String>,
+ pub long_description: Option<String>,
+ pub version: Option<String>,
+ pub context_flags: Vec<String>,
+ pub param_count: Option<u32>,
+ pub error_count: Option<u32>,
+ pub examples: Option<String>,
+ pub notes: Option<String>,
+ // Sysfs-specific fields
+ pub subsystem: Option<String>,
+ pub sysfs_path: Option<String>,
+ pub permissions: Option<String>,
+ pub capabilities: Vec<CapabilitySpec>,
+ pub parameters: Vec<ParamSpec>,
+ pub return_spec: Option<ReturnSpec>,
+ pub errors: Vec<ErrorSpec>,
+ pub signals: Vec<SignalSpec>,
+ pub signal_masks: Vec<SignalMaskSpec>,
+ pub side_effects: Vec<SideEffectSpec>,
+ pub state_transitions: Vec<StateTransitionSpec>,
+ pub constraints: Vec<ConstraintSpec>,
+ pub locks: Vec<LockSpec>,
+ pub struct_specs: Vec<StructSpec>,
+}
+
+/// Trait for extracting API specifications from different sources
+pub trait ApiExtractor {
+ /// Extract all API specifications from the source
+ fn extract_all(&self) -> Result<Vec<ApiSpec>>;
+
+ /// Extract a specific API specification by name
+ fn extract_by_name(&self, name: &str) -> Result<Option<ApiSpec>>;
+
+ /// Display detailed information about a specific API
+ fn display_api_details(
+ &self,
+ api_name: &str,
+ formatter: &mut dyn OutputFormatter,
+ writer: &mut dyn Write,
+ ) -> Result<()>;
+}
+
+/// Helper function to display an ApiSpec using a formatter
+pub fn display_api_spec(
+ spec: &ApiSpec,
+ formatter: &mut dyn OutputFormatter,
+ writer: &mut dyn Write,
+) -> Result<()> {
+ formatter.begin_api_details(writer, &spec.name)?;
+
+ if let Some(desc) = &spec.description {
+ formatter.description(writer, desc)?;
+ }
+
+ if let Some(long_desc) = &spec.long_description {
+ formatter.long_description(writer, long_desc)?;
+ }
+
+ if !spec.context_flags.is_empty() {
+ formatter.begin_context_flags(writer)?;
+ for flag in &spec.context_flags {
+ formatter.context_flag(writer, flag)?;
+ }
+ formatter.end_context_flags(writer)?;
+ }
+
+ if !spec.parameters.is_empty() {
+ formatter.begin_parameters(writer, spec.parameters.len().try_into().unwrap_or(u32::MAX))?;
+ for param in &spec.parameters {
+ formatter.parameter(writer, param)?;
+ }
+ formatter.end_parameters(writer)?;
+ }
+
+ if let Some(ret) = &spec.return_spec {
+ formatter.return_spec(writer, ret)?;
+ }
+
+ if !spec.errors.is_empty() {
+ formatter.begin_errors(writer, spec.errors.len().try_into().unwrap_or(u32::MAX))?;
+ for error in &spec.errors {
+ formatter.error(writer, error)?;
+ }
+ formatter.end_errors(writer)?;
+ }
+
+ if let Some(notes) = &spec.notes {
+ formatter.notes(writer, notes)?;
+ }
+
+ if let Some(examples) = &spec.examples {
+ formatter.examples(writer, examples)?;
+ }
+
+ // Display sysfs-specific fields
+ if spec.api_type == "sysfs" {
+ if let Some(subsystem) = &spec.subsystem {
+ formatter.sysfs_subsystem(writer, subsystem)?;
+ }
+ if let Some(path) = &spec.sysfs_path {
+ formatter.sysfs_path(writer, path)?;
+ }
+ if let Some(perms) = &spec.permissions {
+ formatter.sysfs_permissions(writer, perms)?;
+ }
+ }
+
+ if !spec.capabilities.is_empty() {
+ formatter.begin_capabilities(writer)?;
+ for cap in &spec.capabilities {
+ formatter.capability(writer, cap)?;
+ }
+ formatter.end_capabilities(writer)?;
+ }
+
+ // Display signals
+ if !spec.signals.is_empty() {
+ formatter.begin_signals(writer, spec.signals.len().try_into().unwrap_or(u32::MAX))?;
+ for signal in &spec.signals {
+ formatter.signal(writer, signal)?;
+ }
+ formatter.end_signals(writer)?;
+ }
+
+ // Display signal masks
+ if !spec.signal_masks.is_empty() {
+ formatter.begin_signal_masks(
+ writer,
+ spec.signal_masks.len().try_into().unwrap_or(u32::MAX),
+ )?;
+ for mask in &spec.signal_masks {
+ formatter.signal_mask(writer, mask)?;
+ }
+ formatter.end_signal_masks(writer)?;
+ }
+
+ // Display side effects
+ if !spec.side_effects.is_empty() {
+ formatter.begin_side_effects(
+ writer,
+ spec.side_effects.len().try_into().unwrap_or(u32::MAX),
+ )?;
+ for effect in &spec.side_effects {
+ formatter.side_effect(writer, effect)?;
+ }
+ formatter.end_side_effects(writer)?;
+ }
+
+ // Display state transitions
+ if !spec.state_transitions.is_empty() {
+ formatter.begin_state_transitions(
+ writer,
+ spec.state_transitions.len().try_into().unwrap_or(u32::MAX),
+ )?;
+ for trans in &spec.state_transitions {
+ formatter.state_transition(writer, trans)?;
+ }
+ formatter.end_state_transitions(writer)?;
+ }
+
+ // Display constraints
+ if !spec.constraints.is_empty() {
+ formatter.begin_constraints(
+ writer,
+ spec.constraints.len().try_into().unwrap_or(u32::MAX),
+ )?;
+ for constraint in &spec.constraints {
+ formatter.constraint(writer, constraint)?;
+ }
+ formatter.end_constraints(writer)?;
+ }
+
+ // Display locks
+ if !spec.locks.is_empty() {
+ formatter.begin_locks(writer, spec.locks.len().try_into().unwrap_or(u32::MAX))?;
+ for lock in &spec.locks {
+ formatter.lock(writer, lock)?;
+ }
+ formatter.end_locks(writer)?;
+ }
+
+ // Display struct specs
+ if !spec.struct_specs.is_empty() {
+ formatter.begin_struct_specs(
+ writer,
+ spec.struct_specs.len().try_into().unwrap_or(u32::MAX),
+ )?;
+ for struct_spec in &spec.struct_specs {
+ formatter.struct_spec(writer, struct_spec)?;
+ }
+ formatter.end_struct_specs(writer)?;
+ }
+
+ formatter.end_api_details(writer)?;
+
+ Ok(())
+}
--git a/tools/kapi/src/extractor/source_parser.rs b/tools/kapi/src/extractor/source_parser.rs
new file mode 100644
index 0000000000000..cc50e15f16c83
--- /dev/null
+++ b/tools/kapi/src/extractor/source_parser.rs
@@ -0,0 +1,531 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+
+use super::kerneldoc_parser::KerneldocParser;
+use super::{display_api_spec, ApiExtractor, ApiSpec};
+use crate::formatter::OutputFormatter;
+use anyhow::{bail, Context, Result};
+use regex::Regex;
+use std::fs;
+use std::io::Write;
+use std::path::Path;
+use walkdir::WalkDir;
+
+/// Extractor for kernel source files with KAPI-annotated kerneldoc
+pub struct SourceExtractor {
+ path: String,
+ parser: KerneldocParser,
+ syscall_regex: Regex,
+ ioctl_regex: Regex,
+ function_regex: Regex,
+}
+
+impl SourceExtractor {
+ pub fn new(path: &str) -> Result<Self> {
+ if !Path::new(path).exists() {
+ bail!("Source path does not exist: {path}");
+ }
+
+ Ok(SourceExtractor {
+ path: path.to_string(),
+ parser: KerneldocParser::new(),
+ syscall_regex: Regex::new(r"SYSCALL_DEFINE\d+\((\w+)")?,
+ ioctl_regex: Regex::new(r"(?:static\s+)?long\s+(\w+_ioctl)\s*\(")?,
+ function_regex: Regex::new(concat!(
+ r"(?m)^(?:static\s+)?(?:inline\s+)?",
+ r"(?:(?:unsigned\s+)?",
+ r"(?:long|int|void|char|short",
+ r"|struct\s+\w+\s*\*?",
+ r"|[\w_]+_t)",
+ r"\s*\*?\s+)?",
+ r"(\w+)\s*\([^)]*\)",
+ ))?,
+ })
+ }
+
+ fn extract_from_file(&self, path: &Path) -> Result<Vec<ApiSpec>> {
+ let content = fs::read_to_string(path)
+ .with_context(|| format!("Failed to read file: {}", path.display()))?;
+
+ self.extract_from_content(&content)
+ }
+
+ /// Same rule as has-apispec in scripts/Makefile.build: a contexts: (or
+ /// context-flags:) line plus one more KAPI section.
+ fn has_context_line(doc: &str) -> bool {
+ const SECTIONS: &[&str] = &[
+ "api-type:",
+ "param:",
+ "error:",
+ "capability:",
+ "signal:",
+ "lock:",
+ "state-trans:",
+ "constraint:",
+ "side-effect:",
+ "long-desc:",
+ ];
+ let has_line = |keys: &[&str]| {
+ doc.lines()
+ .map(str::trim_start)
+ .any(|l| keys.iter().any(|k| l.starts_with(k)))
+ };
+
+ has_line(&["contexts:", "context-flags:"]) && has_line(SECTIONS)
+ }
+
+ fn extract_from_content(&self, content: &str) -> Result<Vec<ApiSpec>> {
+ let mut specs = Vec::new();
+ let mut in_kerneldoc = false;
+ let mut current_doc = String::new();
+ let lines: Vec<&str> = content.lines().collect();
+ let mut i = 0;
+
+ while i < lines.len() {
+ let line = lines[i];
+
+ // Start of kerneldoc comment
+ if line.trim_start().starts_with("/**") {
+ in_kerneldoc = true;
+ current_doc.clear();
+ i += 1;
+ continue;
+ }
+
+ // Inside kerneldoc comment
+ if in_kerneldoc {
+ if line.contains("*/") {
+ in_kerneldoc = false;
+
+ // Check if this kerneldoc has KAPI annotations
+ if Self::has_context_line(¤t_doc) {
+ // Look ahead for the function declaration
+ if let Some((name, api_type, signature)) =
+ self.find_function_after(&lines, i + 1)
+ {
+ if let Ok(spec) = self.parser.parse_kerneldoc(
+ ¤t_doc,
+ &name,
+ &api_type,
+ Some(&signature),
+ ) {
+ specs.push(spec);
+ }
+ }
+ }
+ } else {
+ // Remove leading asterisk and preserve content
+ let cleaned = if let Some(stripped) = line.trim_start().strip_prefix("*") {
+ if let Some(no_space) = stripped.strip_prefix(' ') {
+ no_space
+ } else {
+ stripped
+ }
+ } else {
+ line.trim_start()
+ };
+ current_doc.push_str(cleaned);
+ current_doc.push('\n');
+ }
+ }
+
+ i += 1;
+ }
+
+ Ok(specs)
+ }
+
+ fn find_function_after(
+ &self,
+ lines: &[&str],
+ start: usize,
+ ) -> Option<(String, String, String)> {
+ for i in start..lines.len().min(start + 10) {
+ let line = lines[i];
+
+ // Skip blank lines and a plain comment before the function
+ let trimmed = line.trim_start();
+ if trimmed.is_empty() || trimmed.starts_with("/*") || trimmed.starts_with('*') {
+ continue;
+ }
+
+ // Check for SYSCALL_DEFINE
+ if let Some(caps) = self.syscall_regex.captures(line) {
+ let name = format!("sys_{}", caps.get(1).unwrap().as_str());
+ let signature = self.extract_syscall_signature(lines, i);
+ return Some((name, "syscall".to_string(), signature));
+ }
+
+ // Check for ioctl function
+ if let Some(caps) = self.ioctl_regex.captures(line) {
+ let name = caps.get(1).unwrap().as_str().to_string();
+ return Some((name, "ioctl".to_string(), line.to_string()));
+ }
+
+ // Check for regular function
+ if let Some(caps) = self.function_regex.captures(line) {
+ let name = caps.get(1).unwrap().as_str().to_string();
+ return Some((name, "function".to_string(), line.to_string()));
+ }
+
+ // Stop if we hit something that's clearly not part of the function declaration
+ if !line.starts_with(' ') && !line.starts_with('\t') && !line.trim().is_empty() {
+ break;
+ }
+ }
+
+ None
+ }
+
+ fn extract_syscall_signature(&self, lines: &[&str], start: usize) -> String {
+ // Extract the full SYSCALL_DEFINE signature
+ let mut sig = String::new();
+ let mut in_paren = false;
+ let mut paren_count = 0;
+
+ for line in lines.iter().skip(start).take(20) {
+ let line = *line;
+
+ // Start of SYSCALL_DEFINE
+ if line.contains("SYSCALL_DEFINE") {
+ if let Some(pos) = line.find('(') {
+ sig.push_str(&line[pos..]);
+ in_paren = true;
+ paren_count = line[pos..].chars().filter(|&c| c == '(').count()
+ - line[pos..].chars().filter(|&c| c == ')').count();
+ }
+ } else if in_paren {
+ sig.push(' ');
+ sig.push_str(line.trim());
+ paren_count += line.chars().filter(|&c| c == '(').count();
+ paren_count =
+ paren_count.saturating_sub(line.chars().filter(|&c| c == ')').count());
+
+ if paren_count == 0 {
+ break;
+ }
+ }
+ }
+
+ sig
+ }
+}
+
+impl ApiExtractor for SourceExtractor {
+ fn extract_all(&self) -> Result<Vec<ApiSpec>> {
+ let path = Path::new(&self.path);
+ let mut all_specs = Vec::new();
+
+ if path.is_file() {
+ // Single file
+ all_specs.extend(self.extract_from_file(path)?);
+ } else if path.is_dir() {
+ // Directory - walk all .c files
+ for entry in WalkDir::new(path)
+ .into_iter()
+ .filter_map(|e| e.ok())
+ .filter(|e| {
+ e.path()
+ .extension()
+ .is_some_and(|ext| ext == "c" || ext == "h")
+ })
+ {
+ match self.extract_from_file(entry.path()) {
+ Ok(specs) => all_specs.extend(specs),
+ Err(e) => {
+ eprintln!("Warning: failed to parse {}: {}", entry.path().display(), e);
+ }
+ }
+ }
+ }
+
+ Ok(all_specs)
+ }
+
+ fn extract_by_name(&self, name: &str) -> Result<Option<ApiSpec>> {
+ let all_specs = self.extract_all()?;
+ Ok(all_specs.into_iter().find(|s| s.name == name))
+ }
+
+ fn display_api_details(
+ &self,
+ api_name: &str,
+ formatter: &mut dyn OutputFormatter,
+ output: &mut dyn Write,
+ ) -> Result<()> {
+ if let Some(spec) = self.extract_by_name(api_name)? {
+ display_api_spec(&spec, formatter, output)?;
+ } else {
+ writeln!(output, "API '{}' not found", api_name)?;
+ }
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn make_extractor() -> SourceExtractor {
+ SourceExtractor::new("/dev/null").unwrap()
+ }
+
+ #[test]
+ fn detect_contexts_spec() {
+ let content = r#"
+/**
+ * sys_foo - do foo
+ * @arg: the argument
+ *
+ * contexts: process, sleepable
+ *
+ * long-desc: Does foo.
+ */
+SYSCALL_DEFINE1(foo, int, arg)
+{
+ return 0;
+}
+
+/**
+ * bar - not a spec, even though the prose mentions context: here
+ * @x: value
+ */
+SYSCALL_DEFINE1(bar, int, x)
+{
+ return 0;
+}
+
+/**
+ * baz - not a spec either
+ * @x: value
+ *
+ * contexts: task, softirq, hardirq, nmi.
+ */
+SYSCALL_DEFINE1(baz, int, x)
+{
+ return 0;
+}
+"#;
+ let specs = make_extractor().extract_from_content(content).unwrap();
+ assert_eq!(specs.len(), 1);
+ assert_eq!(specs[0].name, "sys_foo");
+ }
+
+ #[test]
+ fn prose_mentioning_a_section_key_is_not_a_spec() {
+ let content = r#"
+/**
+ * __probe - check a device
+ * @drv: driver
+ *
+ * returns 0 on success, else error.
+ * side-effect: dev->driver is set to drv when drv claims dev.
+ */
+static int __probe(struct driver *drv, struct device *dev)
+{
+ return 0;
+}
+"#;
+ let specs = make_extractor().extract_from_content(content).unwrap();
+ assert!(specs.is_empty());
+ }
+
+ #[test]
+ fn nonexistent_path_is_rejected() {
+ let err = SourceExtractor::new("/nonexistent/kapi-test")
+ .err()
+ .unwrap();
+
+ assert!(err.to_string().contains("does not exist"), "{err}");
+ }
+
+ #[test]
+ fn has_context_line_needs_two_sections() {
+ let rule = SourceExtractor::has_context_line;
+
+ assert!(rule("contexts: process\nlong-desc: foo\n"));
+ assert!(rule("context-flags: KAPI_CTX_PROCESS\n lock: foo\n"));
+ assert!(rule("contexts: process\nparam: fd\n"));
+ assert!(rule("contexts: process\nerror: EINVAL, bad fd\n"));
+ assert!(!rule("contexts: process\n"));
+ assert!(!rule("context: process\nlong-desc: foo\n"));
+ assert!(!rule("long-desc: foo\nside-effect: bar\n"));
+ }
+
+ #[test]
+ fn detect_syscall_define3() {
+ let content = r#"
+/**
+ * sys_open - open a file
+ * context-flags: KAPI_CTX_PROCESS
+ * param-count: 3
+ * @filename: pathname to open
+ * param: filename
+ * error: ENOENT, test
+ */
+SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
+{
+ return 0;
+}
+"#;
+ let ext = make_extractor();
+ let specs = ext.extract_from_content(content).unwrap();
+ assert_eq!(specs.len(), 1);
+ assert_eq!(specs[0].name, "sys_open");
+ assert_eq!(specs[0].api_type, "syscall");
+ }
+
+ #[test]
+ fn detect_syscall_define1() {
+ let content = r#"
+/**
+ * sys_close - close a file descriptor
+ * context-flags: KAPI_CTX_PROCESS
+ * @fd: file descriptor to close
+ * error: EBADF, test
+ */
+SYSCALL_DEFINE1(close, unsigned int, fd)
+{
+ return 0;
+}
+"#;
+ let ext = make_extractor();
+ let specs = ext.extract_from_content(content).unwrap();
+ assert_eq!(specs.len(), 1);
+ assert_eq!(specs[0].name, "sys_close");
+ }
+
+ #[test]
+ fn detect_syscall_define6() {
+ let content = r#"
+/**
+ * sys_mmap - map memory
+ * context-flags: KAPI_CTX_PROCESS
+ * error: ENOMEM, test
+ */
+SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len, unsigned long, prot,
+ unsigned long, flags, unsigned long, fd, unsigned long, offset)
+{
+ return 0;
+}
+"#;
+ let ext = make_extractor();
+ let specs = ext.extract_from_content(content).unwrap();
+ assert_eq!(specs.len(), 1);
+ assert_eq!(specs[0].name, "sys_mmap");
+ }
+
+ #[test]
+ fn detect_ioctl_pattern() {
+ let content = r#"
+/**
+ * my_ioctl - handle ioctl
+ * context-flags: KAPI_CTX_PROCESS
+ * error: EINVAL, test
+ */
+static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
+{
+ return 0;
+}
+"#;
+ let ext = make_extractor();
+ let specs = ext.extract_from_content(content).unwrap();
+ assert_eq!(specs.len(), 1);
+ assert_eq!(specs[0].name, "my_ioctl");
+ assert_eq!(specs[0].api_type, "ioctl");
+ }
+
+ #[test]
+ fn find_function_after_skips_blanks() {
+ // Test that find_function_after looks past blank lines
+ let lines = vec!["", "", "SYSCALL_DEFINE2(foo, int, bar, int, baz)", "{"];
+ let ext = make_extractor();
+ let result = ext.find_function_after(&lines, 0);
+ assert!(result.is_some());
+ let (name, api_type, _sig) = result.unwrap();
+ assert_eq!(name, "sys_foo");
+ assert_eq!(api_type, "syscall");
+ }
+
+ #[test]
+ fn find_function_after_skips_plain_comment() {
+ let lines = vec![
+ "/*",
+ " * Careful here!",
+ " */",
+ "SYSCALL_DEFINE1(close, unsigned int, fd)",
+ "{",
+ ];
+ let ext = make_extractor();
+ let (name, api_type, _sig) = ext.find_function_after(&lines, 0).unwrap();
+ assert_eq!(name, "sys_close");
+ assert_eq!(api_type, "syscall");
+ }
+
+ #[test]
+ fn find_function_after_returns_none_for_no_match() {
+ // No function declaration within lookahead range
+ let lines = vec!["#include <linux/fs.h>", "#define FOO 1", "/* comment */"];
+ let ext = make_extractor();
+ let result = ext.find_function_after(&lines, 0);
+ assert!(result.is_none());
+ }
+
+ #[test]
+ fn find_function_after_detects_regular_function() {
+ let lines = vec!["", "int do_something(struct task_struct *task)", "{"];
+ let ext = make_extractor();
+ let result = ext.find_function_after(&lines, 0);
+ assert!(result.is_some());
+ let (name, api_type, _) = result.unwrap();
+ assert_eq!(name, "do_something");
+ assert_eq!(api_type, "function");
+ }
+
+ #[test]
+ fn no_kapi_annotations_produces_empty() {
+ // kerneldoc without any KAPI annotations should not produce a spec
+ let content = r#"
+/**
+ * my_func - does stuff
+ * @arg: an argument
+ */
+void my_func(int arg)
+{
+}
+"#;
+ let ext = make_extractor();
+ let specs = ext.extract_from_content(content).unwrap();
+ assert!(specs.is_empty());
+ }
+
+ #[test]
+ fn multiple_syscalls_in_one_file() {
+ let content = r#"
+/**
+ * sys_read - read from fd
+ * context-flags: KAPI_CTX_PROCESS
+ * error: EBADF, test
+ */
+SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
+{
+ return 0;
+}
+
+/**
+ * sys_write - write to fd
+ * context-flags: KAPI_CTX_PROCESS
+ * error: EBADF, test
+ */
+SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)
+{
+ return 0;
+}
+"#;
+ let ext = make_extractor();
+ let specs = ext.extract_from_content(content).unwrap();
+ assert_eq!(specs.len(), 2);
+ assert_eq!(specs[0].name, "sys_read");
+ assert_eq!(specs[1].name, "sys_write");
+ }
+}
--git a/tools/kapi/src/extractor/vmlinux/binary_utils.rs b/tools/kapi/src/extractor/vmlinux/binary_utils.rs
new file mode 100644
index 0000000000000..046772ea690c1
--- /dev/null
+++ b/tools/kapi/src/extractor/vmlinux/binary_utils.rs
@@ -0,0 +1,461 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+
+// Array-bound constants matching `include/linux/kernel_api_spec.h`.
+// String fields are `const char *`; call `DataReader::ptr_size()` for
+// the per-target pointer width.
+pub mod sizes {
+ pub const MAX_PARAMS: usize = 16;
+ pub const MAX_ERRORS: usize = 32;
+ pub const MAX_CONSTRAINTS: usize = 32;
+ pub const MAX_LOCKS: usize = 16;
+ pub const MAX_CAPABILITIES: usize = 8;
+ pub const MAX_SIGNALS: usize = 32;
+ pub const MAX_STRUCT_SPECS: usize = 8;
+ pub const MAX_SIDE_EFFECTS: usize = 32;
+ pub const MAX_STATE_TRANS: usize = 8;
+
+ pub const NAME: usize = 0;
+ pub const DESC: usize = 0;
+}
+
+// Section markers of `struct kernel_api_spec`. The kernel only sets a
+// marker when the matching *_COUNT()/KAPI_EXAMPLES() macro is used, so
+// a zero marker is as valid as the expected one.
+pub mod magic {
+ pub const PARAMS: u32 = 0x4B415031; // 'KAP1'
+ pub const RETURN: u32 = 0x4B415232; // 'KAR2'
+ pub const ERRORS: u32 = 0x4B414533; // 'KAE3'
+ pub const LOCKS: u32 = 0x4B414C34; // 'KAL4'
+ pub const CONSTRAINTS: u32 = 0x4B414335; // 'KAC5'
+ pub const INFO: u32 = 0x4B414936; // 'KAI6'
+ pub const SIGNALS: u32 = 0x4B415337; // 'KAS7'
+ pub const SIGMASK: u32 = 0x4B414D38; // 'KAM8'
+ pub const STRUCTS: u32 = 0x4B415439; // 'KAT9'
+ pub const EFFECTS: u32 = 0x4B414641; // 'KAFA'
+ pub const TRANS: u32 = 0x4B415442; // 'KATB'
+ pub const CAPS: u32 = 0x4B414343; // 'KACC'
+}
+
+/// Resolve a virtual-address string pointer against the vmlinux ELF
+/// and return the NUL-terminated C string it points at.
+pub fn resolve_vaddr_string(elf: &goblin::elf::Elf, data: &[u8], vaddr: u64) -> Option<String> {
+ if vaddr == 0 {
+ return None;
+ }
+ for sh in &elf.section_headers {
+ let start = sh.sh_addr;
+ let end = start.checked_add(sh.sh_size)?;
+ if vaddr < start || vaddr >= end {
+ continue;
+ }
+ // File-backed sections only (skip SHT_NOBITS etc.)
+ if sh.sh_type == goblin::elf::section_header::SHT_NOBITS {
+ return None;
+ }
+ let rel = (vaddr - start) as usize;
+ let file_start = sh.sh_offset as usize + rel;
+ if file_start >= data.len() {
+ return None;
+ }
+ let tail = &data[file_start..];
+ let nul = tail.iter().position(|&b| b == 0)?;
+ return std::str::from_utf8(&tail[..nul]).ok().map(str::to_string);
+ }
+ None
+}
+
+/// Endianness of the target ELF binary
+#[derive(Clone, Copy, PartialEq)]
+pub enum Endian {
+ Little,
+ Big,
+}
+
+/// Resolves string pointers read from `.kapi_specs` back to their
+/// underlying C strings in the vmlinux rodata.
+pub struct StringResolver<'a> {
+ pub elf: &'a goblin::elf::Elf<'a>,
+ pub vmlinux: &'a [u8],
+}
+
+// Helper for reading data at specific offsets
+pub struct DataReader<'a> {
+ pub data: &'a [u8],
+ pub pos: usize,
+ pub endian: Endian,
+ /// true for 64-bit ELF, false for 32-bit
+ pub is_64bit: bool,
+ /// Used to follow `const char *` fields into rodata.
+ pub resolver: Option<StringResolver<'a>>,
+}
+
+impl<'a> DataReader<'a> {
+ pub fn new(data: &'a [u8], offset: usize, endian: Endian, is_64bit: bool) -> Self {
+ Self {
+ data,
+ pos: offset,
+ endian,
+ is_64bit,
+ resolver: None,
+ }
+ }
+
+ pub fn with_resolver(mut self, resolver: StringResolver<'a>) -> Self {
+ self.resolver = Some(resolver);
+ self
+ }
+
+ /// Pointer width of the target in bytes (4 or 8).
+ pub fn ptr_size(&self) -> usize {
+ if self.is_64bit {
+ 8
+ } else {
+ 4
+ }
+ }
+
+ /// Advance the read position to the next multiple of `align`.
+ /// Needed before every naturally-aligned field when the containing
+ /// struct is not `__packed`.
+ pub fn align_to(&mut self, align: usize) {
+ if align > 1 {
+ let rem = self.pos % align;
+ if rem != 0 {
+ self.pos = (self.pos + (align - rem)).min(self.data.len());
+ }
+ }
+ }
+
+ /// Read a target-sized pointer slot. Returns the virtual address
+ /// stored in the slot, or `None` if there isn't enough data. The
+ /// caller is expected to align the reader first if the containing
+ /// struct demands natural alignment.
+ pub fn read_ptr(&mut self) -> Option<u64> {
+ self.align_to(self.ptr_size());
+ if self.is_64bit {
+ self.read_u64()
+ } else {
+ self.read_u32().map(|v| v as u64)
+ }
+ }
+
+ pub fn read_bytes(&mut self, len: usize) -> Option<&'a [u8]> {
+ if self.pos + len <= self.data.len() {
+ let bytes = &self.data[self.pos..self.pos + len];
+ self.pos += len;
+ Some(bytes)
+ } else {
+ None
+ }
+ }
+
+ pub fn read_u32(&mut self) -> Option<u32> {
+ self.align_to(4);
+ let b: [u8; 4] = self.read_bytes(4)?.try_into().unwrap();
+ Some(match self.endian {
+ Endian::Little => u32::from_le_bytes(b),
+ Endian::Big => u32::from_be_bytes(b),
+ })
+ }
+
+ pub fn read_u8(&mut self) -> Option<u8> {
+ self.read_bytes(1).map(|b| b[0])
+ }
+
+ pub fn read_i32(&mut self) -> Option<i32> {
+ self.align_to(4);
+ let b: [u8; 4] = self.read_bytes(4)?.try_into().unwrap();
+ Some(match self.endian {
+ Endian::Little => i32::from_le_bytes(b),
+ Endian::Big => i32::from_be_bytes(b),
+ })
+ }
+
+ pub fn read_u64(&mut self) -> Option<u64> {
+ self.align_to(8);
+ let b: [u8; 8] = self.read_bytes(8)?.try_into().unwrap();
+ Some(match self.endian {
+ Endian::Little => u64::from_le_bytes(b),
+ Endian::Big => u64::from_be_bytes(b),
+ })
+ }
+
+ pub fn read_i64(&mut self) -> Option<i64> {
+ self.align_to(8);
+ let b: [u8; 8] = self.read_bytes(8)?.try_into().unwrap();
+ Some(match self.endian {
+ Endian::Little => i64::from_le_bytes(b),
+ Endian::Big => i64::from_be_bytes(b),
+ })
+ }
+
+ /// Read a target-sized unsigned value (4 bytes for 32-bit, 8 bytes for 64-bit).
+ pub fn read_usize(&mut self) -> Option<usize> {
+ self.align_to(self.ptr_size());
+ if self.is_64bit {
+ // No double-align: read_u64 would re-align, but we just
+ // did that with ptr_size() which is 8 on 64-bit.
+ let b: [u8; 8] = self.read_bytes(8)?.try_into().unwrap();
+ Some(match self.endian {
+ Endian::Little => u64::from_le_bytes(b) as usize,
+ Endian::Big => u64::from_be_bytes(b) as usize,
+ })
+ } else {
+ let b: [u8; 4] = self.read_bytes(4)?.try_into().unwrap();
+ Some(match self.endian {
+ Endian::Little => u32::from_le_bytes(b) as usize,
+ Endian::Big => u32::from_be_bytes(b) as usize,
+ })
+ }
+ }
+
+ // Helper methods for common patterns
+ pub fn read_bool(&mut self) -> Option<bool> {
+ self.read_u8().map(|v| v != 0)
+ }
+
+ /// Read a `const char *` slot using the target pointer width
+ /// (4 bytes on 32-bit, 8 bytes on 64-bit) and, if a resolver is
+ /// attached, follow the address into the vmlinux to recover the
+ /// C string. The `_max_len` argument is ignored.
+ pub fn read_optional_string(&mut self, _max_len: usize) -> Option<String> {
+ let vaddr = self.read_ptr()?;
+ let resolver = self.resolver.as_ref()?;
+ resolve_vaddr_string(resolver.elf, resolver.vmlinux, vaddr).filter(|s| !s.is_empty())
+ }
+
+ pub fn read_string_or_default(&mut self, max_len: usize) -> String {
+ self.read_optional_string(max_len).unwrap_or_default()
+ }
+
+ /// Follow a `const s64 *` slot (already read with `read_ptr`) to the
+ /// `count` values it points at. A NULL or unresolvable pointer yields
+ /// an empty list.
+ pub fn resolve_s64_array(&self, vaddr: u64, count: u32) -> Vec<i64> {
+ let Some(resolver) = self.resolver.as_ref() else {
+ return Vec::new();
+ };
+ if vaddr == 0 {
+ return Vec::new();
+ }
+ let Some(start) = super::vaddr_to_file_offset(resolver.elf, vaddr) else {
+ return Vec::new();
+ };
+ let raw = (count as usize)
+ .checked_mul(8)
+ .and_then(|len| start.checked_add(len))
+ .and_then(|end| resolver.vmlinux.get(start..end));
+ raw.map_or_else(Vec::new, |raw| {
+ raw.chunks_exact(8)
+ .map(|chunk| {
+ let b: [u8; 8] = chunk.try_into().unwrap();
+ match self.endian {
+ Endian::Little => i64::from_le_bytes(b),
+ Endian::Big => i64::from_be_bytes(b),
+ }
+ })
+ .collect()
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ // ---- DataReader little-endian tests ----
+
+ #[test]
+ fn read_u32_little_endian() {
+ let data = [0x78, 0x56, 0x34, 0x12];
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_u32(), Some(0x12345678));
+ }
+
+ #[test]
+ fn read_u32_big_endian() {
+ let data = [0x12, 0x34, 0x56, 0x78];
+ let mut reader = DataReader::new(&data, 0, Endian::Big, true);
+ assert_eq!(reader.read_u32(), Some(0x12345678));
+ }
+
+ #[test]
+ fn read_u64_little_endian() {
+ let data = 0xDEADBEEFCAFEBABEu64.to_le_bytes();
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_u64(), Some(0xDEADBEEFCAFEBABE));
+ }
+
+ #[test]
+ fn read_u64_big_endian() {
+ let data = 0xDEADBEEFCAFEBABEu64.to_be_bytes();
+ let mut reader = DataReader::new(&data, 0, Endian::Big, true);
+ assert_eq!(reader.read_u64(), Some(0xDEADBEEFCAFEBABE));
+ }
+
+ #[test]
+ fn read_i32_little_endian_negative() {
+ let val: i32 = -42;
+ let data = val.to_le_bytes();
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_i32(), Some(-42));
+ }
+
+ #[test]
+ fn read_i32_big_endian_negative() {
+ let val: i32 = -1;
+ let data = val.to_be_bytes();
+ let mut reader = DataReader::new(&data, 0, Endian::Big, true);
+ assert_eq!(reader.read_i32(), Some(-1));
+ }
+
+ #[test]
+ fn read_i64_little_endian() {
+ let val: i64 = -9999999999;
+ let data = val.to_le_bytes();
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_i64(), Some(-9999999999));
+ }
+
+ #[test]
+ fn read_i64_big_endian() {
+ let val: i64 = i64::MIN;
+ let data = val.to_be_bytes();
+ let mut reader = DataReader::new(&data, 0, Endian::Big, true);
+ assert_eq!(reader.read_i64(), Some(i64::MIN));
+ }
+
+ // ---- read_usize tests ----
+
+ #[test]
+ fn read_usize_64bit() {
+ let val: u64 = 0x00000000FFFFFFFF;
+ let data = val.to_le_bytes();
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_usize(), Some(0xFFFFFFFF));
+ }
+
+ #[test]
+ fn read_usize_32bit() {
+ let val: u32 = 0xABCD1234;
+ let data = val.to_le_bytes();
+ let mut reader = DataReader::new(&data, 0, Endian::Little, false);
+ assert_eq!(reader.read_usize(), Some(0xABCD1234));
+ }
+
+ #[test]
+ fn read_usize_32bit_does_not_consume_8_bytes() {
+ // In 32-bit mode, read_usize should only consume 4 bytes
+ let mut data = [0u8; 8];
+ data[..4].copy_from_slice(&42u32.to_le_bytes());
+ data[4..8].copy_from_slice(&99u32.to_le_bytes());
+ let mut reader = DataReader::new(&data, 0, Endian::Little, false);
+ assert_eq!(reader.read_usize(), Some(42));
+ // After reading 4 bytes, pos should be at 4
+ assert_eq!(reader.pos, 4);
+ assert_eq!(reader.read_usize(), Some(99));
+ }
+
+ // ---- Bounds checking ----
+
+ #[test]
+ fn read_u32_past_end_returns_none() {
+ let data = [0x01, 0x02, 0x03]; // only 3 bytes, need 4
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_u32(), None);
+ }
+
+ #[test]
+ fn read_u64_past_end_returns_none() {
+ let data = [0u8; 7]; // only 7 bytes, need 8
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_u64(), None);
+ }
+
+ #[test]
+ fn read_bytes_past_end_returns_none() {
+ let data = [0u8; 4];
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_bytes(5), None);
+ }
+
+ #[test]
+ fn read_at_offset() {
+ // read_u32 auto-aligns to a 4-byte boundary, so the starting
+ // offset must itself be 4-aligned for the value to be read
+ // from its declared position.
+ let data = [0xFF, 0xFF, 0xFF, 0xFF, 0x78, 0x56, 0x34, 0x12];
+ let mut reader = DataReader::new(&data, 4, Endian::Little, true);
+ assert_eq!(reader.read_u32(), Some(0x12345678));
+ }
+
+ #[test]
+ fn read_u32_auto_aligns() {
+ // Starting mid-word, read_u32 snaps to the next 4-byte boundary.
+ let data = [0xDE, 0xAD, 0xBE, 0xEF, 0x78, 0x56, 0x34, 0x12];
+ let mut reader = DataReader::new(&data, 1, Endian::Little, true);
+ assert_eq!(reader.read_u32(), Some(0x12345678));
+ assert_eq!(reader.pos, 8);
+ }
+
+ #[test]
+ fn read_ptr_32bit_uses_4_bytes() {
+ let data = [0x78, 0x56, 0x34, 0x12];
+ let mut reader = DataReader::new(&data, 0, Endian::Little, false);
+ assert_eq!(reader.read_ptr(), Some(0x12345678));
+ assert_eq!(reader.pos, 4);
+ }
+
+ #[test]
+ fn read_ptr_64bit_uses_8_bytes() {
+ let data = [0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00];
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_ptr(), Some(0x12345678));
+ assert_eq!(reader.pos, 8);
+ }
+
+ #[test]
+ fn read_bool_values() {
+ let data = [0, 1, 255];
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_bool(), Some(false));
+ assert_eq!(reader.read_bool(), Some(true));
+ assert_eq!(reader.read_bool(), Some(true)); // any non-zero is true
+ }
+
+ #[test]
+ fn sequential_reads_advance_position() {
+ let mut data = [0u8; 12];
+ data[..4].copy_from_slice(&1u32.to_le_bytes());
+ data[4..8].copy_from_slice(&2u32.to_le_bytes());
+ data[8..12].copy_from_slice(&3u32.to_le_bytes());
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_u32(), Some(1));
+ assert_eq!(reader.read_u32(), Some(2));
+ assert_eq!(reader.read_u32(), Some(3));
+ assert_eq!(reader.pos, 12);
+ }
+
+ #[test]
+ fn read_optional_string_empty_returns_none() {
+ // A string buffer that is just NUL
+ let data = [0u8; 10];
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_optional_string(10), None);
+ }
+
+ #[test]
+ fn read_string_or_default_with_empty() {
+ let data = [0u8; 10];
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_string_or_default(10), "");
+ }
+
+ #[test]
+ fn read_u8_value() {
+ let data = [0x42];
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+ assert_eq!(reader.read_u8(), Some(0x42));
+ }
+}
--git a/tools/kapi/src/extractor/vmlinux/mod.rs b/tools/kapi/src/extractor/vmlinux/mod.rs
new file mode 100644
index 0000000000000..5202f7d80c8d8
--- /dev/null
+++ b/tools/kapi/src/extractor/vmlinux/mod.rs
@@ -0,0 +1,1160 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+
+use super::{
+ ApiExtractor, ApiSpec, CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, ParamSpec,
+ ReturnSpec, SideEffectSpec, SignalMaskSpec, SignalSpec, StateTransitionSpec, StructFieldSpec,
+ StructSpec,
+};
+use crate::formatter::OutputFormatter;
+use anyhow::{Context, Result};
+use goblin::elf::Elf;
+use std::fs;
+use std::io::Write;
+
+mod binary_utils;
+use binary_utils::{magic, sizes, DataReader, Endian};
+
+// Helper to convert empty strings to None
+fn opt_string(s: String) -> Option<String> {
+ if s.is_empty() {
+ None
+ } else {
+ Some(s)
+ }
+}
+
+pub struct VmlinuxExtractor {
+ vmlinux: Vec<u8>,
+ specs: Vec<KapiSpec>,
+ endian: Endian,
+ is_64bit: bool,
+}
+
+#[derive(Debug)]
+struct KapiSpec {
+ name: String,
+ api_type: String,
+ /// File offset in the vmlinux buffer where this spec's
+ /// `struct kernel_api_spec` begins.
+ file_offset: usize,
+}
+
+impl VmlinuxExtractor {
+ pub fn new(vmlinux_path: &str) -> Result<Self> {
+ let vmlinux = fs::read(vmlinux_path)
+ .with_context(|| format!("Failed to read vmlinux file: {vmlinux_path}"))?;
+
+ let elf = Elf::parse(&vmlinux).context("Failed to parse ELF file")?;
+ let endian = if elf.little_endian {
+ Endian::Little
+ } else {
+ Endian::Big
+ };
+ let is_64bit = elf.is_64;
+
+ // Locate the .kapi_specs section boundaries.
+ let mut start_addr = None;
+ let mut stop_addr = None;
+ for sym in &elf.syms {
+ if let Some(name) = elf.strtab.get_at(sym.st_name) {
+ match name {
+ "__start_kapi_specs" => start_addr = Some(sym.st_value),
+ "__stop_kapi_specs" => stop_addr = Some(sym.st_value),
+ _ => {}
+ }
+ }
+ }
+ let start = start_addr.context("Could not find __start_kapi_specs symbol")?;
+ let stop = stop_addr.context("Could not find __stop_kapi_specs symbol")?;
+ if stop <= start {
+ anyhow::bail!("No kernel API specifications found in vmlinux");
+ }
+
+ // `.kapi_specs` is a tightly-packed array of `struct kernel_api_spec *`
+ // pointers; walk them to find each real spec's vaddr, then resolve to
+ // a file offset inside `vmlinux`. Pointer width tracks the target
+ // (4 bytes for 32-bit, 8 bytes for 64-bit).
+ let ptr_size = if is_64bit { 8usize } else { 4 };
+ let ptr_count = ((stop - start) as usize) / ptr_size;
+ let ptr_file_off =
+ vaddr_to_file_offset(&elf, start).context("Could not locate .kapi_specs in file")?;
+
+ let read_ptr = |raw: &[u8]| -> u64 {
+ match (endian, is_64bit) {
+ (Endian::Little, true) => u64::from_le_bytes(raw.try_into().unwrap()),
+ (Endian::Big, true) => u64::from_be_bytes(raw.try_into().unwrap()),
+ (Endian::Little, false) => u32::from_le_bytes(raw.try_into().unwrap()) as u64,
+ (Endian::Big, false) => u32::from_be_bytes(raw.try_into().unwrap()) as u64,
+ }
+ };
+
+ let mut specs = Vec::with_capacity(ptr_count);
+ for i in 0..ptr_count {
+ let p = ptr_file_off + i * ptr_size;
+ if p + ptr_size > vmlinux.len() {
+ break;
+ }
+ let spec_vaddr = read_ptr(&vmlinux[p..p + ptr_size]);
+ if spec_vaddr == 0 {
+ continue;
+ }
+ let Some(spec_file_off) = vaddr_to_file_offset(&elf, spec_vaddr) else {
+ continue;
+ };
+ // The first field of `struct kernel_api_spec` is `const char *name`.
+ if spec_file_off + ptr_size > vmlinux.len() {
+ continue;
+ }
+ let name_vaddr = read_ptr(&vmlinux[spec_file_off..spec_file_off + ptr_size]);
+ let name =
+ binary_utils::resolve_vaddr_string(&elf, &vmlinux, name_vaddr).unwrap_or_default();
+ if name.is_empty() {
+ continue;
+ }
+ let api_type = if name.starts_with("sys_") {
+ "syscall"
+ } else if name.ends_with("_ioctl") {
+ "ioctl"
+ } else {
+ "function"
+ }
+ .to_string();
+ specs.push(KapiSpec {
+ name,
+ api_type,
+ file_offset: spec_file_off,
+ });
+ }
+
+ Ok(VmlinuxExtractor {
+ vmlinux,
+ specs,
+ endian,
+ is_64bit,
+ })
+ }
+}
+
+/// Map a virtual address to a file offset inside the raw vmlinux bytes.
+fn vaddr_to_file_offset(elf: &Elf, vaddr: u64) -> Option<usize> {
+ for sh in &elf.section_headers {
+ let start = sh.sh_addr;
+ let end = start.checked_add(sh.sh_size)?;
+ if vaddr >= start && vaddr < end {
+ if sh.sh_type == goblin::elf::section_header::SHT_NOBITS {
+ return None;
+ }
+ return Some((sh.sh_offset + (vaddr - start)) as usize);
+ }
+ }
+ None
+}
+
+impl VmlinuxExtractor {
+ fn parse_at(&self, file_offset: usize) -> Result<ApiSpec> {
+ parse_binary_to_api_spec(&self.vmlinux, file_offset, self.endian, self.is_64bit)
+ }
+}
+
+impl ApiExtractor for VmlinuxExtractor {
+ fn extract_all(&self) -> Result<Vec<ApiSpec>> {
+ Ok(self
+ .specs
+ .iter()
+ .map(|spec| {
+ self.parse_at(spec.file_offset).unwrap_or_else(|e| {
+ eprintln!("Warning: cannot parse the spec of {}: {e:#}", spec.name);
+ ApiSpec {
+ name: spec.name.clone(),
+ api_type: spec.api_type.clone(),
+ ..Default::default()
+ }
+ })
+ })
+ .collect())
+ }
+
+ fn extract_by_name(&self, api_name: &str) -> Result<Option<ApiSpec>> {
+ if let Some(spec) = self.specs.iter().find(|s| s.name == api_name) {
+ Ok(Some(self.parse_at(spec.file_offset)?))
+ } else {
+ Ok(None)
+ }
+ }
+
+ fn display_api_details(
+ &self,
+ api_name: &str,
+ formatter: &mut dyn OutputFormatter,
+ writer: &mut dyn Write,
+ ) -> Result<()> {
+ if let Some(spec) = self.specs.iter().find(|s| s.name == api_name) {
+ let api_spec = self.parse_at(spec.file_offset)?;
+ super::display_api_spec(&api_spec, formatter, writer)?;
+ }
+ Ok(())
+ }
+}
+
+const TRUNCATED: &str = "kernel_api_spec runs past the end of the file";
+
+/// Consume a section marker. The kernel leaves it zero when the matching
+/// macro is not used, so only a different non-zero value means that the
+/// reader has lost sync with the struct layout.
+fn read_magic(reader: &mut DataReader, expected: u32) -> Result<()> {
+ let found = reader.read_u32().context(TRUNCATED)?;
+ if found != 0 && found != expected {
+ anyhow::bail!(
+ "unexpected section marker {found:#x} at offset {:#x}, expected {expected:#x}",
+ reader.pos - 4
+ );
+ }
+ Ok(())
+}
+
+/// Parse a `{ u32 magic; u32 count; T items[MAX]; }` section. Every one of
+/// the `max_items` slots is consumed, so the reader ends up behind the
+/// whole array; only the first `count` are returned.
+fn parse_array<T, F>(
+ reader: &mut DataReader,
+ expected_magic: u32,
+ max_items: usize,
+ parse_fn: F,
+) -> Result<Vec<T>>
+where
+ F: Fn(&mut DataReader, usize) -> Option<T>,
+{
+ read_magic(reader, expected_magic)?;
+ let count = reader.read_u32().context(TRUNCATED)? as usize;
+ let align = reader.ptr_size();
+ reader.align_to(align);
+
+ let mut items = Vec::new();
+ for i in 0..max_items {
+ let item = parse_fn(reader, i).context(TRUNCATED)?;
+ reader.align_to(align);
+ if i < count {
+ items.push(item);
+ }
+ }
+ Ok(items)
+}
+
+fn parse_binary_to_api_spec(
+ data: &[u8],
+ offset: usize,
+ endian: Endian,
+ is_64bit: bool,
+) -> Result<ApiSpec> {
+ let elf = Elf::parse(data).context("Failed to re-parse ELF for string resolution")?;
+ let resolver = binary_utils::StringResolver {
+ elf: &elf,
+ vmlinux: data,
+ };
+ let mut reader = DataReader::new(data, offset, endian, is_64bit).with_resolver(resolver);
+
+ // Read fields in exact order of struct kernel_api_spec.
+ // Every string field is a `const char *` pointer resolved via the
+ // StringResolver attached to the DataReader.
+ let name = reader
+ .read_optional_string(sizes::NAME)
+ .ok_or_else(|| anyhow::anyhow!("Failed to read API name"))?;
+
+ // Determine API type
+ let api_type = if name.starts_with("sys_") {
+ "syscall"
+ } else if name.ends_with("_ioctl") {
+ "ioctl"
+ } else if name.contains("sysfs") {
+ "sysfs"
+ } else {
+ "function"
+ }
+ .to_string();
+
+ let version = reader.read_u32().map(|v| v.to_string());
+
+ let description = reader
+ .read_optional_string(sizes::DESC)
+ .filter(|s| !s.is_empty());
+
+ let long_description = reader
+ .read_optional_string(sizes::DESC)
+ .filter(|s| !s.is_empty());
+
+ let context_flags = parse_context_flags(&mut reader);
+
+ let parameters = parse_array(&mut reader, magic::PARAMS, sizes::MAX_PARAMS, parse_param)?;
+
+ read_magic(&mut reader, magic::RETURN)?;
+ let return_spec = parse_return_spec(&mut reader);
+
+ let errors = parse_array(&mut reader, magic::ERRORS, sizes::MAX_ERRORS, |r, _| {
+ parse_error(r)
+ })?;
+
+ let locks = parse_array(&mut reader, magic::LOCKS, sizes::MAX_LOCKS, |r, _| {
+ parse_lock(r)
+ })?;
+
+ let constraints = parse_array(
+ &mut reader,
+ magic::CONSTRAINTS,
+ sizes::MAX_CONSTRAINTS,
+ |r, _| parse_constraint(r),
+ )?;
+
+ // Only KAPI_EXAMPLES() sets info_magic, so it says nothing about
+ // whether notes are present.
+ read_magic(&mut reader, magic::INFO)?;
+ let examples = reader
+ .read_optional_string(sizes::DESC)
+ .filter(|s| !s.is_empty());
+ let notes = reader
+ .read_optional_string(sizes::DESC)
+ .filter(|s| !s.is_empty());
+
+ let signals = parse_array(&mut reader, magic::SIGNALS, sizes::MAX_SIGNALS, |r, _| {
+ parse_signal(r)
+ })?;
+
+ let signal_masks = parse_array(&mut reader, magic::SIGMASK, sizes::MAX_SIGNALS, |r, _| {
+ parse_signal_mask(r)
+ })?;
+
+ let struct_specs = parse_array(
+ &mut reader,
+ magic::STRUCTS,
+ sizes::MAX_STRUCT_SPECS,
+ |r, _| parse_struct_spec(r),
+ )?;
+
+ let side_effects = parse_array(
+ &mut reader,
+ magic::EFFECTS,
+ sizes::MAX_SIDE_EFFECTS,
+ |r, _| parse_side_effect(r),
+ )?;
+
+ let state_transitions =
+ parse_array(&mut reader, magic::TRANS, sizes::MAX_STATE_TRANS, |r, _| {
+ parse_state_transition(r)
+ })?;
+
+ let capabilities = parse_array(&mut reader, magic::CAPS, sizes::MAX_CAPABILITIES, |r, _| {
+ parse_capability(r)
+ })?;
+
+ Ok(ApiSpec {
+ name,
+ api_type,
+ description,
+ long_description,
+ version,
+ context_flags,
+ param_count: if parameters.is_empty() {
+ None
+ } else {
+ Some(parameters.len() as u32)
+ },
+ error_count: if errors.is_empty() {
+ None
+ } else {
+ Some(errors.len() as u32)
+ },
+ examples,
+ notes,
+ subsystem: None,
+ sysfs_path: None,
+ permissions: None,
+ capabilities,
+ parameters,
+ return_spec,
+ errors,
+ signals,
+ signal_masks,
+ side_effects,
+ state_transitions,
+ constraints,
+ locks,
+ struct_specs,
+ })
+}
+
+// Helper parsing functions
+
+fn parse_context_flags(reader: &mut DataReader) -> Vec<String> {
+ const KAPI_CTX_PROCESS: u32 = 1 << 0;
+ const KAPI_CTX_SOFTIRQ: u32 = 1 << 1;
+ const KAPI_CTX_HARDIRQ: u32 = 1 << 2;
+ const KAPI_CTX_NMI: u32 = 1 << 3;
+ const KAPI_CTX_ATOMIC: u32 = 1 << 4;
+ const KAPI_CTX_SLEEPABLE: u32 = 1 << 5;
+ const KAPI_CTX_PREEMPT_DISABLED: u32 = 1 << 6;
+ const KAPI_CTX_IRQ_DISABLED: u32 = 1 << 7;
+
+ if let Some(flags) = reader.read_u32() {
+ let mut parts = Vec::new();
+
+ if flags & KAPI_CTX_PROCESS != 0 {
+ parts.push("KAPI_CTX_PROCESS");
+ }
+ if flags & KAPI_CTX_SOFTIRQ != 0 {
+ parts.push("KAPI_CTX_SOFTIRQ");
+ }
+ if flags & KAPI_CTX_HARDIRQ != 0 {
+ parts.push("KAPI_CTX_HARDIRQ");
+ }
+ if flags & KAPI_CTX_NMI != 0 {
+ parts.push("KAPI_CTX_NMI");
+ }
+ if flags & KAPI_CTX_ATOMIC != 0 {
+ parts.push("KAPI_CTX_ATOMIC");
+ }
+ if flags & KAPI_CTX_SLEEPABLE != 0 {
+ parts.push("KAPI_CTX_SLEEPABLE");
+ }
+ if flags & KAPI_CTX_PREEMPT_DISABLED != 0 {
+ parts.push("KAPI_CTX_PREEMPT_DISABLED");
+ }
+ if flags & KAPI_CTX_IRQ_DISABLED != 0 {
+ parts.push("KAPI_CTX_IRQ_DISABLED");
+ }
+
+ parts.into_iter().map(|s| s.to_string()).collect()
+ } else {
+ vec![]
+ }
+}
+
+fn parse_param(reader: &mut DataReader, index: usize) -> Option<ParamSpec> {
+ let name = reader.read_string_or_default(sizes::NAME);
+ let type_name = reader.read_string_or_default(sizes::NAME);
+ let param_type = reader.read_u32()?;
+ let flags = reader.read_u32()?;
+ let size = reader.read_usize()?;
+ let alignment = reader.read_usize()?;
+ let min_value = reader.read_i64()?;
+ let max_value = reader.read_i64()?;
+ let valid_mask = reader.read_u64()?;
+
+ let enum_values_ptr = reader.read_ptr()?;
+ let enum_count = reader.read_u32()?;
+ let constraint_type = reader.read_u32()?;
+ // Skip validate function pointer
+ reader.read_ptr()?;
+
+ let description = reader.read_string_or_default(sizes::DESC);
+ let constraint = reader.read_optional_string(sizes::DESC);
+ let size_param_idx_raw = reader.read_i32()?; // Must use ? to propagate errors
+ let _size_multiplier = reader.read_usize()?; // Must use ? to propagate errors
+
+ // In the C struct, size_param_idx is stored 1-based; 0 means
+ // "no size-carrying param". Surface the real (0-based) index as
+ // `Option<u32>`.
+ let size_param_idx = if size_param_idx_raw > 0 {
+ Some((size_param_idx_raw - 1) as u32)
+ } else {
+ None
+ };
+
+ let mut param = ParamSpec {
+ index: index as u32,
+ name,
+ type_name,
+ description,
+ flags,
+ param_type,
+ constraint_type,
+ constraint,
+ min_value: Some(min_value),
+ max_value: Some(max_value),
+ valid_mask: Some(valid_mask),
+ enum_values: reader
+ .resolve_s64_array(enum_values_ptr, enum_count)
+ .iter()
+ .map(i64::to_string)
+ .collect(),
+ size: Some(size as u32),
+ alignment: Some(alignment as u32),
+ size_param_idx,
+ };
+ param.keep_used_numbers();
+ Some(param)
+}
+
+fn parse_return_spec(reader: &mut DataReader) -> Option<ReturnSpec> {
+ // Read type_name, but treat empty as valid (will be empty string)
+ let type_name = reader.read_string_or_default(sizes::NAME);
+
+ // Read return_type and check_type
+ let return_type = reader.read_u32().unwrap_or(0);
+ let check_type = reader.read_u32().unwrap_or(0);
+ let success_value = reader.read_i64().unwrap_or(0);
+ let success_min = reader.read_i64().unwrap_or(0);
+ let success_max = reader.read_i64().unwrap_or(0);
+
+ let error_values_ptr = reader.read_ptr().unwrap_or(0);
+ let error_count = reader.read_u32().unwrap_or(0);
+ // Skip is_success function pointer
+ let _ = reader.read_ptr();
+
+ let description = reader.read_string_or_default(sizes::DESC);
+
+ // Return a spec even if type_name is empty, as long as we have some data
+ // The type_name might be a string like "KAPI_TYPE_INT" that gets stored literally
+ if type_name.is_empty() && return_type == 0 && check_type == 0 && success_value == 0 {
+ // No return spec at all
+ return None;
+ }
+
+ let mut ret = ReturnSpec {
+ type_name,
+ description,
+ return_type,
+ check_type,
+ success_value: Some(success_value),
+ success_min: Some(success_min),
+ success_max: Some(success_max),
+ error_values: reader
+ .resolve_s64_array(error_values_ptr, error_count)
+ .into_iter()
+ .filter_map(|v| i32::try_from(v).ok())
+ .collect(),
+ };
+ ret.keep_used_success_fields();
+ Some(ret)
+}
+
+fn parse_error(reader: &mut DataReader) -> Option<ErrorSpec> {
+ let error_code = reader.read_i32()?;
+ let name = reader.read_string_or_default(sizes::NAME);
+ let condition = reader.read_string_or_default(sizes::DESC);
+ let description = reader.read_string_or_default(sizes::DESC);
+
+ Some(ErrorSpec {
+ error_code,
+ name,
+ condition,
+ description,
+ })
+}
+
+fn parse_lock(reader: &mut DataReader) -> Option<LockSpec> {
+ let lock_name = reader.read_string_or_default(sizes::NAME);
+ let lock_type = reader.read_u32()?;
+ let scope = reader.read_u32()?;
+ let description = reader.read_string_or_default(sizes::DESC);
+
+ Some(LockSpec {
+ lock_name,
+ lock_type,
+ scope,
+ description,
+ })
+}
+
+fn parse_constraint(reader: &mut DataReader) -> Option<ConstraintSpec> {
+ let name = reader.read_string_or_default(sizes::NAME);
+ let description = reader.read_string_or_default(sizes::DESC);
+ let expression = reader.read_string_or_default(sizes::DESC);
+
+ Some(ConstraintSpec {
+ name,
+ description,
+ expression: opt_string(expression),
+ })
+}
+
+fn parse_signal(reader: &mut DataReader) -> Option<SignalSpec> {
+ // Matches `struct kapi_signal_spec`. All string fields are pointers.
+ let signal_num = reader.read_i32()?;
+ let signal_name = reader.read_optional_string(sizes::NAME).unwrap_or_default();
+ let direction = reader.read_u32()?;
+ let action = reader.read_u32()?;
+ let target = reader.read_optional_string(sizes::DESC);
+ let condition = reader.read_optional_string(sizes::DESC);
+ let description = reader.read_optional_string(sizes::DESC);
+ let restartable = reader.read_bool()?;
+ let sa_flags_required = reader.read_u32()?;
+ let sa_flags_forbidden = reader.read_u32()?;
+ let error_on_signal = reader.read_i32()?;
+ let transform_to = reader.read_i32()?;
+ // Read the symbolic timing token (const char *) and map it to the
+ // numeric timing code used by downstream consumers.
+ let timing_str = reader.read_optional_string(sizes::NAME).unwrap_or_default();
+ let timing = match timing_str.as_str() {
+ "KAPI_SIGNAL_TIME_BEFORE" | "before" => 0u32,
+ "KAPI_SIGNAL_TIME_DURING" | "during" => 1,
+ "KAPI_SIGNAL_TIME_AFTER" | "after" => 2,
+ _ => 0,
+ };
+ let priority = reader.read_u8()?;
+ let interruptible = reader.read_bool()?;
+ let queue_behavior = reader.read_optional_string(sizes::NAME);
+ let state_required = reader.read_u32()?;
+ let state_forbidden = reader.read_u32()?;
+
+ Some(SignalSpec {
+ signal_num,
+ signal_name,
+ direction,
+ action,
+ target,
+ condition,
+ description,
+ timing,
+ priority: priority as u32,
+ restartable,
+ interruptible,
+ queue: queue_behavior,
+ sa_flags: 0, // Not a field of struct kapi_signal_spec
+ sa_flags_required,
+ sa_flags_forbidden,
+ state_required,
+ state_forbidden,
+ // `error_on_signal` of 0 means "no errno returned"; surface
+ // that as None to match the source-parser convention.
+ error_on_signal: if error_on_signal != 0 {
+ Some(error_on_signal)
+ } else {
+ None
+ },
+ transform_to: if transform_to != 0 {
+ // The compiled struct holds the numeric value; the C
+ // preprocessor already resolved any signal symbol.
+ Some(transform_to)
+ } else {
+ None
+ },
+ })
+}
+
+fn parse_signal_mask(reader: &mut DataReader) -> Option<SignalMaskSpec> {
+ let name = reader.read_string_or_default(sizes::NAME);
+
+ let mut signals = Vec::with_capacity(sizes::MAX_SIGNALS);
+ for _ in 0..sizes::MAX_SIGNALS {
+ signals.push(reader.read_i32()?);
+ }
+ let signal_count = reader.read_u32()?;
+ signals.truncate(signal_count as usize);
+
+ let description = reader.read_string_or_default(sizes::DESC);
+
+ Some(SignalMaskSpec {
+ name,
+ description,
+ signals,
+ })
+}
+
+fn parse_struct_field(reader: &mut DataReader) -> Option<StructFieldSpec> {
+ let name = reader.read_string_or_default(sizes::NAME);
+ let field_type = reader.read_u32()?;
+ let type_name = reader.read_string_or_default(sizes::NAME);
+ let offset = reader.read_usize()?;
+ let size = reader.read_usize()?;
+ let flags = reader.read_u32()?;
+ let constraint_type = reader.read_u32()?;
+ let min_value = reader.read_i64()?;
+ let max_value = reader.read_i64()?;
+ let valid_mask = reader.read_u64()?;
+ // enum_values is a `const char *` that StructFieldSpec has no slot for
+ reader.read_ptr()?;
+ let description = reader.read_string_or_default(sizes::DESC);
+
+ Some(StructFieldSpec {
+ name,
+ field_type,
+ type_name,
+ offset,
+ size,
+ flags,
+ constraint_type,
+ min_value,
+ max_value,
+ valid_mask,
+ description,
+ })
+}
+
+fn parse_struct_spec(reader: &mut DataReader) -> Option<StructSpec> {
+ let name = reader.read_string_or_default(sizes::NAME);
+ let size = reader.read_usize()?;
+ let alignment = reader.read_usize()?;
+ let field_count = reader.read_u32()?;
+
+ let mut fields = Vec::new();
+ for i in 0..sizes::MAX_PARAMS {
+ let field = parse_struct_field(reader)?;
+ reader.align_to(reader.ptr_size());
+ if i < field_count as usize {
+ fields.push(field);
+ }
+ }
+
+ let description = reader.read_string_or_default(sizes::DESC);
+
+ Some(StructSpec {
+ name,
+ size,
+ alignment,
+ field_count: fields.len() as u32,
+ fields,
+ description,
+ })
+}
+
+fn parse_side_effect(reader: &mut DataReader) -> Option<SideEffectSpec> {
+ let effect_type = reader.read_u32()?;
+ let target = reader.read_string_or_default(sizes::NAME);
+ let condition = reader.read_string_or_default(sizes::DESC);
+ let description = reader.read_string_or_default(sizes::DESC);
+ let reversible = reader.read_bool()?;
+
+ Some(SideEffectSpec {
+ effect_type,
+ target,
+ condition: opt_string(condition),
+ description,
+ reversible,
+ })
+}
+
+fn parse_state_transition(reader: &mut DataReader) -> Option<StateTransitionSpec> {
+ let from_state = reader.read_string_or_default(sizes::NAME);
+ let to_state = reader.read_string_or_default(sizes::NAME);
+ let condition = reader.read_string_or_default(sizes::DESC);
+ let object = reader.read_string_or_default(sizes::NAME);
+ let description = reader.read_string_or_default(sizes::DESC);
+
+ Some(StateTransitionSpec {
+ object,
+ from_state,
+ to_state,
+ condition: opt_string(condition),
+ description,
+ })
+}
+
+fn parse_capability(reader: &mut DataReader) -> Option<CapabilitySpec> {
+ // Struct layout matches `struct kapi_capability_spec`:
+ // int capability; const char *cap_name; enum action;
+ // const char *allows; const char *without_cap;
+ // const char *check_condition; u8 priority;
+ // int alternative[KAPI_MAX_CAPABILITIES]; u32 alternative_count;
+ let capability = reader.read_i32()?;
+ let cap_name = reader.read_string_or_default(sizes::NAME);
+ let action = reader.read_u32()?;
+ let allows = reader.read_string_or_default(sizes::DESC);
+ let without_cap = reader.read_string_or_default(sizes::DESC);
+ let check_condition = reader.read_optional_string(sizes::DESC);
+ let priority = reader.read_u8()?;
+
+ let mut alternatives = Vec::with_capacity(sizes::MAX_CAPABILITIES);
+ for _ in 0..sizes::MAX_CAPABILITIES {
+ alternatives.push(reader.read_i32()?);
+ }
+ let alternative_count = reader.read_u32()?;
+ alternatives.truncate(alternative_count as usize);
+
+ Some(CapabilitySpec {
+ capability,
+ name: cap_name,
+ action: capability_action_to_string(action),
+ allows,
+ without_cap,
+ check_condition,
+ priority: Some(priority),
+ alternatives,
+ })
+}
+
+/// Map the `enum kapi_capability_action` numeric value to its symbolic
+/// spelling, matching `include/linux/kernel_api_spec.h`.
+fn capability_action_to_string(n: u32) -> String {
+ match n {
+ 0 => "KAPI_CAP_BYPASS_CHECK",
+ 1 => "KAPI_CAP_INCREASE_LIMIT",
+ 2 => "KAPI_CAP_OVERRIDE_RESTRICTION",
+ 3 => "KAPI_CAP_GRANT_PERMISSION",
+ 4 => "KAPI_CAP_MODIFY_BEHAVIOR",
+ 5 => "KAPI_CAP_ACCESS_RESOURCE",
+ 6 => "KAPI_CAP_PERFORM_OPERATION",
+ _ => return n.to_string(),
+ }
+ .to_string()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn state_transition_with_empty_strings_consumes_the_whole_struct() {
+ // five `const char *` slots: from_state, to_state, condition,
+ // object, description
+ let data = [0u8; 5 * 8];
+ let mut reader = DataReader::new(&data, 0, Endian::Little, true);
+
+ let trans = parse_state_transition(&mut reader).unwrap();
+
+ assert_eq!(reader.pos, data.len());
+ assert_eq!(trans.from_state, "");
+ assert_eq!(trans.condition, None);
+ assert_eq!(trans.description, "");
+ }
+
+ // Offsets inside `struct kernel_api_spec` and its element structs as
+ // laid out by gcc on x86-64, taken from offsetof()/sizeof().
+ const SPEC_SIZE: usize = 26400;
+ const PARAM_MAGIC: usize = 36;
+ const PARAMS: usize = 48;
+ const PARAM_SIZE: usize = 120;
+ const RETURN_MAGIC: usize = 1968;
+ const RETURN_SPEC: usize = 1976;
+ const ERROR_MAGIC: usize = 2048;
+ const ERRORS: usize = 2056;
+ const ERROR_SIZE: usize = 32;
+ const LOCK_MAGIC: usize = 3080;
+ const LOCKS: usize = 3088;
+ const LOCK_SIZE: usize = 24;
+ const CONSTRAINT_MAGIC: usize = 3472;
+ const CONSTRAINTS: usize = 3480;
+ const CONSTRAINT_SIZE: usize = 24;
+ const INFO_MAGIC: usize = 4248;
+ const NOTES: usize = 4264;
+ const SIGNAL_MAGIC: usize = 4272;
+ const SIGNALS: usize = 4280;
+ const SIGNAL_SIZE: usize = 104;
+ const SIGMASK_MAGIC: usize = 7608;
+ const SIGNAL_MASKS: usize = 7616;
+ const SIGNAL_MASK_SIZE: usize = 152;
+ const STRUCT_MAGIC: usize = 12480;
+ const STRUCT_SPECS: usize = 12488;
+ const STRUCT_SPEC_SIZE: usize = 1448;
+ const STRUCT_FIELD_SIZE: usize = 88;
+ const EFFECT_MAGIC: usize = 24072;
+ const SIDE_EFFECTS: usize = 24080;
+ const SIDE_EFFECT_SIZE: usize = 40;
+ const TRANS_MAGIC: usize = 25360;
+ const STATE_TRANSITIONS: usize = 25368;
+ const STATE_TRANSITION_SIZE: usize = 40;
+ const CAP_MAGIC: usize = 25688;
+ const CAPABILITIES: usize = 25696;
+ const CAPABILITY_SIZE: usize = 88;
+
+ const BASE: u64 = 0xffff_ffff_8100_0000;
+ const CONTENT_OFFSET: usize = 0x1000;
+
+ /// A `struct kernel_api_spec` image preceded by the strings and
+ /// `s64` arrays it points at, wrapped in a minimal ELF file.
+ struct Image {
+ spec: Vec<u8>,
+ tail: Vec<u8>,
+ }
+
+ impl Image {
+ fn new() -> Self {
+ Image {
+ spec: vec![0; SPEC_SIZE],
+ tail: Vec::new(),
+ }
+ }
+
+ fn u32(&mut self, at: usize, v: u32) {
+ self.spec[at..at + 4].copy_from_slice(&v.to_le_bytes());
+ }
+
+ fn u64(&mut self, at: usize, v: u64) {
+ self.spec[at..at + 8].copy_from_slice(&v.to_le_bytes());
+ }
+
+ fn tail_vaddr(&self) -> u64 {
+ BASE + self.tail.len() as u64
+ }
+
+ fn string(&mut self, at: usize, s: &str) {
+ let vaddr = self.tail_vaddr();
+ self.tail.extend_from_slice(s.as_bytes());
+ self.tail.push(0);
+ self.u64(at, vaddr);
+ }
+
+ fn s64s(&mut self, at: usize, vals: &[i64]) {
+ while self.tail.len() % 8 != 0 {
+ self.tail.push(0);
+ }
+ let vaddr = self.tail_vaddr();
+ for v in vals {
+ self.tail.extend_from_slice(&v.to_le_bytes());
+ }
+ self.u64(at, vaddr);
+ }
+
+ fn elf(&self) -> Vec<u8> {
+ let mut content = self.tail.clone();
+ content.extend_from_slice(&self.spec);
+
+ let mut img = vec![0u8; CONTENT_OFFSET];
+ img[..4].copy_from_slice(b"\x7fELF");
+ img[4] = 2; // ELFCLASS64
+ img[5] = 1; // ELFDATA2LSB
+ img[6] = 1; // EV_CURRENT
+ img[16..18].copy_from_slice(&2u16.to_le_bytes()); // ET_EXEC
+ img[18..20].copy_from_slice(&62u16.to_le_bytes()); // EM_X86_64
+ img[20..24].copy_from_slice(&1u32.to_le_bytes());
+ img[40..48].copy_from_slice(&64u64.to_le_bytes()); // e_shoff
+ img[52..54].copy_from_slice(&64u16.to_le_bytes()); // e_ehsize
+ img[58..60].copy_from_slice(&64u16.to_le_bytes()); // e_shentsize
+ img[60..62].copy_from_slice(&2u16.to_le_bytes()); // e_shnum
+ let sh = 64 + 64; // section 1 follows the null section header at 64
+ img[sh + 4..sh + 8].copy_from_slice(&1u32.to_le_bytes()); // SHT_PROGBITS
+ img[sh + 8..sh + 16].copy_from_slice(&2u64.to_le_bytes()); // SHF_ALLOC
+ img[sh + 16..sh + 24].copy_from_slice(&BASE.to_le_bytes());
+ img[sh + 24..sh + 32].copy_from_slice(&(CONTENT_OFFSET as u64).to_le_bytes());
+ img[sh + 32..sh + 40].copy_from_slice(&(content.len() as u64).to_le_bytes());
+ img[sh + 48..sh + 56].copy_from_slice(&8u64.to_le_bytes());
+ img.extend_from_slice(&content);
+ img
+ }
+
+ fn parse(&mut self) -> Result<ApiSpec> {
+ self.string(0, "kapi_fixture");
+ while self.tail.len() % 8 != 0 {
+ self.tail.push(0);
+ }
+ let img = self.elf();
+ parse_binary_to_api_spec(&img, CONTENT_OFFSET + self.tail.len(), Endian::Little, true)
+ }
+ }
+
+ #[test]
+ fn every_array_slot_is_consumed() {
+ let mut img = Image::new();
+ img.u32(PARAM_MAGIC, magic::PARAMS);
+ img.u32(PARAM_MAGIC + 4, 16);
+ img.u32(RETURN_MAGIC, magic::RETURN);
+ img.u32(ERROR_MAGIC, magic::ERRORS);
+ img.u32(ERROR_MAGIC + 4, 32);
+ img.u32(LOCK_MAGIC, magic::LOCKS);
+ img.u32(LOCK_MAGIC + 4, 16);
+ img.u32(CONSTRAINT_MAGIC, magic::CONSTRAINTS);
+ img.u32(CONSTRAINT_MAGIC + 4, 32);
+ img.u32(SIGNAL_MAGIC, magic::SIGNALS);
+ img.u32(SIGNAL_MAGIC + 4, 32);
+ img.u32(SIGMASK_MAGIC, magic::SIGMASK);
+ img.u32(SIGMASK_MAGIC + 4, 32);
+ img.u32(STRUCT_MAGIC, magic::STRUCTS);
+ img.u32(STRUCT_MAGIC + 4, 8);
+ img.u32(EFFECT_MAGIC, magic::EFFECTS);
+ img.u32(EFFECT_MAGIC + 4, 32);
+ img.u32(TRANS_MAGIC, magic::TRANS);
+ img.u32(TRANS_MAGIC + 4, 8);
+ img.u32(CAP_MAGIC, magic::CAPS);
+ img.u32(CAP_MAGIC + 4, 8);
+
+ img.string(PARAMS + 15 * PARAM_SIZE, "last_param");
+ img.u32(ERRORS + 31 * ERROR_SIZE, -7i32 as u32);
+ img.string(ERRORS + 31 * ERROR_SIZE + 8, "ELAST");
+ img.string(LOCKS + 15 * LOCK_SIZE, "last_lock");
+ img.string(CONSTRAINTS + 31 * CONSTRAINT_SIZE, "last_constraint");
+ img.u32(SIGNALS + 31 * SIGNAL_SIZE, 31);
+ img.u32(SIGNAL_MASKS + 31 * SIGNAL_MASK_SIZE + 8, 64);
+ img.u32(SIGNAL_MASKS + 31 * SIGNAL_MASK_SIZE + 136, 1);
+ img.string(SIGNAL_MASKS + 31 * SIGNAL_MASK_SIZE, "last mask");
+ img.string(SIGNAL_MASKS + 31 * SIGNAL_MASK_SIZE + 144, "mask desc");
+
+ let last_struct = STRUCT_SPECS + 7 * STRUCT_SPEC_SIZE;
+ img.string(last_struct, "last_struct");
+ img.u32(last_struct + 24, 16);
+ img.string(last_struct + 32 + 15 * STRUCT_FIELD_SIZE, "last_field");
+ img.string(last_struct + 32 + 15 * STRUCT_FIELD_SIZE + 80, "field desc");
+ img.string(last_struct + 1440, "struct desc");
+
+ img.string(SIDE_EFFECTS + 31 * SIDE_EFFECT_SIZE + 8, "last_effect");
+ img.string(
+ STATE_TRANSITIONS + 7 * STATE_TRANSITION_SIZE + 24,
+ "last_object",
+ );
+ let last_cap = CAPABILITIES + 7 * CAPABILITY_SIZE;
+ img.u32(last_cap, 40);
+ img.u32(last_cap + 52 + 7 * 4, 99);
+ img.u32(last_cap + 84, 8);
+
+ let spec = img.parse().unwrap();
+
+ assert_eq!(spec.parameters.len(), 16);
+ assert_eq!(spec.parameters[15].name, "last_param");
+ assert_eq!(spec.errors.len(), 32);
+ assert_eq!(spec.errors[31].error_code, -7);
+ assert_eq!(spec.errors[31].name, "ELAST");
+ assert_eq!(spec.locks[15].lock_name, "last_lock");
+ assert_eq!(spec.constraints[31].name, "last_constraint");
+ assert_eq!(spec.signals[31].signal_num, 31);
+ assert_eq!(spec.signal_masks.len(), 32);
+ assert_eq!(spec.signal_masks[31].name, "last mask");
+ assert_eq!(spec.signal_masks[31].signals, [64]);
+ assert_eq!(spec.signal_masks[31].description, "mask desc");
+
+ let st = &spec.struct_specs[7];
+ assert_eq!(st.name, "last_struct");
+ assert_eq!(st.description, "struct desc");
+ assert_eq!(st.field_count, 16);
+ assert_eq!(st.fields[15].name, "last_field");
+ assert_eq!(st.fields[15].description, "field desc");
+
+ assert_eq!(spec.side_effects[31].target, "last_effect");
+ assert_eq!(spec.state_transitions[7].object, "last_object");
+ let cap = &spec.capabilities[7];
+ assert_eq!(cap.capability, 40);
+ assert_eq!(cap.alternatives, [0, 0, 0, 0, 0, 0, 0, 99]);
+ }
+
+ #[test]
+ fn notes_and_signal_masks_are_read_without_their_markers() {
+ let mut img = Image::new();
+ img.u32(SIGMASK_MAGIC + 4, 2);
+ img.string(NOTES, "only notes");
+ img.string(SIGNAL_MASKS, "first");
+ img.u32(SIGNAL_MASKS + 8, 2);
+ img.u32(SIGNAL_MASKS + 12, 15);
+ img.u32(SIGNAL_MASKS + 136, 2);
+ img.string(SIGNAL_MASKS + 144, "first desc");
+ img.string(SIGNAL_MASKS + SIGNAL_MASK_SIZE, "second");
+
+ let spec = img.parse().unwrap();
+
+ assert_eq!(spec.examples, None);
+ assert_eq!(spec.notes.as_deref(), Some("only notes"));
+ assert_eq!(spec.signal_masks.len(), 2);
+ assert_eq!(spec.signal_masks[0].name, "first");
+ assert_eq!(spec.signal_masks[0].description, "first desc");
+ assert_eq!(spec.signal_masks[0].signals, [2, 15]);
+ assert_eq!(spec.signal_masks[1].name, "second");
+ assert!(spec.signal_masks[1].signals.is_empty());
+ }
+
+ #[test]
+ fn examples_are_read_next_to_notes() {
+ let mut img = Image::new();
+ img.u32(INFO_MAGIC, magic::INFO);
+ img.string(INFO_MAGIC + 8, "an example");
+ img.string(NOTES, "a note");
+
+ let spec = img.parse().unwrap();
+
+ assert_eq!(spec.examples.as_deref(), Some("an example"));
+ assert_eq!(spec.notes.as_deref(), Some("a note"));
+ }
+
+ #[test]
+ fn enum_and_error_values_are_followed_through_their_pointers() {
+ let mut img = Image::new();
+ img.u32(PARAM_MAGIC, magic::PARAMS);
+ img.u32(PARAM_MAGIC + 4, 2);
+ img.string(PARAMS, "mode");
+ img.s64s(PARAMS + 64, &[0, 1, -7, 1 << 40]);
+ img.u32(PARAMS + 72, 4);
+ img.string(PARAMS + PARAM_SIZE, "plain");
+ img.u32(RETURN_MAGIC, magic::RETURN);
+ img.string(RETURN_SPEC, "long");
+ img.u32(RETURN_SPEC + 12, 2);
+ img.s64s(RETURN_SPEC + 40, &[-22, -2, -(1 << 40)]);
+ img.u32(RETURN_SPEC + 48, 3);
+
+ let spec = img.parse().unwrap();
+
+ assert_eq!(
+ spec.parameters[0].enum_values,
+ ["0", "1", "-7", "1099511627776"]
+ );
+ assert!(spec.parameters[1].enum_values.is_empty());
+ let ret = spec.return_spec.unwrap();
+ assert_eq!(ret.check_type, 2);
+ assert_eq!(ret.error_values, [-22, -2]);
+ }
+
+ #[test]
+ fn numbers_the_constraint_does_not_use_are_unset() {
+ let mut img = Image::new();
+ img.u32(PARAM_MAGIC, magic::PARAMS);
+ img.u32(PARAM_MAGIC + 4, 4);
+ img.string(PARAMS, "plain");
+ let ranged = PARAMS + PARAM_SIZE;
+ img.string(ranged, "ranged");
+ img.u64(ranged + 24, 16);
+ img.u64(ranged + 40, -5i64 as u64);
+ img.u64(ranged + 48, 9);
+ img.u64(ranged + 56, 0xff);
+ img.u32(ranged + 76, 1);
+ let string = PARAMS + 2 * PARAM_SIZE;
+ img.string(string, "string");
+ img.u64(string + 40, 1);
+ img.u64(string + 48, 255);
+ img.u32(string + 76, 8);
+ let unlimited = PARAMS + 3 * PARAM_SIZE;
+ img.string(unlimited, "unlimited");
+ img.u32(unlimited + 76, 8);
+ img.u32(RETURN_MAGIC, magic::RETURN);
+ img.string(RETURN_SPEC, "long");
+ img.u32(RETURN_SPEC + 12, 1);
+ img.u64(RETURN_SPEC + 16, 7);
+ img.u64(RETURN_SPEC + 32, 100);
+
+ let spec = img.parse().unwrap();
+
+ let plain = &spec.parameters[0];
+ assert_eq!((plain.min_value, plain.max_value), (None, None));
+ assert_eq!(
+ (plain.valid_mask, plain.size, plain.alignment),
+ (None, None, None)
+ );
+ let ranged = &spec.parameters[1];
+ assert_eq!((ranged.min_value, ranged.max_value), (Some(-5), Some(9)));
+ assert_eq!((ranged.valid_mask, ranged.size), (None, Some(16)));
+ let string = &spec.parameters[2];
+ assert_eq!((string.min_value, string.max_value), (Some(1), Some(255)));
+ let unlimited = &spec.parameters[3];
+ assert_eq!((unlimited.min_value, unlimited.max_value), (None, None));
+ let ret = spec.return_spec.unwrap();
+ assert_eq!(ret.success_value, None);
+ assert_eq!((ret.success_min, ret.success_max), (Some(0), Some(100)));
+ }
+
+ #[test]
+ fn enum_values_behind_a_dangling_pointer_are_dropped() {
+ let mut img = Image::new();
+ img.u32(PARAM_MAGIC, magic::PARAMS);
+ img.u32(PARAM_MAGIC + 4, 1);
+ img.string(PARAMS, "mode");
+ img.u64(PARAMS + 64, 0x1000);
+ img.u32(PARAMS + 72, 4);
+
+ let spec = img.parse().unwrap();
+
+ assert!(spec.parameters[0].enum_values.is_empty());
+ }
+
+ #[test]
+ fn unexpected_section_marker_is_rejected() {
+ let mut img = Image::new();
+ img.u32(ERROR_MAGIC, 0xdead_beef);
+
+ let err = img.parse().unwrap_err();
+
+ assert!(err.to_string().contains("0xdeadbeef"), "{err}");
+ }
+
+ #[test]
+ fn truncated_spec_is_rejected() {
+ let mut img = Image::new();
+ img.spec.truncate(SIGNAL_MASKS);
+
+ let err = img.parse().unwrap_err();
+
+ assert!(err.to_string().contains("past the end"), "{err}");
+ }
+}
diff --git a/tools/kapi/src/formatter/json.rs b/tools/kapi/src/formatter/json.rs
new file mode 100644
index 0000000000000..5f2367ad12d40
--- /dev/null
+++ b/tools/kapi/src/formatter/json.rs
@@ -0,0 +1,659 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+
+use super::OutputFormatter;
+use crate::extractor::{
+ CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, ParamSpec, ReturnSpec, SideEffectSpec,
+ SignalMaskSpec, SignalSpec, StateTransitionSpec, StructSpec,
+};
+use serde::Serialize;
+use std::io::Write;
+
+pub struct JsonFormatter {
+ data: JsonData,
+}
+
+#[derive(Serialize)]
+struct JsonData {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ apis: Option<Vec<JsonApi>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ api_details: Option<JsonApiDetails>,
+}
+
+#[derive(Serialize)]
+struct JsonApi {
+ name: String,
+ api_type: String,
+}
+
+#[derive(Serialize)]
+struct JsonApiDetails {
+ name: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ description: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ long_description: Option<String>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ context_flags: Vec<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ examples: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ notes: Option<String>,
+ // Sysfs-specific fields
+ #[serde(skip_serializing_if = "Option::is_none")]
+ subsystem: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ sysfs_path: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ permissions: Option<String>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ capabilities: Vec<CapabilitySpec>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ state_transitions: Vec<StateTransitionSpec>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ side_effects: Vec<SideEffectSpec>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ parameters: Vec<ParamSpec>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ return_spec: Option<ReturnSpec>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ errors: Vec<ErrorSpec>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ locks: Vec<LockSpec>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ struct_specs: Vec<StructSpec>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ signals: Vec<SignalSpec>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ signal_masks: Vec<SignalMaskSpec>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ constraints: Vec<ConstraintSpec>,
+}
+
+impl JsonFormatter {
+ pub fn new() -> Self {
+ JsonFormatter {
+ data: JsonData {
+ apis: None,
+ api_details: None,
+ },
+ }
+ }
+}
+
+impl OutputFormatter for JsonFormatter {
+ fn begin_document(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn end_document(&mut self, w: &mut dyn Write) -> std::io::Result<()> {
+ let json = serde_json::to_string_pretty(&self.data)?;
+ writeln!(w, "{json}")?;
+ Ok(())
+ }
+
+ fn begin_api_list(&mut self, _w: &mut dyn Write, _title: &str) -> std::io::Result<()> {
+ if self.data.apis.is_none() {
+ self.data.apis = Some(Vec::new());
+ }
+ Ok(())
+ }
+
+ fn api_item(&mut self, _w: &mut dyn Write, name: &str, api_type: &str) -> std::io::Result<()> {
+ if let Some(apis) = &mut self.data.apis {
+ apis.push(JsonApi {
+ name: name.to_string(),
+ api_type: api_type.to_string(),
+ });
+ }
+ Ok(())
+ }
+
+ fn end_api_list(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn total_specs(&mut self, _w: &mut dyn Write, _count: usize) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_api_details(&mut self, _w: &mut dyn Write, name: &str) -> std::io::Result<()> {
+ self.data.api_details = Some(JsonApiDetails {
+ name: name.to_string(),
+ description: None,
+ long_description: None,
+ context_flags: Vec::new(),
+ examples: None,
+ notes: None,
+ subsystem: None,
+ sysfs_path: None,
+ permissions: None,
+ capabilities: Vec::new(),
+ state_transitions: Vec::new(),
+ side_effects: Vec::new(),
+ parameters: Vec::new(),
+ return_spec: None,
+ errors: Vec::new(),
+ locks: Vec::new(),
+ struct_specs: Vec::new(),
+ signals: Vec::new(),
+ signal_masks: Vec::new(),
+ constraints: Vec::new(),
+ });
+ Ok(())
+ }
+
+ fn end_api_details(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn description(&mut self, _w: &mut dyn Write, desc: &str) -> std::io::Result<()> {
+ if let Some(details) = &mut self.data.api_details {
+ details.description = Some(desc.to_string());
+ }
+ Ok(())
+ }
+
+ fn long_description(&mut self, _w: &mut dyn Write, desc: &str) -> std::io::Result<()> {
+ if let Some(details) = &mut self.data.api_details {
+ details.long_description = Some(desc.to_string());
+ }
+ Ok(())
+ }
+
+ fn begin_context_flags(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn context_flag(&mut self, _w: &mut dyn Write, flag: &str) -> std::io::Result<()> {
+ if let Some(details) = &mut self.data.api_details {
+ details.context_flags.push(flag.to_string());
+ }
+ Ok(())
+ }
+
+ fn end_context_flags(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_parameters(&mut self, _w: &mut dyn Write, _count: u32) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn end_parameters(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_errors(&mut self, _w: &mut dyn Write, _count: u32) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn end_errors(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn examples(&mut self, _w: &mut dyn Write, examples: &str) -> std::io::Result<()> {
+ if let Some(details) = &mut self.data.api_details {
+ details.examples = Some(examples.to_string());
+ }
+ Ok(())
+ }
+
+ fn notes(&mut self, _w: &mut dyn Write, notes: &str) -> std::io::Result<()> {
+ if let Some(details) = &mut self.data.api_details {
+ details.notes = Some(notes.to_string());
+ }
+ Ok(())
+ }
+
+ fn sysfs_subsystem(&mut self, _w: &mut dyn Write, subsystem: &str) -> std::io::Result<()> {
+ if let Some(details) = &mut self.data.api_details {
+ details.subsystem = Some(subsystem.to_string());
+ }
+ Ok(())
+ }
+
+ fn sysfs_path(&mut self, _w: &mut dyn Write, path: &str) -> std::io::Result<()> {
+ if let Some(details) = &mut self.data.api_details {
+ details.sysfs_path = Some(path.to_string());
+ }
+ Ok(())
+ }
+
+ fn sysfs_permissions(&mut self, _w: &mut dyn Write, perms: &str) -> std::io::Result<()> {
+ if let Some(details) = &mut self.data.api_details {
+ details.permissions = Some(perms.to_string());
+ }
+ Ok(())
+ }
+
+ fn begin_capabilities(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn capability(&mut self, _w: &mut dyn Write, cap: &CapabilitySpec) -> std::io::Result<()> {
+ if let Some(details) = &mut self.data.api_details {
+ details.capabilities.push(cap.clone());
+ }
+ Ok(())
+ }
+
+ fn end_capabilities(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn parameter(&mut self, _w: &mut dyn Write, param: &ParamSpec) -> std::io::Result<()> {
+ if let Some(details) = &mut self.data.api_details {
+ details.parameters.push(param.clone());
+ }
+ Ok(())
+ }
+
+ fn return_spec(&mut self, _w: &mut dyn Write, ret: &ReturnSpec) -> std::io::Result<()> {
+ if let Some(details) = &mut self.data.api_details {
+ details.return_spec = Some(ret.clone());
+ }
+ Ok(())
+ }
+
+ fn error(&mut self, _w: &mut dyn Write, error: &ErrorSpec) -> std::io::Result<()> {
+ if let Some(details) = &mut self.data.api_details {
+ details.errors.push(error.clone());
+ }
+ Ok(())
+ }
+
+ fn begin_signals(&mut self, _w: &mut dyn Write, _count: u32) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn signal(&mut self, _w: &mut dyn Write, signal: &SignalSpec) -> std::io::Result<()> {
+ if let Some(api_details) = &mut self.data.api_details {
+ api_details.signals.push(signal.clone());
+ }
+ Ok(())
+ }
+
+ fn end_signals(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_signal_masks(&mut self, _w: &mut dyn Write, _count: u32) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn signal_mask(&mut self, _w: &mut dyn Write, mask: &SignalMaskSpec) -> std::io::Result<()> {
+ if let Some(api_details) = &mut self.data.api_details {
+ api_details.signal_masks.push(mask.clone());
+ }
+ Ok(())
+ }
+
+ fn end_signal_masks(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_side_effects(&mut self, _w: &mut dyn Write, _count: u32) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn side_effect(&mut self, _w: &mut dyn Write, effect: &SideEffectSpec) -> std::io::Result<()> {
+ if let Some(details) = &mut self.data.api_details {
+ details.side_effects.push(effect.clone());
+ }
+ Ok(())
+ }
+
+ fn end_side_effects(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_state_transitions(&mut self, _w: &mut dyn Write, _count: u32) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn state_transition(
+ &mut self,
+ _w: &mut dyn Write,
+ trans: &StateTransitionSpec,
+ ) -> std::io::Result<()> {
+ if let Some(details) = &mut self.data.api_details {
+ details.state_transitions.push(trans.clone());
+ }
+ Ok(())
+ }
+
+ fn end_state_transitions(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_constraints(&mut self, _w: &mut dyn Write, _count: u32) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn constraint(
+ &mut self,
+ _w: &mut dyn Write,
+ constraint: &ConstraintSpec,
+ ) -> std::io::Result<()> {
+ if let Some(api_details) = &mut self.data.api_details {
+ api_details.constraints.push(constraint.clone());
+ }
+ Ok(())
+ }
+
+ fn end_constraints(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_locks(&mut self, _w: &mut dyn Write, _count: u32) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn lock(&mut self, _w: &mut dyn Write, lock: &LockSpec) -> std::io::Result<()> {
+ if let Some(details) = &mut self.data.api_details {
+ details.locks.push(lock.clone());
+ }
+ Ok(())
+ }
+
+ fn end_locks(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_struct_specs(&mut self, _w: &mut dyn Write, _count: u32) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn struct_spec(&mut self, _w: &mut dyn Write, spec: &StructSpec) -> std::io::Result<()> {
+ if let Some(ref mut details) = self.data.api_details {
+ details.struct_specs.push(spec.clone());
+ }
+ Ok(())
+ }
+
+ fn end_struct_specs(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::extractor::{ErrorSpec, ParamSpec, ReturnSpec};
+
+ fn render_json(f: &mut JsonFormatter) -> String {
+ let mut buf = Vec::new();
+ f.end_document(&mut buf).unwrap();
+ String::from_utf8(buf).unwrap()
+ }
+
+ #[test]
+ fn json_output_is_valid() {
+ let mut f = JsonFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_test").unwrap();
+ f.description(&mut sink, "A test syscall").unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let json = render_json(&mut f);
+
+ // Verify it parses as valid JSON
+ let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
+ assert_eq!(parsed["api_details"]["name"].as_str(), Some("sys_test"));
+ assert_eq!(
+ parsed["api_details"]["description"].as_str(),
+ Some("A test syscall")
+ );
+ }
+
+ #[test]
+ fn json_api_list() {
+ let mut f = JsonFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_list(&mut sink, "Syscalls").unwrap();
+ f.api_item(&mut sink, "sys_open", "syscall").unwrap();
+ f.api_item(&mut sink, "sys_read", "syscall").unwrap();
+ f.end_api_list(&mut sink).unwrap();
+
+ let json = render_json(&mut f);
+ let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
+
+ let apis = parsed["apis"].as_array().unwrap();
+ assert_eq!(apis.len(), 2);
+ assert_eq!(apis[0]["name"].as_str(), Some("sys_open"));
+ assert_eq!(apis[0]["api_type"].as_str(), Some("syscall"));
+ assert_eq!(apis[1]["name"].as_str(), Some("sys_read"));
+ }
+
+ #[test]
+ fn json_special_characters_in_description() {
+ let mut f = JsonFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_test").unwrap();
+ f.description(&mut sink, "Contains \"quotes\" and \\backslashes\\")
+ .unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let json = render_json(&mut f);
+
+ // Must be valid JSON despite special characters
+ let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
+ assert_eq!(
+ parsed["api_details"]["description"].as_str(),
+ Some("Contains \"quotes\" and \\backslashes\\")
+ );
+ }
+
+ #[test]
+ fn json_special_characters_in_name() {
+ let mut f = JsonFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_list(&mut sink, "APIs").unwrap();
+ // Names with underscores (common in kernel) and unusual strings
+ f.api_item(&mut sink, "sys_new\tline", "syscall").unwrap();
+ f.end_api_list(&mut sink).unwrap();
+
+ let json = render_json(&mut f);
+
+ // Must parse correctly; serde_json handles escaping for us
+ let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
+ assert_eq!(parsed["apis"][0]["name"].as_str(), Some("sys_new\tline"));
+ }
+
+ #[test]
+ fn json_parameters_serialized() {
+ let mut f = JsonFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_write").unwrap();
+ f.begin_parameters(&mut sink, 2).unwrap();
+ f.parameter(
+ &mut sink,
+ &ParamSpec {
+ index: 0,
+ name: "fd".to_string(),
+ type_name: "unsigned int".to_string(),
+ description: "file descriptor".to_string(),
+ flags: 1,
+ param_type: 2,
+ constraint_type: 0,
+ constraint: None,
+ min_value: Some(0),
+ max_value: Some(1024),
+ valid_mask: None,
+ enum_values: vec![],
+ size: None,
+ alignment: None,
+ size_param_idx: None,
+ },
+ )
+ .unwrap();
+ f.end_parameters(&mut sink).unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let json = render_json(&mut f);
+ let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
+
+ let params = parsed["api_details"]["parameters"].as_array().unwrap();
+ assert_eq!(params.len(), 1);
+ assert_eq!(params[0]["name"].as_str(), Some("fd"));
+ assert_eq!(params[0]["param_type"].as_u64(), Some(2));
+ }
+
+ #[test]
+ fn json_errors_serialized() {
+ let mut f = JsonFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_read").unwrap();
+ f.begin_errors(&mut sink, 1).unwrap();
+ f.error(
+ &mut sink,
+ &ErrorSpec {
+ error_code: -9,
+ name: "EBADF".to_string(),
+ condition: "fd is not valid".to_string(),
+ description: "Bad file descriptor".to_string(),
+ },
+ )
+ .unwrap();
+ f.end_errors(&mut sink).unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let json = render_json(&mut f);
+ let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
+
+ let errors = parsed["api_details"]["errors"].as_array().unwrap();
+ assert_eq!(errors.len(), 1);
+ assert_eq!(errors[0]["name"].as_str(), Some("EBADF"));
+ assert_eq!(errors[0]["error_code"].as_i64(), Some(-9));
+ }
+
+ #[test]
+ fn json_empty_details_omits_empty_fields() {
+ let mut f = JsonFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_empty").unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let json = render_json(&mut f);
+ let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
+
+ // description should not be present (skip_serializing_if = Option::is_none)
+ assert!(parsed["api_details"]["description"].is_null());
+ // parameters empty array should not be present (skip_serializing_if = Vec::is_empty)
+ assert!(parsed["api_details"]["parameters"].is_null());
+ // errors empty array should not be present
+ assert!(parsed["api_details"]["errors"].is_null());
+ }
+
+ #[test]
+ fn json_braces_balance() {
+ let mut f = JsonFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_balanced").unwrap();
+ f.description(&mut sink, "Test braces balance").unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let json = render_json(&mut f);
+
+ let open_braces = json.chars().filter(|&c| c == '{').count();
+ let close_braces = json.chars().filter(|&c| c == '}').count();
+ assert_eq!(open_braces, close_braces, "Braces are unbalanced");
+
+ let open_brackets = json.chars().filter(|&c| c == '[').count();
+ let close_brackets = json.chars().filter(|&c| c == ']').count();
+ assert_eq!(open_brackets, close_brackets, "Brackets are unbalanced");
+ }
+
+ #[test]
+ fn json_return_spec_serialized() {
+ let mut f = JsonFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_open").unwrap();
+ f.return_spec(
+ &mut sink,
+ &ReturnSpec {
+ type_name: "int".to_string(),
+ description: "file descriptor on success".to_string(),
+ return_type: 1,
+ check_type: 3,
+ success_value: Some(0),
+ success_min: None,
+ success_max: None,
+ error_values: vec![-1],
+ },
+ )
+ .unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let json = render_json(&mut f);
+ let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
+
+ let ret = &parsed["api_details"]["return_spec"];
+ assert_eq!(ret["type_name"].as_str(), Some("int"));
+ assert_eq!(ret["check_type"].as_u64(), Some(3));
+ }
+
+ #[test]
+ fn json_unicode_in_description() {
+ let mut f = JsonFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_uni").unwrap();
+ f.description(&mut sink, "Supports unicode: \u{00e9}\u{00e8}\u{00ea}")
+ .unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let json = render_json(&mut f);
+ let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
+ assert!(parsed["api_details"]["description"]
+ .as_str()
+ .unwrap()
+ .contains('\u{00e9}'));
+ }
+
+ #[test]
+ fn json_escapes_embedded_newlines() {
+ let mut f = JsonFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_test").unwrap();
+ f.long_description(&mut sink, "One.\n\n- a\n- b").unwrap();
+ f.examples(&mut sink, "a();\nb();").unwrap();
+ f.notes(&mut sink, "Note one.\n\nNote two.").unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let json = render_json(&mut f);
+ assert!(json.contains(r#""examples": "a();\nb();""#));
+ let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
+ assert_eq!(
+ parsed["api_details"]["long_description"].as_str(),
+ Some("One.\n\n- a\n- b")
+ );
+ assert_eq!(
+ parsed["api_details"]["notes"].as_str(),
+ Some("Note one.\n\nNote two.")
+ );
+ }
+}
diff --git a/tools/kapi/src/formatter/mod.rs b/tools/kapi/src/formatter/mod.rs
new file mode 100644
index 0000000000000..8d375cb310b0b
--- /dev/null
+++ b/tools/kapi/src/formatter/mod.rs
@@ -0,0 +1,220 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+
+use crate::extractor::{
+ CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, ParamSpec, ReturnSpec, SideEffectSpec,
+ SignalMaskSpec, SignalSpec, StateTransitionSpec, StructSpec,
+};
+use std::io::Write;
+
+mod json;
+mod plain;
+mod rst;
+
+pub use json::JsonFormatter;
+pub use plain::PlainFormatter;
+pub use rst::RstFormatter;
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum OutputFormat {
+ Plain,
+ Json,
+ Rst,
+}
+
+impl std::str::FromStr for OutputFormat {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s.to_lowercase().as_str() {
+ "plain" => Ok(OutputFormat::Plain),
+ "json" => Ok(OutputFormat::Json),
+ "rst" => Ok(OutputFormat::Rst),
+ _ => Err(format!("Unknown output format: {}", s)),
+ }
+ }
+}
+
+/// Names of the KAPI_PARAM_* flags set in `flags`.
+fn param_flag_names(flags: u32) -> Vec<&'static str> {
+ const IN: u32 = 1 << 0;
+ const OUT: u32 = 1 << 1;
+ const OTHER: [(u32, &str); 6] = [
+ (1 << 3, "OPTIONAL"),
+ (1 << 4, "CONST"),
+ (1 << 5, "VOLATILE"),
+ (1 << 6, "USER"),
+ (1 << 7, "DMA"),
+ (1 << 8, "ALIGNED"),
+ ];
+
+ let mut names = Vec::new();
+ if flags & (IN | OUT) == IN | OUT {
+ names.push("INOUT");
+ } else if flags & IN != 0 {
+ names.push("IN");
+ } else if flags & OUT != 0 {
+ names.push("OUT");
+ }
+ names.extend(
+ OTHER
+ .iter()
+ .filter(|(bit, _)| flags & bit != 0)
+ .map(|(_, name)| *name),
+ );
+ names
+}
+
+/// Text for a parameter's bounds; either end may be unset.
+fn range_text(min: Option<i64>, max: Option<i64>) -> Option<String> {
+ match (min, max) {
+ (Some(min), Some(max)) => Some(format!("{min} to {max}")),
+ (Some(min), None) => Some(format!(">= {min}")),
+ (None, Some(max)) => Some(format!("<= {max}")),
+ (None, None) => None,
+ }
+}
+
+/// Write `text` line by line, indenting every non-blank line by `indent`.
+fn write_indented(w: &mut dyn Write, indent: &str, text: &str) -> std::io::Result<()> {
+ for line in text.lines() {
+ if line.is_empty() {
+ writeln!(w)?;
+ } else {
+ writeln!(w, "{indent}{line}")?;
+ }
+ }
+ Ok(())
+}
+
+/// Prepare text with embedded newlines for reStructuredText. Wrapped
+/// prose stays in its paragraph, but a bullet list needs a blank line
+/// before its first item and after its last one to be recognised.
+fn rst_text(text: &str) -> String {
+ let mut out: Vec<&str> = Vec::new();
+ let mut prev_bullet = false;
+ for line in text.lines() {
+ let bullet = line.starts_with("- ");
+ if !line.is_empty() && bullet != prev_bullet && out.last().is_some_and(|l| !l.is_empty()) {
+ out.push("");
+ }
+ out.push(line);
+ if !line.is_empty() {
+ prev_bullet = bullet;
+ }
+ }
+ out.join("\n")
+}
+
+pub trait OutputFormatter {
+ fn begin_document(&mut self, w: &mut dyn Write) -> std::io::Result<()>;
+ fn end_document(&mut self, w: &mut dyn Write) -> std::io::Result<()>;
+
+ fn begin_api_list(&mut self, w: &mut dyn Write, title: &str) -> std::io::Result<()>;
+ fn api_item(&mut self, w: &mut dyn Write, name: &str, api_type: &str) -> std::io::Result<()>;
+ fn end_api_list(&mut self, w: &mut dyn Write) -> std::io::Result<()>;
+
+ fn total_specs(&mut self, w: &mut dyn Write, count: usize) -> std::io::Result<()>;
+
+ fn begin_api_details(&mut self, w: &mut dyn Write, name: &str) -> std::io::Result<()>;
+ fn end_api_details(&mut self, w: &mut dyn Write) -> std::io::Result<()>;
+ fn description(&mut self, w: &mut dyn Write, desc: &str) -> std::io::Result<()>;
+ fn long_description(&mut self, w: &mut dyn Write, desc: &str) -> std::io::Result<()>;
+
+ fn begin_context_flags(&mut self, w: &mut dyn Write) -> std::io::Result<()>;
+ fn context_flag(&mut self, w: &mut dyn Write, flag: &str) -> std::io::Result<()>;
+ fn end_context_flags(&mut self, w: &mut dyn Write) -> std::io::Result<()>;
+
+ fn begin_parameters(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()>;
+ fn parameter(&mut self, w: &mut dyn Write, param: &ParamSpec) -> std::io::Result<()>;
+ fn end_parameters(&mut self, w: &mut dyn Write) -> std::io::Result<()>;
+
+ fn return_spec(&mut self, w: &mut dyn Write, ret: &ReturnSpec) -> std::io::Result<()>;
+
+ fn begin_errors(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()>;
+ fn error(&mut self, w: &mut dyn Write, error: &ErrorSpec) -> std::io::Result<()>;
+ fn end_errors(&mut self, w: &mut dyn Write) -> std::io::Result<()>;
+
+ fn examples(&mut self, w: &mut dyn Write, examples: &str) -> std::io::Result<()>;
+ fn notes(&mut self, w: &mut dyn Write, notes: &str) -> std::io::Result<()>;
+
+ // Sysfs-specific methods
+ fn sysfs_subsystem(&mut self, w: &mut dyn Write, subsystem: &str) -> std::io::Result<()>;
+ fn sysfs_path(&mut self, w: &mut dyn Write, path: &str) -> std::io::Result<()>;
+ fn sysfs_permissions(&mut self, w: &mut dyn Write, perms: &str) -> std::io::Result<()>;
+
+ fn begin_capabilities(&mut self, w: &mut dyn Write) -> std::io::Result<()>;
+ fn capability(&mut self, w: &mut dyn Write, cap: &CapabilitySpec) -> std::io::Result<()>;
+ fn end_capabilities(&mut self, w: &mut dyn Write) -> std::io::Result<()>;
+
+ // Signal-related methods
+ fn begin_signals(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()>;
+ fn signal(&mut self, w: &mut dyn Write, signal: &SignalSpec) -> std::io::Result<()>;
+ fn end_signals(&mut self, w: &mut dyn Write) -> std::io::Result<()>;
+
+ fn begin_signal_masks(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()>;
+ fn signal_mask(&mut self, w: &mut dyn Write, mask: &SignalMaskSpec) -> std::io::Result<()>;
+ fn end_signal_masks(&mut self, w: &mut dyn Write) -> std::io::Result<()>;
+
+ // Side effects and state transitions
+ fn begin_side_effects(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()>;
+ fn side_effect(&mut self, w: &mut dyn Write, effect: &SideEffectSpec) -> std::io::Result<()>;
+ fn end_side_effects(&mut self, w: &mut dyn Write) -> std::io::Result<()>;
+
+ fn begin_state_transitions(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()>;
+ fn state_transition(
+ &mut self,
+ w: &mut dyn Write,
+ trans: &StateTransitionSpec,
+ ) -> std::io::Result<()>;
+ fn end_state_transitions(&mut self, w: &mut dyn Write) -> std::io::Result<()>;
+
+ // Constraints and locks
+ fn begin_constraints(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()>;
+ fn constraint(&mut self, w: &mut dyn Write, constraint: &ConstraintSpec)
+ -> std::io::Result<()>;
+ fn end_constraints(&mut self, w: &mut dyn Write) -> std::io::Result<()>;
+
+ fn begin_locks(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()>;
+ fn lock(&mut self, w: &mut dyn Write, lock: &LockSpec) -> std::io::Result<()>;
+ fn end_locks(&mut self, w: &mut dyn Write) -> std::io::Result<()>;
+
+ fn begin_struct_specs(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()>;
+ fn struct_spec(&mut self, w: &mut dyn Write, spec: &StructSpec) -> std::io::Result<()>;
+ fn end_struct_specs(&mut self, w: &mut dyn Write) -> std::io::Result<()>;
+}
+
+pub fn create_formatter(format: OutputFormat) -> Box<dyn OutputFormatter> {
+ match format {
+ OutputFormat::Plain => Box::new(PlainFormatter::new()),
+ OutputFormat::Json => Box::new(JsonFormatter::new()),
+ OutputFormat::Rst => Box::new(RstFormatter::new()),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn param_flags_use_the_header_bit_values() {
+ assert!(param_flag_names(0).is_empty());
+ assert_eq!(param_flag_names(0x1), ["IN"]);
+ assert_eq!(param_flag_names(0x2), ["OUT"]);
+ assert_eq!(param_flag_names(0x3), ["INOUT"]);
+ assert_eq!(param_flag_names(0x42), ["OUT", "USER"]);
+ assert_eq!(param_flag_names(0x9), ["IN", "OPTIONAL"]);
+ assert_eq!(
+ param_flag_names(0x1f8),
+ ["OPTIONAL", "CONST", "VOLATILE", "USER", "DMA", "ALIGNED"]
+ );
+ }
+
+ #[test]
+ fn range_text_shows_one_sided_bounds() {
+ assert_eq!(range_text(Some(0), Some(9)).as_deref(), Some("0 to 9"));
+ assert_eq!(range_text(Some(1), None).as_deref(), Some(">= 1"));
+ assert_eq!(range_text(None, Some(255)).as_deref(), Some("<= 255"));
+ assert_eq!(range_text(None, None), None);
+ }
+}
diff --git a/tools/kapi/src/formatter/plain.rs b/tools/kapi/src/formatter/plain.rs
new file mode 100644
index 0000000000000..0aa03ab8573b6
--- /dev/null
+++ b/tools/kapi/src/formatter/plain.rs
@@ -0,0 +1,679 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+
+use super::OutputFormatter;
+use crate::extractor::{
+ CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, ParamSpec, ReturnSpec, SideEffectSpec,
+ SignalMaskSpec, SignalSpec, StateTransitionSpec,
+};
+use std::io::Write;
+
+pub struct PlainFormatter;
+
+impl PlainFormatter {
+ pub fn new() -> Self {
+ PlainFormatter
+ }
+}
+
+impl OutputFormatter for PlainFormatter {
+ fn begin_document(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn end_document(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_api_list(&mut self, w: &mut dyn Write, title: &str) -> std::io::Result<()> {
+ writeln!(w, "\n{title}:")?;
+ writeln!(w, "{}", "-".repeat(title.len() + 1))
+ }
+
+ fn api_item(&mut self, w: &mut dyn Write, name: &str, _api_type: &str) -> std::io::Result<()> {
+ writeln!(w, " {name}")
+ }
+
+ fn end_api_list(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn total_specs(&mut self, w: &mut dyn Write, count: usize) -> std::io::Result<()> {
+ writeln!(w, "\nTotal specifications found: {count}")
+ }
+
+ fn begin_api_details(&mut self, w: &mut dyn Write, name: &str) -> std::io::Result<()> {
+ writeln!(w, "\nDetailed information for {name}:")?;
+ writeln!(w, "{}=", "=".repeat(25 + name.len()))
+ }
+
+ fn end_api_details(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn description(&mut self, w: &mut dyn Write, desc: &str) -> std::io::Result<()> {
+ writeln!(w, "Description: {desc}")
+ }
+
+ fn long_description(&mut self, w: &mut dyn Write, desc: &str) -> std::io::Result<()> {
+ writeln!(w, "\nDetailed Description:")?;
+ super::write_indented(w, " ", desc)
+ }
+
+ fn begin_context_flags(&mut self, w: &mut dyn Write) -> std::io::Result<()> {
+ writeln!(w, "\nExecution Context:")
+ }
+
+ fn context_flag(&mut self, w: &mut dyn Write, flag: &str) -> std::io::Result<()> {
+ writeln!(w, " - {flag}")
+ }
+
+ fn end_context_flags(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_parameters(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()> {
+ writeln!(w, "\nParameters ({count}):")
+ }
+
+ fn parameter(&mut self, w: &mut dyn Write, param: &ParamSpec) -> std::io::Result<()> {
+ writeln!(
+ w,
+ " [{}] {} ({})",
+ param.index, param.name, param.type_name
+ )?;
+ if !param.description.is_empty() {
+ writeln!(w, " {}", param.description)?;
+ }
+
+ // Display flags
+ let flags = super::param_flag_names(param.flags);
+ if !flags.is_empty() {
+ writeln!(w, " Flags: {}", flags.join(" | "))?;
+ }
+
+ // Display constraints
+ if let Some(constraint) = ¶m.constraint {
+ writeln!(w, " Constraint: {constraint}")?;
+ }
+ if let Some(range) = super::range_text(param.min_value, param.max_value) {
+ writeln!(w, " Range: {range}")?;
+ }
+ if let Some(mask) = param.valid_mask {
+ writeln!(w, " Valid mask: 0x{mask:x}")?;
+ }
+ Ok(())
+ }
+
+ fn end_parameters(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn return_spec(&mut self, w: &mut dyn Write, ret: &ReturnSpec) -> std::io::Result<()> {
+ writeln!(w, "\nReturn Value:")?;
+ writeln!(w, " Type: {}", ret.type_name)?;
+ writeln!(w, " {}", ret.description)?;
+ if let Some(val) = ret.success_value {
+ writeln!(w, " Success value: {val}")?;
+ }
+ if let (Some(min), Some(max)) = (ret.success_min, ret.success_max) {
+ writeln!(w, " Success range: {min} to {max}")?;
+ }
+ Ok(())
+ }
+
+ fn begin_errors(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()> {
+ writeln!(w, "\nPossible Errors ({count}):")
+ }
+
+ fn error(&mut self, w: &mut dyn Write, error: &ErrorSpec) -> std::io::Result<()> {
+ writeln!(w, " {} ({})", error.name, error.error_code)?;
+ if !error.condition.is_empty() {
+ writeln!(w, " Condition: {}", error.condition)?;
+ }
+ if !error.description.is_empty() {
+ writeln!(w, " {}", error.description)?;
+ }
+ Ok(())
+ }
+
+ fn end_errors(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn examples(&mut self, w: &mut dyn Write, examples: &str) -> std::io::Result<()> {
+ writeln!(w, "\nExamples:")?;
+ super::write_indented(w, " ", examples)
+ }
+
+ fn notes(&mut self, w: &mut dyn Write, notes: &str) -> std::io::Result<()> {
+ writeln!(w, "\nNotes:")?;
+ super::write_indented(w, " ", notes)
+ }
+
+ fn sysfs_subsystem(&mut self, w: &mut dyn Write, subsystem: &str) -> std::io::Result<()> {
+ writeln!(w, "Subsystem: {subsystem}")
+ }
+
+ fn sysfs_path(&mut self, w: &mut dyn Write, path: &str) -> std::io::Result<()> {
+ writeln!(w, "Sysfs Path: {path}")
+ }
+
+ fn sysfs_permissions(&mut self, w: &mut dyn Write, perms: &str) -> std::io::Result<()> {
+ writeln!(w, "Permissions: {perms}")
+ }
+
+ fn begin_capabilities(&mut self, w: &mut dyn Write) -> std::io::Result<()> {
+ writeln!(w, "\nRequired Capabilities:")
+ }
+
+ fn capability(&mut self, w: &mut dyn Write, cap: &CapabilitySpec) -> std::io::Result<()> {
+ writeln!(w, " {} ({}) - {}", cap.name, cap.capability, cap.action)?;
+ if !cap.allows.is_empty() {
+ writeln!(w, " Allows: {}", cap.allows)?;
+ }
+ if !cap.without_cap.is_empty() {
+ writeln!(w, " Without capability: {}", cap.without_cap)?;
+ }
+ if let Some(cond) = &cap.check_condition {
+ writeln!(w, " Condition: {cond}")?;
+ }
+ Ok(())
+ }
+
+ fn end_capabilities(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ // Signal-related methods
+ fn begin_signals(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()> {
+ writeln!(w, "\nSignal Specifications ({count}):")
+ }
+
+ fn signal(&mut self, w: &mut dyn Write, signal: &SignalSpec) -> std::io::Result<()> {
+ write!(w, " {} ({})", signal.signal_name, signal.signal_num)?;
+
+ // Display direction (bitmask matching C enum kapi_signal_direction)
+ let mut dirs = Vec::new();
+ if signal.direction & 1 != 0 {
+ dirs.push("RECEIVE");
+ }
+ if signal.direction & 2 != 0 {
+ dirs.push("SEND");
+ }
+ if signal.direction & 4 != 0 {
+ dirs.push("HANDLE");
+ }
+ if signal.direction & 8 != 0 {
+ dirs.push("BLOCK");
+ }
+ if signal.direction & 16 != 0 {
+ dirs.push("IGNORE");
+ }
+ let direction = if dirs.is_empty() {
+ "UNKNOWN".to_string()
+ } else {
+ dirs.join("|")
+ };
+ write!(w, " - {direction}")?;
+
+ // Display action (matching C enum kapi_signal_action)
+ let action = match signal.action {
+ 0 => "DEFAULT",
+ 1 => "TERMINATE",
+ 2 => "COREDUMP",
+ 3 => "STOP",
+ 4 => "CONTINUE",
+ 5 => "CUSTOM",
+ 6 => "RETURN",
+ 7 => "RESTART",
+ 8 => "QUEUE",
+ 9 => "DISCARD",
+ 10 => "TRANSFORM",
+ _ => "UNKNOWN",
+ };
+ writeln!(w, " - {action}")?;
+
+ if let Some(target) = &signal.target {
+ writeln!(w, " Target: {target}")?;
+ }
+ if let Some(condition) = &signal.condition {
+ writeln!(w, " Condition: {condition}")?;
+ }
+ if let Some(desc) = &signal.description {
+ writeln!(w, " {desc}")?;
+ }
+
+ // Display timing
+ let timing = match signal.timing {
+ 0 => "BEFORE",
+ 1 => "DURING",
+ 2 => "AFTER",
+ 3 => "EXIT",
+ _ => "UNKNOWN",
+ };
+ writeln!(w, " Timing: {timing}")?;
+ writeln!(w, " Priority: {}", signal.priority)?;
+
+ if signal.restartable {
+ writeln!(w, " Restartable: yes")?;
+ }
+ if signal.interruptible {
+ writeln!(w, " Interruptible: yes")?;
+ }
+ if let Some(queue) = &signal.queue {
+ writeln!(w, " Queue: {queue}")?;
+ }
+ if signal.sa_flags_required != 0 {
+ writeln!(
+ w,
+ " SA flags required: {:#x}",
+ signal.sa_flags_required
+ )?;
+ }
+ if signal.sa_flags_forbidden != 0 {
+ writeln!(
+ w,
+ " SA flags forbidden: {:#x}",
+ signal.sa_flags_forbidden
+ )?;
+ }
+ if signal.state_required != 0 {
+ writeln!(w, " State required: {:#x}", signal.state_required)?;
+ }
+ if signal.state_forbidden != 0 {
+ writeln!(w, " State forbidden: {:#x}", signal.state_forbidden)?;
+ }
+ if let Some(error) = signal.error_on_signal {
+ writeln!(w, " Error on signal: {error}")?;
+ }
+ if let Some(transform) = signal.transform_to {
+ writeln!(w, " Transform to: {transform}")?;
+ }
+ Ok(())
+ }
+
+ fn end_signals(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_signal_masks(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()> {
+ writeln!(w, "\nSignal Masks ({count}):")
+ }
+
+ fn signal_mask(&mut self, w: &mut dyn Write, mask: &SignalMaskSpec) -> std::io::Result<()> {
+ writeln!(w, " {}", mask.name)?;
+ if !mask.description.is_empty() {
+ writeln!(w, " {}", mask.description)?;
+ }
+ if !mask.signals.is_empty() {
+ let signals: Vec<String> = mask.signals.iter().map(i32::to_string).collect();
+ writeln!(w, " Signals: {}", signals.join(", "))?;
+ }
+ Ok(())
+ }
+
+ fn end_signal_masks(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ // Side effects and state transitions
+ fn begin_side_effects(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()> {
+ writeln!(w, "\nSide Effects ({count}):")
+ }
+
+ fn side_effect(&mut self, w: &mut dyn Write, effect: &SideEffectSpec) -> std::io::Result<()> {
+ writeln!(w, " {} - {}", effect.target, effect.description)?;
+ if let Some(condition) = &effect.condition {
+ writeln!(w, " Condition: {condition}")?;
+ }
+ if effect.reversible {
+ writeln!(w, " Reversible: yes")?;
+ }
+ Ok(())
+ }
+
+ fn end_side_effects(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_state_transitions(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()> {
+ writeln!(w, "\nState Transitions ({count}):")
+ }
+
+ fn state_transition(
+ &mut self,
+ w: &mut dyn Write,
+ trans: &StateTransitionSpec,
+ ) -> std::io::Result<()> {
+ writeln!(
+ w,
+ " {} : {} -> {}",
+ trans.object, trans.from_state, trans.to_state
+ )?;
+ if let Some(condition) = &trans.condition {
+ writeln!(w, " Condition: {condition}")?;
+ }
+ if !trans.description.is_empty() {
+ writeln!(w, " {}", trans.description)?;
+ }
+ Ok(())
+ }
+
+ fn end_state_transitions(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ // Constraints and locks
+ fn begin_constraints(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()> {
+ writeln!(w, "\nAdditional Constraints ({count}):")
+ }
+
+ fn constraint(
+ &mut self,
+ w: &mut dyn Write,
+ constraint: &ConstraintSpec,
+ ) -> std::io::Result<()> {
+ writeln!(w, " {}", constraint.name)?;
+ if !constraint.description.is_empty() {
+ writeln!(w, " {}", constraint.description)?;
+ }
+ if let Some(expr) = &constraint.expression {
+ writeln!(w, " Expression: {expr}")?;
+ }
+ Ok(())
+ }
+
+ fn end_constraints(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_locks(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()> {
+ writeln!(w, "\nLocking Requirements ({count}):")
+ }
+
+ fn lock(&mut self, w: &mut dyn Write, lock: &LockSpec) -> std::io::Result<()> {
+ write!(w, " {}", lock.lock_name)?;
+
+ // Display lock type
+ let lock_type = match lock.lock_type {
+ 0 => "NONE",
+ 1 => "MUTEX",
+ 2 => "SPINLOCK",
+ 3 => "RWLOCK",
+ 4 => "SEQLOCK",
+ 5 => "RCU",
+ 6 => "SEMAPHORE",
+ 7 => "CUSTOM",
+ _ => "UNKNOWN",
+ };
+ writeln!(w, " ({lock_type})")?;
+
+ let scope_str = match lock.scope {
+ 0 => "acquired and released",
+ 1 => "acquired (not released)",
+ 2 => "released (held on entry)",
+ 3 => "held by caller",
+ _ => "unknown",
+ };
+ writeln!(w, " Scope: {scope_str}")?;
+
+ if !lock.description.is_empty() {
+ writeln!(w, " {}", lock.description)?;
+ }
+ Ok(())
+ }
+
+ fn end_locks(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_struct_specs(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()> {
+ writeln!(w, "\nStructure Specifications ({count}):")
+ }
+
+ fn struct_spec(
+ &mut self,
+ w: &mut dyn Write,
+ spec: &crate::extractor::StructSpec,
+ ) -> std::io::Result<()> {
+ writeln!(
+ w,
+ " {} (size={}, align={}):",
+ spec.name, spec.size, spec.alignment
+ )?;
+ if !spec.description.is_empty() {
+ writeln!(w, " {}", spec.description)?;
+ }
+
+ if !spec.fields.is_empty() {
+ writeln!(w, " Fields ({}):", spec.field_count)?;
+ for field in &spec.fields {
+ write!(w, " - {} ({}):", field.name, field.type_name)?;
+ if !field.description.is_empty() {
+ write!(w, " {}", field.description)?;
+ }
+ writeln!(w)?;
+
+ // Show constraints if present
+ if field.min_value != 0 || field.max_value != 0 {
+ writeln!(
+ w,
+ " Range: [{}, {}]",
+ field.min_value, field.max_value
+ )?;
+ }
+ if field.valid_mask != 0 {
+ writeln!(w, " Mask: {:#x}", field.valid_mask)?;
+ }
+ }
+ }
+ Ok(())
+ }
+
+ fn end_struct_specs(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::extractor::{ErrorSpec, ParamSpec, ReturnSpec};
+
+ fn render_plain(f: &mut PlainFormatter, sink: &mut Vec<u8>) -> String {
+ f.end_document(sink).unwrap();
+ String::from_utf8(sink.clone()).unwrap()
+ }
+
+ #[test]
+ fn plain_api_list() {
+ let mut f = PlainFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_list(&mut sink, "System Calls").unwrap();
+ f.api_item(&mut sink, "sys_open", "syscall").unwrap();
+ f.api_item(&mut sink, "sys_read", "syscall").unwrap();
+ f.end_api_list(&mut sink).unwrap();
+ f.total_specs(&mut sink, 2).unwrap();
+
+ let out = render_plain(&mut f, &mut sink);
+ assert!(out.contains("sys_open"));
+ assert!(out.contains("sys_read"));
+ assert!(out.contains("Total specifications found: 2"));
+ }
+
+ #[test]
+ fn plain_api_details() {
+ let mut f = PlainFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_test").unwrap();
+ f.description(&mut sink, "A test syscall").unwrap();
+ f.long_description(&mut sink, "Detailed description here")
+ .unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let out = render_plain(&mut f, &mut sink);
+ assert!(out.contains("sys_test"));
+ assert!(out.contains("A test syscall"));
+ assert!(out.contains("Detailed description here"));
+ }
+
+ #[test]
+ fn plain_parameters() {
+ let mut f = PlainFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_write").unwrap();
+ f.begin_parameters(&mut sink, 1).unwrap();
+ f.parameter(
+ &mut sink,
+ &ParamSpec {
+ index: 0,
+ name: "fd".to_string(),
+ type_name: "unsigned int".to_string(),
+ description: "file descriptor".to_string(),
+ flags: 1,
+ param_type: 2,
+ constraint_type: 0,
+ constraint: None,
+ min_value: None,
+ max_value: None,
+ valid_mask: None,
+ enum_values: vec![],
+ size: None,
+ alignment: None,
+ size_param_idx: None,
+ },
+ )
+ .unwrap();
+ f.end_parameters(&mut sink).unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let out = render_plain(&mut f, &mut sink);
+ assert!(out.contains("fd"));
+ assert!(out.contains("unsigned int"));
+ assert!(out.contains("file descriptor"));
+ }
+
+ #[test]
+ fn plain_errors() {
+ let mut f = PlainFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_test").unwrap();
+ f.begin_errors(&mut sink, 1).unwrap();
+ f.error(
+ &mut sink,
+ &ErrorSpec {
+ error_code: -2,
+ name: "ENOENT".to_string(),
+ condition: "File not found".to_string(),
+ description: "The file does not exist".to_string(),
+ },
+ )
+ .unwrap();
+ f.end_errors(&mut sink).unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let out = render_plain(&mut f, &mut sink);
+ assert!(out.contains("ENOENT"));
+ assert!(out.contains("-2"));
+ assert!(out.contains("File not found"));
+ }
+
+ #[test]
+ fn plain_return_spec() {
+ let mut f = PlainFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_test").unwrap();
+ f.return_spec(
+ &mut sink,
+ &ReturnSpec {
+ type_name: "KAPI_TYPE_INT".to_string(),
+ description: "Returns 0 on success".to_string(),
+ return_type: 1,
+ check_type: 0,
+ success_value: Some(0),
+ success_min: None,
+ success_max: None,
+ error_values: vec![],
+ },
+ )
+ .unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let out = render_plain(&mut f, &mut sink);
+ assert!(out.contains("KAPI_TYPE_INT"));
+ assert!(out.contains("Returns 0 on success"));
+ }
+
+ #[test]
+ fn plain_context_flags() {
+ let mut f = PlainFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_test").unwrap();
+ f.begin_context_flags(&mut sink).unwrap();
+ f.context_flag(&mut sink, "KAPI_CTX_PROCESS").unwrap();
+ f.context_flag(&mut sink, "KAPI_CTX_SLEEPABLE").unwrap();
+ f.end_context_flags(&mut sink).unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let out = render_plain(&mut f, &mut sink);
+ assert!(out.contains("KAPI_CTX_PROCESS"));
+ assert!(out.contains("KAPI_CTX_SLEEPABLE"));
+ }
+
+ #[test]
+ fn plain_signal_mask_lists_its_signals() {
+ let mut f = PlainFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_test").unwrap();
+ f.begin_signal_masks(&mut sink, 1).unwrap();
+ f.signal_mask(
+ &mut sink,
+ &SignalMaskSpec {
+ name: "blocked".to_string(),
+ description: "Blocked while running".to_string(),
+ signals: vec![2, 15],
+ },
+ )
+ .unwrap();
+ f.end_signal_masks(&mut sink).unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let out = render_plain(&mut f, &mut sink);
+ assert!(out.contains(" blocked\n Blocked while running\n Signals: 2, 15\n"));
+ }
+
+ #[test]
+ fn plain_multiline_blocks_are_indented() {
+ let mut f = PlainFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_test").unwrap();
+ f.long_description(&mut sink, "First paragraph.\n\n- item one\n- item two")
+ .unwrap();
+ f.examples(&mut sink, "a();\nif (x) {\n b();\n}").unwrap();
+ f.notes(&mut sink, "Note one.\n\nNote two.").unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let out = render_plain(&mut f, &mut sink);
+ assert!(out
+ .contains("Detailed Description:\n First paragraph.\n\n - item one\n - item two\n"));
+ assert!(out.contains("Examples:\n a();\n if (x) {\n b();\n }\n"));
+ assert!(out.contains("Notes:\n Note one.\n\n Note two.\n"));
+ }
+}
diff --git a/tools/kapi/src/formatter/rst.rs b/tools/kapi/src/formatter/rst.rs
new file mode 100644
index 0000000000000..c208d876c9777
--- /dev/null
+++ b/tools/kapi/src/formatter/rst.rs
@@ -0,0 +1,802 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+
+use super::OutputFormatter;
+use crate::extractor::{
+ CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, ParamSpec, ReturnSpec, SideEffectSpec,
+ SignalMaskSpec, SignalSpec, StateTransitionSpec,
+};
+use std::io::Write;
+
+pub struct RstFormatter;
+
+impl RstFormatter {
+ pub fn new() -> Self {
+ RstFormatter
+ }
+
+ fn section_char(level: usize) -> char {
+ match level {
+ 0 => '=',
+ 1 => '-',
+ 2 => '~',
+ 3 => '^',
+ _ => '"',
+ }
+ }
+}
+
+impl OutputFormatter for RstFormatter {
+ fn begin_document(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn end_document(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_api_list(&mut self, w: &mut dyn Write, title: &str) -> std::io::Result<()> {
+ writeln!(w, "\n{title}")?;
+ writeln!(
+ w,
+ "{}",
+ Self::section_char(0).to_string().repeat(title.len())
+ )?;
+ writeln!(w)
+ }
+
+ fn api_item(&mut self, w: &mut dyn Write, name: &str, api_type: &str) -> std::io::Result<()> {
+ writeln!(w, "* **{name}** (*{api_type}*)")
+ }
+
+ fn end_api_list(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn total_specs(&mut self, w: &mut dyn Write, count: usize) -> std::io::Result<()> {
+ writeln!(w, "\n**Total specifications found:** {count}")
+ }
+
+ fn begin_api_details(&mut self, w: &mut dyn Write, name: &str) -> std::io::Result<()> {
+ writeln!(w, "\n{name}")?;
+ writeln!(
+ w,
+ "{}",
+ Self::section_char(0).to_string().repeat(name.len())
+ )?;
+ writeln!(w)
+ }
+
+ fn end_api_details(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn description(&mut self, w: &mut dyn Write, desc: &str) -> std::io::Result<()> {
+ writeln!(w, "**{desc}**")?;
+ writeln!(w)
+ }
+
+ fn long_description(&mut self, w: &mut dyn Write, desc: &str) -> std::io::Result<()> {
+ writeln!(w, "{}", super::rst_text(desc))?;
+ writeln!(w)
+ }
+
+ fn begin_context_flags(&mut self, w: &mut dyn Write) -> std::io::Result<()> {
+ let title = "Execution Context";
+ writeln!(w, "{title}")?;
+ writeln!(
+ w,
+ "{}",
+ Self::section_char(1).to_string().repeat(title.len())
+ )?;
+ writeln!(w)
+ }
+
+ fn context_flag(&mut self, w: &mut dyn Write, flag: &str) -> std::io::Result<()> {
+ writeln!(w, "* {flag}")
+ }
+
+ fn end_context_flags(&mut self, w: &mut dyn Write) -> std::io::Result<()> {
+ writeln!(w)
+ }
+
+ fn begin_parameters(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()> {
+ let title = format!("Parameters ({count})");
+ writeln!(w, "{title}")?;
+ writeln!(
+ w,
+ "{}",
+ Self::section_char(1).to_string().repeat(title.len())
+ )?;
+ writeln!(w)
+ }
+
+ fn end_parameters(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_errors(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()> {
+ let title = format!("Possible Errors ({count})");
+ writeln!(w, "{title}")?;
+ writeln!(
+ w,
+ "{}",
+ Self::section_char(1).to_string().repeat(title.len())
+ )?;
+ writeln!(w)
+ }
+
+ fn end_errors(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn examples(&mut self, w: &mut dyn Write, examples: &str) -> std::io::Result<()> {
+ let title = "Examples";
+ writeln!(w, "{title}")?;
+ writeln!(
+ w,
+ "{}",
+ Self::section_char(1).to_string().repeat(title.len())
+ )?;
+ writeln!(w)?;
+ writeln!(w, ".. code-block:: c")?;
+ writeln!(w)?;
+ super::write_indented(w, " ", examples)?;
+ writeln!(w)
+ }
+
+ fn notes(&mut self, w: &mut dyn Write, notes: &str) -> std::io::Result<()> {
+ let title = "Notes";
+ writeln!(w, "{title}")?;
+ writeln!(
+ w,
+ "{}",
+ Self::section_char(1).to_string().repeat(title.len())
+ )?;
+ writeln!(w)?;
+ writeln!(w, "{}", super::rst_text(notes))?;
+ writeln!(w)
+ }
+
+ fn sysfs_subsystem(&mut self, w: &mut dyn Write, subsystem: &str) -> std::io::Result<()> {
+ writeln!(w, ":Subsystem: {subsystem}")?;
+ writeln!(w)
+ }
+
+ fn sysfs_path(&mut self, w: &mut dyn Write, path: &str) -> std::io::Result<()> {
+ writeln!(w, ":Sysfs Path: {path}")?;
+ writeln!(w)
+ }
+
+ fn sysfs_permissions(&mut self, w: &mut dyn Write, perms: &str) -> std::io::Result<()> {
+ writeln!(w, ":Permissions: {perms}")?;
+ writeln!(w)
+ }
+
+ fn begin_capabilities(&mut self, w: &mut dyn Write) -> std::io::Result<()> {
+ let title = "Required Capabilities";
+ writeln!(w, "{title}")?;
+ writeln!(
+ w,
+ "{}",
+ Self::section_char(1).to_string().repeat(title.len())
+ )?;
+ writeln!(w)
+ }
+
+ fn capability(&mut self, w: &mut dyn Write, cap: &CapabilitySpec) -> std::io::Result<()> {
+ writeln!(w, "**{} ({})** - {}", cap.name, cap.capability, cap.action)?;
+ writeln!(w)?;
+ if !cap.allows.is_empty() {
+ writeln!(w, "* **Allows:** {}", cap.allows)?;
+ }
+ if !cap.without_cap.is_empty() {
+ writeln!(w, "* **Without capability:** {}", cap.without_cap)?;
+ }
+ if let Some(cond) = &cap.check_condition {
+ writeln!(w, "* **Condition:** {}", cond)?;
+ }
+ writeln!(w)
+ }
+
+ fn end_capabilities(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn parameter(&mut self, w: &mut dyn Write, param: &ParamSpec) -> std::io::Result<()> {
+ writeln!(
+ w,
+ "**[{}] {}** (*{}*)",
+ param.index, param.name, param.type_name
+ )?;
+ writeln!(w)?;
+ writeln!(w, " {}", param.description)?;
+
+ // Display flags
+ let flags = super::param_flag_names(param.flags);
+ if !flags.is_empty() {
+ writeln!(w, " :Flags: {}", flags.join(", "))?;
+ }
+
+ if let Some(constraint) = ¶m.constraint {
+ writeln!(w, " :Constraint: {}", constraint)?;
+ }
+
+ if let Some(range) = super::range_text(param.min_value, param.max_value) {
+ writeln!(w, " :Range: {}", range)?;
+ }
+
+ writeln!(w)
+ }
+
+ fn return_spec(&mut self, w: &mut dyn Write, ret: &ReturnSpec) -> std::io::Result<()> {
+ writeln!(w, "\nReturn Value")?;
+ writeln!(w, "{}\n", Self::section_char(1).to_string().repeat(12))?;
+ writeln!(w)?;
+ writeln!(w, ":Type: {}", ret.type_name)?;
+ writeln!(w, ":Description: {}", ret.description)?;
+ if let Some(success) = ret.success_value {
+ writeln!(w, ":Success value: {}", success)?;
+ }
+ writeln!(w)
+ }
+
+ fn error(&mut self, w: &mut dyn Write, error: &ErrorSpec) -> std::io::Result<()> {
+ writeln!(w, "**{}** ({})", error.name, error.error_code)?;
+ writeln!(w)?;
+ writeln!(w, " :Condition: {}", error.condition)?;
+ if !error.description.is_empty() {
+ writeln!(w, " :Description: {}", error.description)?;
+ }
+ writeln!(w)
+ }
+
+ fn begin_signals(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()> {
+ let title = format!("Signals ({count})");
+ writeln!(w, "{title}")?;
+ writeln!(
+ w,
+ "{}",
+ Self::section_char(1).to_string().repeat(title.len())
+ )?;
+ writeln!(w)
+ }
+
+ fn signal(&mut self, w: &mut dyn Write, signal: &SignalSpec) -> std::io::Result<()> {
+ write!(w, "* **{}**", signal.signal_name)?;
+ if signal.signal_num != 0 {
+ write!(w, " ({})", signal.signal_num)?;
+ }
+ writeln!(w)?;
+
+ // Direction (bitmask matching C enum kapi_signal_direction)
+ let mut dirs = Vec::new();
+ if signal.direction & 1 != 0 {
+ dirs.push("receive");
+ }
+ if signal.direction & 2 != 0 {
+ dirs.push("send");
+ }
+ if signal.direction & 4 != 0 {
+ dirs.push("handle");
+ }
+ if signal.direction & 8 != 0 {
+ dirs.push("block");
+ }
+ if signal.direction & 16 != 0 {
+ dirs.push("ignore");
+ }
+ let direction = if dirs.is_empty() {
+ "unknown".to_string()
+ } else {
+ dirs.join(", ")
+ };
+ writeln!(w, " :Direction: {}", direction)?;
+
+ // Action (matching C enum kapi_signal_action)
+ let action = match signal.action {
+ 0 => "default",
+ 1 => "terminate",
+ 2 => "coredump",
+ 3 => "stop",
+ 4 => "continue",
+ 5 => "custom",
+ 6 => "return",
+ 7 => "restart",
+ 8 => "queue",
+ 9 => "discard",
+ 10 => "transform",
+ _ => "unknown",
+ };
+ writeln!(w, " :Action: {}", action)?;
+
+ if let Some(target) = &signal.target {
+ writeln!(w, " :Target: {}", target)?;
+ }
+ if let Some(cond) = &signal.condition {
+ writeln!(w, " :Condition: {}", cond)?;
+ }
+ if let Some(desc) = &signal.description {
+ writeln!(w, " :Description: {}", desc)?;
+ }
+ let timing = match signal.timing {
+ 0 => "before",
+ 1 => "during",
+ 2 => "after",
+ 3 => "exit",
+ _ => "",
+ };
+ if !timing.is_empty() {
+ writeln!(w, " :Timing: {}", timing)?;
+ }
+ if signal.priority != 0 {
+ writeln!(w, " :Priority: {}", signal.priority)?;
+ }
+ if signal.interruptible {
+ writeln!(w, " :Interruptible: yes")?;
+ }
+ if signal.restartable {
+ writeln!(w, " :Restartable: yes")?;
+ }
+ if let Some(queue) = &signal.queue {
+ writeln!(w, " :Queue: {}", queue)?;
+ }
+ if signal.sa_flags_required != 0 {
+ writeln!(w, " :SA flags required: {:#x}", signal.sa_flags_required)?;
+ }
+ if signal.sa_flags_forbidden != 0 {
+ writeln!(w, " :SA flags forbidden: {:#x}", signal.sa_flags_forbidden)?;
+ }
+ if signal.state_required != 0 {
+ writeln!(w, " :State required: {:#x}", signal.state_required)?;
+ }
+ if signal.state_forbidden != 0 {
+ writeln!(w, " :State forbidden: {:#x}", signal.state_forbidden)?;
+ }
+ if let Some(error) = signal.error_on_signal {
+ writeln!(w, " :Error on signal: {}", error)?;
+ }
+ if let Some(transform) = signal.transform_to {
+ writeln!(w, " :Transform to: {}", transform)?;
+ }
+ writeln!(w)
+ }
+
+ fn end_signals(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_signal_masks(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()> {
+ let title = format!("Signal Masks ({count})");
+ writeln!(w, "{title}")?;
+ writeln!(
+ w,
+ "{}",
+ Self::section_char(1).to_string().repeat(title.len())
+ )?;
+ writeln!(w)
+ }
+
+ fn signal_mask(&mut self, w: &mut dyn Write, mask: &SignalMaskSpec) -> std::io::Result<()> {
+ writeln!(w, "* **{}**", mask.name)?;
+ if !mask.description.is_empty() {
+ writeln!(w, " {}", mask.description)?;
+ }
+ if !mask.signals.is_empty() {
+ let signals: Vec<String> = mask.signals.iter().map(i32::to_string).collect();
+ writeln!(w, " Signals: {}", signals.join(", "))?;
+ }
+ writeln!(w)
+ }
+
+ fn end_signal_masks(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_side_effects(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()> {
+ let title = format!("Side Effects ({count})");
+ writeln!(w, "{}\n", title)?;
+ writeln!(
+ w,
+ "{}\n",
+ Self::section_char(1).to_string().repeat(title.len())
+ )
+ }
+
+ fn side_effect(&mut self, w: &mut dyn Write, effect: &SideEffectSpec) -> std::io::Result<()> {
+ write!(w, "* **{}**", effect.target)?;
+ if effect.reversible {
+ write!(w, " *(reversible)*")?;
+ }
+ writeln!(w)?;
+ writeln!(w, " {}", effect.description)?;
+ if let Some(cond) = &effect.condition {
+ writeln!(w, " :Condition: {}", cond)?;
+ }
+ writeln!(w)
+ }
+
+ fn end_side_effects(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_state_transitions(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()> {
+ let title = format!("State Transitions ({count})");
+ writeln!(w, "{}\n", title)?;
+ writeln!(
+ w,
+ "{}\n",
+ Self::section_char(1).to_string().repeat(title.len())
+ )
+ }
+
+ fn state_transition(
+ &mut self,
+ w: &mut dyn Write,
+ trans: &StateTransitionSpec,
+ ) -> std::io::Result<()> {
+ writeln!(
+ w,
+ "* **{}**: {} → {}",
+ trans.object, trans.from_state, trans.to_state
+ )?;
+ writeln!(w, " {}", trans.description)?;
+ if let Some(cond) = &trans.condition {
+ writeln!(w, " :Condition: {}", cond)?;
+ }
+ writeln!(w)
+ }
+
+ fn end_state_transitions(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_constraints(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()> {
+ let title = format!("Constraints ({count})");
+ writeln!(w, "{title}")?;
+ writeln!(
+ w,
+ "{}",
+ Self::section_char(1).to_string().repeat(title.len())
+ )?;
+ writeln!(w)
+ }
+
+ fn constraint(
+ &mut self,
+ w: &mut dyn Write,
+ constraint: &ConstraintSpec,
+ ) -> std::io::Result<()> {
+ writeln!(w, "* **{}**", constraint.name)?;
+ if !constraint.description.is_empty() {
+ writeln!(w, " {}", constraint.description)?;
+ }
+ if let Some(expr) = &constraint.expression {
+ writeln!(w, " :Expression: ``{}``", expr)?;
+ }
+ writeln!(w)
+ }
+
+ fn end_constraints(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_locks(&mut self, w: &mut dyn Write, count: u32) -> std::io::Result<()> {
+ let title = format!("Locks ({count})");
+ writeln!(w, "{}\n", title)?;
+ writeln!(
+ w,
+ "{}\n",
+ Self::section_char(1).to_string().repeat(title.len())
+ )
+ }
+
+ fn lock(&mut self, w: &mut dyn Write, lock: &LockSpec) -> std::io::Result<()> {
+ write!(w, "* **{}**", lock.lock_name)?;
+ let lock_type_str = match lock.lock_type {
+ 1 => " *(mutex)*",
+ 2 => " *(spinlock)*",
+ 3 => " *(rwlock)*",
+ 4 => " *(seqlock)*",
+ 5 => " *(RCU)*",
+ 6 => " *(semaphore)*",
+ 7 => " *(custom)*",
+ _ => "",
+ };
+ writeln!(w, "{}", lock_type_str)?;
+ if !lock.description.is_empty() {
+ writeln!(w, " {}", lock.description)?;
+ }
+ writeln!(w)
+ }
+
+ fn end_locks(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+
+ fn begin_struct_specs(&mut self, w: &mut dyn Write, _count: u32) -> std::io::Result<()> {
+ writeln!(w)?;
+ writeln!(w, "Structure Specifications")?;
+ writeln!(w, "~~~~~~~~~~~~~~~~~~~~~~~")?;
+ writeln!(w)
+ }
+
+ fn struct_spec(
+ &mut self,
+ w: &mut dyn Write,
+ spec: &crate::extractor::StructSpec,
+ ) -> std::io::Result<()> {
+ writeln!(w, "**{}**", spec.name)?;
+ writeln!(w)?;
+
+ if !spec.description.is_empty() {
+ writeln!(w, " {}", spec.description)?;
+ writeln!(w)?;
+ }
+
+ writeln!(w, " :Size: {} bytes", spec.size)?;
+ writeln!(w, " :Alignment: {} bytes", spec.alignment)?;
+ writeln!(w, " :Fields: {}", spec.field_count)?;
+ writeln!(w)?;
+
+ if !spec.fields.is_empty() {
+ for field in &spec.fields {
+ writeln!(w, " * **{}** ({})", field.name, field.type_name)?;
+ if !field.description.is_empty() {
+ writeln!(w, " {}", field.description)?;
+ }
+ if field.min_value != 0 || field.max_value != 0 {
+ writeln!(w, " Range: [{}, {}]", field.min_value, field.max_value)?;
+ }
+ }
+ writeln!(w)?;
+ }
+
+ Ok(())
+ }
+
+ fn end_struct_specs(&mut self, _w: &mut dyn Write) -> std::io::Result<()> {
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::extractor::{ErrorSpec, LockSpec, ParamSpec, ReturnSpec};
+
+ fn render_rst(f: &mut RstFormatter, sink: &mut Vec<u8>) -> String {
+ f.end_document(sink).unwrap();
+ String::from_utf8(sink.clone()).unwrap()
+ }
+
+ #[test]
+ fn rst_api_details_has_heading() {
+ let mut f = RstFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_test").unwrap();
+ f.description(&mut sink, "A test syscall").unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let out = render_rst(&mut f, &mut sink);
+ assert!(out.contains("sys_test"));
+ assert!(out.contains("========"));
+ assert!(out.contains("**A test syscall**"));
+ }
+
+ #[test]
+ fn rst_api_list() {
+ let mut f = RstFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_list(&mut sink, "System Calls").unwrap();
+ f.api_item(&mut sink, "sys_open", "syscall").unwrap();
+ f.api_item(&mut sink, "sys_read", "syscall").unwrap();
+ f.end_api_list(&mut sink).unwrap();
+ f.total_specs(&mut sink, 2).unwrap();
+
+ let out = render_rst(&mut f, &mut sink);
+ assert!(out.contains("sys_open"));
+ assert!(out.contains("sys_read"));
+ }
+
+ #[test]
+ fn rst_parameters() {
+ let mut f = RstFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_write").unwrap();
+ f.begin_parameters(&mut sink, 1).unwrap();
+ f.parameter(
+ &mut sink,
+ &ParamSpec {
+ index: 0,
+ name: "fd".to_string(),
+ type_name: "unsigned int".to_string(),
+ description: "file descriptor".to_string(),
+ flags: 1,
+ param_type: 2,
+ constraint_type: 0,
+ constraint: None,
+ min_value: None,
+ max_value: None,
+ valid_mask: None,
+ enum_values: vec![],
+ size: None,
+ alignment: None,
+ size_param_idx: None,
+ },
+ )
+ .unwrap();
+ f.end_parameters(&mut sink).unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let out = render_rst(&mut f, &mut sink);
+ assert!(out.contains("**[0] fd**"));
+ assert!(out.contains("unsigned int"));
+ assert!(out.contains("file descriptor"));
+ }
+
+ #[test]
+ fn rst_errors() {
+ let mut f = RstFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_test").unwrap();
+ f.begin_errors(&mut sink, 1).unwrap();
+ f.error(
+ &mut sink,
+ &ErrorSpec {
+ error_code: -2,
+ name: "ENOENT".to_string(),
+ condition: "File not found".to_string(),
+ description: "The file does not exist".to_string(),
+ },
+ )
+ .unwrap();
+ f.end_errors(&mut sink).unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let out = render_rst(&mut f, &mut sink);
+ assert!(out.contains("**ENOENT**"));
+ assert!(out.contains("-2"));
+ assert!(out.contains("File not found"));
+ }
+
+ #[test]
+ fn rst_return_spec() {
+ let mut f = RstFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_test").unwrap();
+ f.return_spec(
+ &mut sink,
+ &ReturnSpec {
+ type_name: "KAPI_TYPE_INT".to_string(),
+ description: "Returns 0 on success".to_string(),
+ return_type: 1,
+ check_type: 0,
+ success_value: Some(0),
+ success_min: None,
+ success_max: None,
+ error_values: vec![],
+ },
+ )
+ .unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let out = render_rst(&mut f, &mut sink);
+ assert!(out.contains("KAPI_TYPE_INT"));
+ assert!(out.contains("Returns 0 on success"));
+ assert!(out.contains("Return Value"));
+ }
+
+ #[test]
+ fn rst_context_flags() {
+ let mut f = RstFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_test").unwrap();
+ f.begin_context_flags(&mut sink).unwrap();
+ f.context_flag(&mut sink, "KAPI_CTX_PROCESS").unwrap();
+ f.context_flag(&mut sink, "KAPI_CTX_SLEEPABLE").unwrap();
+ f.end_context_flags(&mut sink).unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let out = render_rst(&mut f, &mut sink);
+ assert!(out.contains("KAPI_CTX_PROCESS"));
+ assert!(out.contains("KAPI_CTX_SLEEPABLE"));
+ assert!(out.contains("Execution Context"));
+ }
+
+ #[test]
+ fn rst_signal_mask_lists_its_signals() {
+ let mut f = RstFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_test").unwrap();
+ f.begin_signal_masks(&mut sink, 1).unwrap();
+ f.signal_mask(
+ &mut sink,
+ &SignalMaskSpec {
+ name: "blocked".to_string(),
+ description: "Blocked while running".to_string(),
+ signals: vec![2, 15],
+ },
+ )
+ .unwrap();
+ f.end_signal_masks(&mut sink).unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let out = render_rst(&mut f, &mut sink);
+ assert!(out.contains("* **blocked**\n Blocked while running\n Signals: 2, 15\n"));
+ }
+
+ #[test]
+ fn rst_examples_are_a_literal_block_with_blank_lines_kept() {
+ let mut f = RstFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_test").unwrap();
+ f.examples(&mut sink, "a();\n b();\n\nc();").unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let out = render_rst(&mut f, &mut sink);
+ assert!(out.contains(".. code-block:: c\n\n a();\n b();\n\n c();\n"));
+ }
+
+ #[test]
+ fn rst_bullets_get_blank_lines_around_the_list() {
+ let mut f = RstFormatter::new();
+ let mut sink = Vec::new();
+
+ f.begin_document(&mut sink).unwrap();
+ f.begin_api_details(&mut sink, "sys_test").unwrap();
+ f.long_description(&mut sink, "Intro:\n- one\n- two\n\nTail.")
+ .unwrap();
+ f.notes(&mut sink, "Para one.\n\n- a\n- b\nafter").unwrap();
+ f.end_api_details(&mut sink).unwrap();
+
+ let out = render_rst(&mut f, &mut sink);
+ assert!(out.contains("Intro:\n\n- one\n- two\n\nTail.\n"));
+ assert!(out.contains("Para one.\n\n- a\n- b\n\nafter\n"));
+ }
+
+ #[test]
+ fn rst_lock_types_follow_the_kernel_enum() {
+ let mut f = RstFormatter::new();
+ let mut sink = Vec::new();
+
+ for (lock_type, label) in [
+ (1, "mutex"),
+ (2, "spinlock"),
+ (3, "rwlock"),
+ (4, "seqlock"),
+ (5, "RCU"),
+ (6, "semaphore"),
+ (7, "custom"),
+ ] {
+ f.lock(
+ &mut sink,
+ &LockSpec {
+ lock_name: "l".to_string(),
+ lock_type,
+ scope: 0,
+ description: String::new(),
+ },
+ )
+ .unwrap();
+ let out = String::from_utf8(std::mem::take(&mut sink)).unwrap();
+ assert_eq!(out, format!("* **l** *({label})*\n\n"));
+ }
+ }
+}
diff --git a/tools/kapi/src/main.rs b/tools/kapi/src/main.rs
new file mode 100644
index 0000000000000..93ed11b508e9f
--- /dev/null
+++ b/tools/kapi/src/main.rs
@@ -0,0 +1,123 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+
+//! kapi - Kernel API Specification Tool
+//!
+//! This tool extracts and displays kernel API specifications from multiple sources:
+//! - Kernel source code (kerneldoc blocks)
+//! - Compiled vmlinux binaries (`.kapi_specs` ELF section)
+//! - Running kernel via debugfs
+
+use anyhow::Result;
+use clap::Parser;
+use std::io::{self, Write};
+
+mod extractor;
+mod formatter;
+
+use extractor::{ApiExtractor, DebugfsExtractor, SourceExtractor, VmlinuxExtractor};
+use formatter::{create_formatter, OutputFormat};
+
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+struct Args {
+ /// Path to the vmlinux file
+ #[arg(long, value_name = "PATH", group = "input")]
+ vmlinux: Option<String>,
+
+ /// Path to kernel source directory or file
+ #[arg(long, value_name = "PATH", group = "input")]
+ source: Option<String>,
+
+ /// Path to debugfs (defaults to /sys/kernel/debug if not specified)
+ #[arg(long, value_name = "PATH", group = "input")]
+ debugfs: Option<String>,
+
+ /// Optional: Name of specific API to show details for
+ api_name: Option<String>,
+
+ /// Output format
+ #[arg(long, short = 'f', default_value = "plain")]
+ format: String,
+}
+
+fn main() -> Result<()> {
+ let args = Args::parse();
+
+ let output_format: OutputFormat = args
+ .format
+ .parse()
+ .map_err(|e: String| anyhow::anyhow!(e))?;
+
+ let extractor: Box<dyn ApiExtractor> = match (&args.vmlinux, &args.source, &args.debugfs) {
+ (Some(vmlinux_path), None, None) => Box::new(VmlinuxExtractor::new(vmlinux_path)?),
+ (None, Some(source_path), None) => Box::new(SourceExtractor::new(source_path)?),
+ (None, None, Some(_) | None) => {
+ // If debugfs is specified or no input is provided, use debugfs
+ Box::new(DebugfsExtractor::new(args.debugfs.clone())?)
+ }
+ _ => {
+ anyhow::bail!("Please specify only one of --vmlinux, --source, or --debugfs")
+ }
+ };
+
+ display_apis(extractor.as_ref(), args.api_name, output_format)
+}
+
+fn display_apis(
+ extractor: &dyn ApiExtractor,
+ api_name: Option<String>,
+ output_format: OutputFormat,
+) -> Result<()> {
+ let mut formatter = create_formatter(output_format);
+ let mut stdout = io::stdout();
+
+ formatter.begin_document(&mut stdout)?;
+
+ if let Some(api_name_req) = api_name {
+ // Use the extractor to display API details
+ if let Some(_spec) = extractor.extract_by_name(&api_name_req)? {
+ extractor.display_api_details(&api_name_req, &mut *formatter, &mut stdout)?;
+ } else {
+ eprintln!("API '{}' not found.", api_name_req);
+ if output_format == OutputFormat::Plain {
+ writeln!(stdout, "\nAvailable APIs:")?;
+ for spec in extractor.extract_all()? {
+ writeln!(stdout, " {} ({})", spec.name, spec.api_type)?;
+ }
+ }
+ std::process::exit(1);
+ }
+ } else {
+ // Display list of APIs using the extractor
+ let all_specs = extractor.extract_all()?;
+
+ // Helper to display API list for a specific type
+ let mut display_api_type = |api_type: &str, title: &str| -> Result<()> {
+ let filtered: Vec<_> = all_specs
+ .iter()
+ .filter(|s| s.api_type == api_type)
+ .collect();
+
+ if !filtered.is_empty() {
+ formatter.begin_api_list(&mut stdout, title)?;
+ for spec in filtered {
+ formatter.api_item(&mut stdout, &spec.name, &spec.api_type)?;
+ }
+ formatter.end_api_list(&mut stdout)?;
+ }
+ Ok(())
+ };
+
+ display_api_type("syscall", "System Calls")?;
+ display_api_type("ioctl", "IOCTLs")?;
+ display_api_type("function", "Functions")?;
+ display_api_type("sysfs", "Sysfs Attributes")?;
+
+ formatter.total_specs(&mut stdout, all_specs.len())?;
+ }
+
+ formatter.end_document(&mut stdout)?;
+
+ Ok(())
+}
--
2.53.0
^ permalink raw reply related [flat|nested] 21+ messages in thread